Go,我该用值 OR 指针?

TL;DR

  • Go语言的类型系统
  • 作为方法接收者,选哪个?
  • 作为函数参数,选哪个?
    • interface{} 作为函数参数
  • 作为struct field, 选哪个?
  • 彩蛋

Go语言类型系统

buildin

go语言的基本数据类型(buildin类型)结构都定义在**src/reflect/type.go**

const (
	Invalid Kind = iota
	Bool
	Int
	Int8
	Int16
	Int32
	Int64
	Uint
	Uint8
	Uint16
	Uint32
	Uint64
	Uintptr
	Float32
	Float64
	Complex64
	Complex128
	Array
	Chan
	Func
	Interface
	Map
	Ptr
	Slice
	String
	Struct
	UnsafePointer
)

interface{}

interface{} 有两类,一类是类型efaceinterface{}) ,一类是接口iface
所有的定义都有一个公用的embedded类型rtype

type rtype struct {
	size       uintptr
	ptrdata    uintptr // number of bytes in the type that can contain pointers
	hash       uint32  // hash of type; avoids computation in hash tables
	tflag      tflag   // extra type information flags
	align      uint8   // alignment of variable with this type
	fieldAlign uint8   // alignment of struct field with this type
	kind       uint8   // enumeration for C
	// function for comparing objects of this type
	// (ptr to object A, ptr to object B) -> ==?
	equal     func(unsafe.Pointer, unsafe.Pointer) bool
	gcdata    *byte   // garbage collection data
	str       nameOff // string form
	ptrToThis typeOff // type for pointer to this type, may be zero
}

eface

// emptyInterface is the header for an interface{} value.
type emptyInterface struct {
	typ  *rtype // ityp or typ,定义见iface
	word unsafe.Pointer // 指针,指向实际的结构体data address
}

iface

// nonEmptyInterface is the header for an interface value with methods.
type nonEmptyInterface struct {
	// see ../runtime/iface.go:/Itab
	itab *struct {
		ityp *rtype // static interface type,e.g. io.Writer
		typ  *rtype // dynamic concrete type, e.g. *os.File
		hash uint32 // copy of typ.hash, 判断itab是否相等
		_    [4]byte
		fun  [100000]unsafe.Pointer // method table,copy from ityp方法列表
	}
	word unsafe.Pointer // 指针,指向实际的结构体data
}

struct

type structType struct {
	rtype
	pkgPath name
	fields  []structField // sorted by offset
}
// Struct field
type structField struct {
	name        name    // name is always non-empty
	typ         *rtype  // type of field
	offsetEmbed uintptr // byte offset of field<<1 | isEmbedded
}

作为方法接收者,选哪个?

接收者其实是方法的第一个参数;所以如果涉及到对接收者的修改,那么就必须要指针接收者了;不然修改无效

type A struct{
	B int
}
func (a *A) PtrRecv() {
	a.B = 1
}
func (a A) StructRecv() {
	a.B = 1
}
a := A{}
a.StructRecv() // a.B = 0
&a.PtrRecv() // a.B = 1

接收者这个概念,有一个更直观的、简单的理解方式:

// 能改变 a这个对象本身,适合 充血/胀血模型
func (a *A) PtrRecv() => func PtrRecv(a *A) 
// 不能改变 a这个对象本身,看不到修改的效果,适合只读对象/贫血模型
func (a A) StructRecv() => func StructRecv(a A) 
// 不能改变 a这个对象本身,但是能看到修改的效果,适合函数式编程
func (a A) StructRecv() A => func StructRecv(a A) A 

作为函数参数,选哪个?

“传参值拷贝”

Go语言的参数都是值拷贝,传递值copy的是对象本身,传递指针copy的指针的值(Addr)。
所以不想修改参数可以传递值,参数非常大或者需要修改参数传递指针。但是interface{}比较特殊

interface{} 作为函数参数

interface{}看起来是传递值,但是interface{}实际上是指针类型,那么对interface{}的修改就会改变原有的参数对象,这就不符合Go传参值拷贝的语义了
会额外产生一份data的copy在栈上,又满足传参值拷贝,又实际获得了指针指向的对象。但是copy会带来额外的性能开销,这就是为什么生产环境不推荐使用reflect的原因

作为struct field, 选哪个?

比较好的实践是:

  • 如果结构体比较小,且没有频繁的创建和销毁,使用值会好一点(防止并发写入、意外修改)
  • 如果rust写的比较6,能很好的区分清楚变量作用域和所有权,可以用指针(但是仍不推荐)
  • 如果是共享变量、全局变量:开放修改的使用指针,不修改的用值;
  • 如果是比较大的对象:用指针

彩蛋

reflect包,发现了如何判断type可以 == 或者当作map的key,意外的简单

func isReflexive(t *rtype) bool {
	switch t.Kind() {
	case Bool, Int, Int8, Int16, Int32, Int64, Uint, Uint8, Uint16, Uint32, Uint64, Uintptr, Chan, Ptr, String, UnsafePointer: // 这些是OK👌的
		return true
	case Float32, Float64, Complex64, Complex128, Interface: // 这些不OK(发现float都不行,精度的问题吧)
		return false
	case Array: // array会比较elem是否可以
		tt := (*arrayType)(unsafe.Pointer(t))
		return isReflexive(tt.elem)
	case Struct: // struct会递归比较它的各个field是否满足要求
		tt := (*structType)(unsafe.Pointer(t))
		for _, f := range tt.fields {
			if !isReflexive(f.typ) {
				return false
			}
		}
		return true
	default: // 这些是不可以比较的,也不可以当作map的key,否则编译就会报错
		// Func, Map, Slice, Invalid
		panic("isReflexive called on non-key type " + t.String())
	}
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值