Golang 语言 method 接收者使用值类型和指针类型的区别

简介: Golang 语言 method 接收者使用值类型和指针类型的区别

介绍

在 Golang 语言中,function 的参数和 method 的接收者都可以选择使用值传递和指针传递(“引用传递”),需要注意的是,其中指针传递是传递的指针值的副本,而不是指针指向的数据的副本。也就是说 Golang 语言和 C 系的所有语言相同,一切传递都是值传递。本文我们主要介绍 method 的接收者怎么选择使用值类型和指针类型。

method 接收者的类型选择

在使用关键字 type 定义的类型上定义 method,method 的接收者也可以作为 method 的参数,类似于 function 的参数,所以 method 的接收者和 function 参数一样,我们也需要考虑选择使用值类型和指针类型。

关于这个问题,我们通常会从两方面去考虑,一是如果该 method 需要修改接收者,那么接收者必须使用指针类型;二是如果接收者占用的内存大小较大,出于性能考虑,我们也会选择使用指针类型的接收者。

除此之外,我们还需考虑一致性。也就是说,如果该类型的某些 method 必须使用指针类型的接收者,其他 method 也应该使用指针类型的接收者。因此无论如何使用该类型,它的方法集都是一致的。

最后,如果接收者是基本类型,切片和小结构体,他们的值类型的内存占用较低,并且易读。所以,该情况下除非 method 的语义需要必须使用指针类型的接收者,否则,我们可以选择使用值类型的接收者。

type User struct {
 name string
}
func (u User) SetNameValueType(str string) {
 fmt.Printf("SetNameValueType() pointer:%p\n", &u) // SetNameValueType() pointer:0xc000096240
 u.name = str
}
func (u *User) SetNamePointerType(str string) {
 fmt.Printf("SetNamePointerType() pointer:%p\n", u) // SetNamePointerType() pointer:0xc000096220
 u.name = str
}
func main () {
 user1 := &User{}
 fmt.Printf("pointer:%p\n", user1) // pointer:0xc000096220
 fmt.Println(user1) // &{}
 user1.SetNameValueType("lucy")
 fmt.Println(user1) // &{}
 user1.SetNamePointerType("lily")
 fmt.Println(user1) // &{lily}
}

阅读上面这段代码,我们可以发现值类型的接收者,调用方拷贝了副本;指针类型的接收者,调用方未拷贝副本。

03

复合类型

map 和 slice 值类似于指针:它们是包含指向底层 map 或 slice 数据的指针的描述符。复制 map 或 slice 值不会复制它指向的数据。需要注意的是,如果超过 slice 的容量,运行时会重新分配一个新内存地址。

map 源码:

type hmap struct {
 count     int // # live cells == size of map.  Must be first (used by len() builtin)
 flags     uint8
 B         uint8  // log_2 of # of buckets (can hold up to loadFactor * 2^B items)
 noverflow uint16 // approximate number of overflow buckets; see incrnoverflow for details
 hash0     uint32 // hash seed
 buckets    unsafe.Pointer // array of 2^B Buckets. may be nil if count==0.
 oldbuckets unsafe.Pointer // previous bucket array of half the size, non-nil only when growing
 nevacuate  uintptr        // progress counter for evacuation (buckets less than this have been evacuated)
 extra *mapextra // optional fields
}

slice 源码:

type slice struct {
 array unsafe.Pointer
 len   int
 cap   int
}

示例代码:

func main () {
 user1 := &User{}
 fmt.Printf("pointer:%p\n", user1) // pointer:0xc000096220
 fmt.Println(user1) // &{}
 user1.SetNameValueType("lucy")
 fmt.Println(user1) // &{}
 user1.SetNamePointerType("lily")
 fmt.Println(user1) // &{lily}
 // m := make(map[int]int)
 m := map[int]int{}
 fmt.Printf("map pointer:%p\n", m) // map pointer:0xc000100180
 m[0] = 1
 fmt.Printf("map pointer:%p\n", m) // map pointer:0xc000100180
 m[1] = 2
 s := make([]int, 0, 1)
 fmt.Printf("slice pointer:%p\n", s) // slice pointer:0xc00001c0a0
 s = append(s, 1)
 fmt.Printf("slice pointer:%p\n", s) // slice pointer:0xc00001c0a0
 s = append(s, 2)
 fmt.Printf("slice pointer:%p\n", s) // slice pointer:0xc00001c0b0
}

阅读上面这段代码,我们可以发现 map 类型未分配新内存地址,使用 append 函数向 slice 中追加元素,当元素个数未超出其容量之前,slice 也未分配新内存地址。

关于接口类型,复制接口值将复制存储在接口值中的对象。如果接口值持有一个结构体,则复制接口值会复制该结构体。如果接口值持有指针,则复制接口值会复制指针,但不会复制它指向的数据。

04

值类型怎么避免拷贝副本

阅读到这里,读者朋友可能会简单认为使用值类型会拷贝副本,使用指针类型不会拷贝副本。实际上,我们可以通过优化代码,在不改变语义的前提下,实现使用值类型也不会拷贝副本。

示例代码:

type User struct {
 name string
}
func (u User) SetNameValueType(str string) {
 fmt.Printf("SetNameValueType() pointer:%p\n", &u) // SetNameValueType() pointer:0xc000096240
 u.name = str
}
func (u User) ValueSetName(str string) User {
 u.name = str
 return u
}
func main () {
 user2 := &User{}
 fmt.Printf("user2 pointer:%p\n", user2) // user2 pointer:0xc000010290
 user2.SetNameValueType("tom") // SetNameValueType() pointer:0xc0000102a0
 user3 := &User{}
 fmt.Printf("user3 pointer:%p\n", user3) // user3 pointer:0xc0000102b0
 user3.ValueSetName("bob")
 fmt.Printf("pointer:%p\n", user3) // pointer:0xc0000102b0
}

阅读上面这段代码,我们发现 User 的 SetNameValueType 方法和 ValueSetName 方法,二者都是值传递,但是 SetNameValueType 方法会拷贝副本,ValueSetName 方法不会拷贝副本。原因是我们给 ValueSetName 方法定义了一个 User 类型的返回值,从而避免了 ValueSetName 方法拷贝副本。

05

总结

本文我们主要介绍了 method 的接收者使用值传递和指针传递的区别,并且讲述了选择使用值传递和指针传递需要考虑的决定因素,也指出了复合类型与值类型的区别。最后,使用一个简单示例演示了通过优化代码,在不改变语义的前提下,怎么实现使用值类型也不会拷贝副本。

推荐阅读:

Golang 语言的编程技巧之类型

Golang 语言的编程技巧之变量

Golang 语言中的非类型安全指针

参考资料:

https://golang.org/doc/faq#pass_by_value 

https://golang.org/doc/faq#methods_on_values_or_pointers 

目录
相关文章
|
8月前
|
Go
Golang语言之管道channel快速入门篇
这篇文章是关于Go语言中管道(channel)的快速入门教程,涵盖了管道的基本使用、有缓冲和无缓冲管道的区别、管道的关闭、遍历、协程和管道的协同工作、单向通道的使用以及select多路复用的详细案例和解释。
262 4
Golang语言之管道channel快速入门篇
|
8月前
|
Go
Golang语言之gRPC程序设计示例
这篇文章是关于Golang语言使用gRPC进行程序设计的详细教程,涵盖了RPC协议的介绍、gRPC环境的搭建、Protocol Buffers的使用、gRPC服务的编写和通信示例。
228 3
Golang语言之gRPC程序设计示例
|
7月前
|
前端开发 中间件 Go
实践Golang语言N层应用架构
【10月更文挑战第2天】本文介绍了如何在Go语言中使用Gin框架实现N层体系结构,借鉴了J2EE平台的多层分布式应用程序模型。文章首先概述了N层体系结构的基本概念,接着详细列出了Go语言中对应的构件名称,包括前端框架(如Vue.js、React)、Gin的处理函数和中间件、依赖注入和配置管理、会话管理和ORM库(如gorm或ent)。最后,提供了具体的代码示例,展示了如何实现HTTP请求处理、会话管理和数据库操作。
106 1
|
8月前
|
Prometheus Cloud Native Go
Golang语言之Prometheus的日志模块使用案例
这篇文章是关于如何在Golang语言项目中使用Prometheus的日志模块的案例,包括源代码编写、编译和测试步骤。
145 3
Golang语言之Prometheus的日志模块使用案例
|
11月前
|
C语言
指针进阶(C语言终)
指针进阶(C语言终)
|
6月前
|
存储 C语言
C语言如何使用结构体和指针来操作动态分配的内存
在C语言中,通过定义结构体并使用指向该结构体的指针,可以对动态分配的内存进行操作。首先利用 `malloc` 或 `calloc` 分配内存,然后通过指针访问和修改结构体成员,最后用 `free` 释放内存,实现资源的有效管理。
566 13
|
8月前
|
存储 人工智能 C语言
C语言程序设计核心详解 第八章 指针超详细讲解_指针变量_二维数组指针_指向字符串指针
本文详细讲解了C语言中的指针,包括指针变量的定义与引用、指向数组及字符串的指针变量等。首先介绍了指针变量的基本概念和定义格式,随后通过多个示例展示了如何使用指针变量来操作普通变量、数组和字符串。文章还深入探讨了指向函数的指针变量以及指针数组的概念,并解释了空指针的意义和使用场景。通过丰富的代码示例和图形化展示,帮助读者更好地理解和掌握C语言中的指针知识。
321 4
|
7月前
|
C语言
无头链表二级指针方式实现(C语言描述)
本文介绍了如何在C语言中使用二级指针实现无头链表,并提供了创建节点、插入、删除、查找、销毁链表等操作的函数实现,以及一个示例程序来演示这些操作。
91 0
|
10月前
|
编译器 C语言
【C语言初阶】指针篇—下
【C语言初阶】指针篇—下
|
10月前
|
存储 C语言
【C语言初阶】指针篇—上
【C语言初阶】指针篇—上