从文本输出理解 Go 方法与接口

想在终端显示一行 hello,平时写 fmt.Println("hello") 就够了。它默认写到标准输出 os.Stdout。下面第二行把这个位置显式写了出来,效果和第一行一样。

go 复制代码
fmt.Println("hello")
fmt.Fprintln(os.Stdout, "hello")

换掉 Fprintln 的第一个参数,就能让同一句 hello 先存到内存里。两种写入位置各有自己的 Write 方法。要看懂 Fprintln 怎样调用它们,先从一次简单的方法调用开始。

同样的点号,两种读法

一笔订单的商品价格是 30 元,运费是 5 元。把下面的代码保存为 main.go,运行 go run main.go。

go 复制代码
package main

import "fmt"

type Order struct {
	Price int
}

func (o Order) Total(shipping int) int {
	return o.Price + shipping
}

func main() {
	order := Order{Price: 30}
	total := order.Total(5)
	fmt.Println(total)
}
text 复制代码
35
调用 点号左边是什么 哪些值传进去了
order.Total(5) order 是一笔订单 order 传给 o,5 传给 shipping
fmt.Println(total) fmt 是 import "fmt" 引入的包名 total 传给 Println

Total 前面的 (o Order) 叫接收者 。这里的 o 收到订单值的一份副本,所以 o.Price 取到 30。加上 shipping 的 5,结果存进 total,再交给 fmt 包里的 Println 显示。

把同一段计算写成普通函数

同样的计算也可以用普通函数表达。把下面的函数加在 main 外,再把计算总价的那行改成 total := calculateTotal(order, 5),运行后仍然得到 35。

go 复制代码
func calculateTotal(order Order, shipping int) int {
	return order.Price + shipping
}

order.Total(5) 把订单放在点号左边,calculateTotal(order, 5) 把订单放进参数列表。两种写法都能算出总价。把 Total 写成 Order 的方法,可以直接读作「算这笔订单的总价」。

先存起来,再显示

把 main.go 换成下面的程序,再运行 go run main.go。

go 复制代码
package main

import (
	"bytes"
	"fmt"
	"os"
)

func main() {
	var buf bytes.Buffer

	fmt.Fprintln(os.Stdout, "hello")
	fmt.Fprintln(&buf, "hello")

	fmt.Print("从内存取出:", buf.String())
}
text 复制代码
hello
从内存取出:hello

第一条 Fprintln 直接把 hello 写到标准输出。第二条把 hello 存到 buf。var buf bytes.Buffer 声明了一个空的缓冲区,可以用来暂存文字。最后一行用 buf.String() 取出文字,再用 fmt.Print 连同前缀一起显示出来。

为什么把 &buf 传给 Fprintln

最后一行读的是原来的 buf。第二次调用 Fprintln 时,写入的代码得找到这同一个缓冲区。&buf 里的 & 表示取地址 ,得到一个指向 buf 的值。拿到这个值,写入的代码就能修改原来的 buf。

这个指向 buf 的值叫指针 。buf 的类型是 bytes.Buffer。在类型名前加上 *,*bytes.Buffer 就表示「指向缓冲区的指针」这种类型。&buf 是取地址的写法,取到的值属于 *bytes.Buffer 类型。

Fprintln 在哪一步把文字写出去

下面是标准库中 Fprintln 的实现。

go 复制代码
func Fprintln(w io.Writer, a ...any) (n int, err error) {
	p := newPrinter()
	p.doPrintln(a)
	n, err = w.Write(p.buf)
	p.free()
	return
}

把声明和 fmt.Fprintln(&buf, "hello") 对起来看。w io.Writer 声明了第一个参数,&buf 传给 w。a ...any 接收后面的参数,这次只有 "hello"。p.doPrintln(a) 把待写入的字节存进 p.buf,再由 w.Write(p.buf) 把它们写出去。

每次调用都走到同一行 w.Write(p.buf)。图里的两条路取决于 w 这次收到的值。

flowchart TD A[第一个参数交给 w] --> B[把 hello 和换行整理成字节] B --> C[调用 w 的 Write] C -->|w 持有标准输出| D[写到标准输出] C -->|w 持有 buf 的地址| E[写进 buf] E --> F[示例最后一行读出 buf 再显示]

走到图中「写进 buf」这一步,调用的是 bytes 包里的 Write。

go 复制代码
func (b *Buffer) Write(p []byte) (n int, err error)

方法名前的 (b *Buffer) 是接收者。b 收到的是 &buf 得到的指针,因此可以修改原来的 buf。*Buffer 是这个指针类型在 bytes 包里的写法,在包外写作 *bytes.Buffer。

方法名后面的 p []byte 是要写入的数据。Write 返回写入的字节数 n 和错误 err,供调用者判断写入是否成功。

为什么 Fprintln 能接收两种值

Fprintln 的第一个参数类型是 io.Writer。这是 io 包定义的接口,规定了传进来的值必须能做什么。在这里,只要求它有这样一个 Write 方法。

go 复制代码
type Writer interface {
	Write(p []byte) (n int, err error)
}

os.Stdout 的类型是 *os.File,&buf 的类型是 *bytes.Buffer。它们都提供了符合要求的 Write 方法,所以都可以传给 Fprintln。

把两次写入都改到内存

把示例里的 fmt.Fprintln(os.Stdout, "hello") 改成 fmt.Fprintln(&buf, "hello"),再从 import 中删掉 "os"。改完后,main 里的写入和显示代码如下。

go 复制代码
fmt.Fprintln(&buf, "hello")
fmt.Fprintln(&buf, "hello")

fmt.Print("从内存取出:", buf.String())

重新运行。两次 Fprintln 现在都往 buf 里写,每次存入一行 hello。最后的 fmt.Print 把两行一次显示出来,所以「从内存取出」这个前缀只出现一次。

text 复制代码
从内存取出:hello
hello

延伸阅读

为什么读取时可以写 buf.String()

String 也接收缓冲区的指针。直接写 buf.String() 时,Go 可以自动取这个变量的地址,效果相当于 (&buf).String()。调用 Fprintln 时,传进去的第一个值必须满足 io.Writer。Write 的接收者是 *bytes.Buffer,所以这里要传 &buf。

Go 里什么时候用方法,什么时候用函数

Go 和 Python 都可以在同一个程序里混用不同的编程风格。Go 常用包组织代码,用普通函数串起处理步骤,把一个类型自己的行为写成方法,再用小接口让同一段代码使用不同的实现。这里 fmt.Fprintln 负责整理文字和发起写入,Write 是各个输出位置自己的方法,io.Writer 让两者能接在一起。

Total 是订单的方法,也是一段只读取输入并返回结果的计算。给它同样的订单价格和运费,就会得到同样的总价。这是函数式写法里常见的做法。把计算改写成 calculateTotal,也可以保留这个性质。

从其他语言的 this 和 self 看接收者

如果在 JavaScript 对象里用 total(shipping) { ... } 定义方法,调用 order.total(5) 时,就可以在方法里用 this 访问当前订单。Python 通常把当前对象写成参数列表里的 self。Go 例子里的 o 也让方法访问这次调用的订单,名字和类型直接写在方法名前的 (o Order) 里。

Rust 也有类似的点号调用。它用 impl 块写明方法属于哪个类型,方法声明的第一个参数 self 表示这次操作的值。

把接收者放在方法名前的写法,在更早的 Oberon-2 里就能看到。Go 共同设计者 Robert Griesemer 在《The Evolution of Go》中用两种语言展示了同一个树节点查找方法。Oberon-2 用 PROCEDURE (t: Tree) Lookup,Go 对应写成 func (t *Node) Lookup。

参考与数据源

相关推荐
QCoding2 小时前
企业级Agent Evaluation实战:Agent能跑就能上线?AI Agent到底怎么测?
后端·spring
苍何3 小时前
让 WorkBuddy 操控浏览器,干活有点猛啊1
后端
IT_陈寒3 小时前
当Java的hashCode遇上Lombok,我的缓存被全清了
前端·人工智能·后端
XW3 小时前
常用提示词(Prompt)框架总结
后端
ServBay3 小时前
DeepSeek 再次崩了,DeepSeek V5正在路上?
后端·deepseek
IT规划师4 小时前
编程术语中的同步与异步:从"等不等"到工程取舍
后端·面试
孟陬5 小时前
Foobar 技术周刊#159:当看不懂代码时,如何高效排障调试。脏页陷阱:一条只读查询,写入数 TB 数据。推测解码:大模型加速利器
前端·人工智能·后端
GoGeekBaird5 小时前
别再给每个 AI 做自我介绍了:我做了一个「个人上下文」管理系统
后端·github·agent
码事漫谈5 小时前
腾讯混元 Hy4 正式版发布了
后端