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{}
有两类,一类是类型eface
(interface{}
) ,一类是接口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())
}
}