golang中的值传递与引用传递,如何理解结构体的方法?为什么 T 和 *T 有不同的方法集?

先从一个例子说起

go 复制代码
type Counter struct {
	count int
}

func (c Counter) Inc() {
	c.count++
}

func test1() {
	c := Counter{}

	do := func() {
		for i := 0; i < 10; i++ {
			c.count++
		}
		fmt.Println("done")
	}

	go do()
	go do()
	time.Sleep(3 * time.Second)
	fmt.Println(c.count)
}

func test2() {
	c := Counter{}
	do := func() {
		for i := 0; i < 10; i++ {
			c.Inc()
		}
		fmt.Println("done")
	}

	go do()
	go do()
	time.Sleep(3 * time.Second)
	fmt.Println(c.count)
}

结果:test1() 打印20,test2() 打印0

现在焦点放在 test2() 上。

有人说,Inc() 方法的定义应该使用结构体指针,是的,答案的确如此,但是这底层的原因是什么呢?

对于普通的函数,我们很容易就知道是否需要传入指针,比如

go 复制代码
func f1(obj *Counter)
func f2(obj Counter)

实际上,我们将结构体的方法做个反射就会发现

go 复制代码
type Counter struct {
	count int
}

func (c Counter) Inc() {
	c.count++
}

func test3() {
	c := Counter{}
	reflectValue := reflect.TypeOf(c)
	fmt.Println(reflectValue.Method(0).Type)
}

打印:func(main.Counter)

或者

go 复制代码
type Counter struct {
	count int
}

func (c *Counter) Inc() {
	c.count++
}

func test3() {
	c := &Counter{}
	reflectValue := reflect.TypeOf(c)
	fmt.Println(reflectValue.Method(0).Type)
}

打印:func(*main.Counter)

所以在调用结构体的方法时,c.Inc(),实际上是Inc(c),这样一来,我们就很容易知道该使用Counter还是*Counter来定义Inc()。

还是最开始的代码,没有做修改,我们在Inc中打印c的地址

go 复制代码
func (c Counter) Inc() {
	fmt.Printf("%p\n", &c)
	c.count++
}

执行test2,打印如下

bash 复制代码
0xc0000a6058
0xc0000a60a0
0xc0000a60a8
0xc0000a60b0
0xc0000a60b8
0xc0000a60c0
0xc0000a60c8
0xc0000a60d0
0xc0000a60d8
0xc0000a60e0
0xc00001c050
done
0xc000102000
0xc0000a60e8
0xc0000a6100
0xc0000a6108
0xc0000a6110
0xc0000a6118
0xc0000a6120
0xc0000a6128
0xc0000a6130
done
0

可见每一次执行Inc,c的地址都不一样,这是因为do函数中的c只是外面的c的一个拷贝。

修改后的代码

go 复制代码
func (c *Counter) Inc() {
	fmt.Printf("%p\n", c)
	c.count++
}
bash 复制代码
0xc0000a6058
0xc0000a6058
0xc0000a6058
0xc0000a6058
0xc0000a6058
0xc0000a6058
0xc0000a6058
0xc0000a6058
0xc0000a6058
0xc0000a6058
0xc0000a6058
0xc0000a6058
0xc0000a6058
0xc0000a6058
0xc0000a6058
0xc0000a6058
0xc0000a6058
0xc0000a6058
0xc0000a6058
0xc0000a6058
done
done
20

将 test2() 中的c := &Counter{}改成c := &Counter{},打印信息依旧正确,这是因为golang的语法特性(下面会说到)。

如果将count int换成map或者slice就是另外的情况了,因为这两个类型本身就是引用类型,你不需要显示的声明*,它们就是以引用来工作的。

当函数参数比较大时,使用指针类型可以避免数据拷贝,提升效率。但是使用指针需要注意连贯性,也就是说后续都应该使用指针,否则这个变量就是混乱的。

在Go的 FAQ 中也有关于这一话题的阐述。

为什么 T 和 *T 有不同的方法集?

Go 规范中规定,一个类型T的方法集由所有接收者类型为T的方法组成,而对应指针类型*T的方法集由所有接收者为*T和T的方法组成。也就是说*T的方法集包含T的方法集,反之则不成立。

出现这种区别的原因是,如果接口值包含一个指针*T,则方法调用可以通过取消引用该指针来获取值,但是如果接口值包含一个值T,则方法调用没有安全的方法来获取指针(这样做会允许方法修改接口内值的内容,而这是语言规范所不允许的)

即使在编译器可以获取值的地址并将其传递给方法的情况下,如果该方法修改了该值,则调用者所做的更改将会丢失。

例如,如果以下代码有效:

go 复制代码
var buf bytes.Buffer
io.Copy(buf, os.Stdin)

它会将标准输入复制到的buf的副本中,而不是复制到buf上。这几乎从来不是期望的行为,因此是语言所不允许的。

这种写法会提示错误cannot use buf (variable of type bytes.Buffer) as io.Writer value in argument to io.Copy: bytes.Buffer does not implement io.Writer (method Write has pointer receiver)

实际上是*bytes.Buffer实现了io.Writer ,而不是bytes.Buffer

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

概括就是,*T的方法大都带有修改数据的需要,应该谨慎一些,所以不该对T类型可见。

所以应该改成

go 复制代码
var buf *bytes.Buffer
io.Copy(buf, os.Stdin)
参考

Beware of copying mutexes in Go
Should I define methods on values or pointers?
Why do T and *T have different method sets?

相关推荐
2601_9669496515 小时前
如何根据研究的起止时间截取分钟级行情?Python 实战与 QuantDash 数据处理指南
开发语言·python·pandas·量化交易·1024程序员节·股票数据·quantdash
晨曦中的暮雨17 小时前
限流算法——固定窗口、滑动窗口、漏桶、令牌桶
java·开发语言·算法
喵了几个咪18 小时前
GoWind CMS 拆解:在 Go 里给 TipTap 搭一套 Yjs 协同后端
开发语言·后端·golang
Flynt18 小时前
我把微信运动的排行榜和定点推送复刻了一遍:100万次步数更新只要4秒,坑在别处
redis·后端·性能优化
程序员清风18 小时前
数据处理脚本工程化:日志、配置、命令行与测试
服务器·开发语言·数据库
2601_9628857220 小时前
如何用 Python 把 K 线重采样成任意周期?
开发语言·python
_孤傲_20 小时前
快速掌握 MySQL 语法
数据库·后端·mysql
Wang's Blog20 小时前
Java 项目实战: 交友项目 - 圈子功能实现:总结
java·开发语言·交友
下一站春风里21 小时前
位运算完整学习笔记:按位与(&)+ 异或(^)
开发语言·c++·算法
IT_陈寒1 天前
Java静态变量初始化坑得我头皮发麻
前端·人工智能·后端