Golang 语言基础


函数调用

C语言

C 语言同时使用寄存器和栈传递参数,使用 eax 寄存器传递返回值,由于只使用一个寄存器存储返回值,所以 C 语言的函数不能同时返回多个值

优点:极大地减少函数调用的额外开销,CPU 访问栈的开销比访问寄存器高几十倍
缺点:增加了实现的复杂度,需要单独处理函数参数过多的情况

Go

Go 语言使用栈传递参数和返回值

优点:能够降低实现的复杂度并支持多返回值,不需要考虑超过寄存器数量的参数应该如何传递,不需要考虑不同架构上的寄存器差异
缺点:牺牲了函数调用的性能,函数入参和出参的内存空间需要在栈上进行分配

Go的这种实现方式可以让编译器更加简单、更容易维护

  • 传值

函数调用时会对参数进行拷贝,被调用方和调用方两者持有不相关的两份数据

  • 传引用

函数调用时会传递参数的指针,被调用方和调用方两者持有相同的数据,任意一方做出的修改都会影响另一方

Go 语言选择了传值的方式,无论是传递基本类型、结构体还是指针,都会对传递的参数进行拷贝

整型、数组:值传递

结构体、指针
传递结构体时:会拷贝结构体中的全部内容
传递结构体指针时:会拷贝结构体指针

修改结构体指针是改变了指针指向的结构体

结构体在内存的布局

type MyStruct struct {
	i int
	j int
}

func myFunction(ms *MyStruct) {
	ptr := unsafe.Pointer(ms)
	for i := 0; i < 2; i++ {
		c := (*int)(unsafe.Pointer((uintptr(ptr) + uintptr(8*i))))
		*c += i + 1
		fmt.Printf("[%p] %d\n", c, *c)
	}
}

func main() {
	a := &MyStruct{i: 40, j: 50}
	myFunction(a)
	fmt.Printf("[%p] %v\n", a, a)
}

$ go run main.go
[0xc000018180] 41
[0xc000018188] 52
[0xc000018180] &{41 52}

在这段代码中,我们通过指针修改结构体中的成员变量,结构体在内存中是一片连续的空间,指向结构体的指针也是指向这个结构体的首地址。将 MyStruct 指针修改成 int 类型的,那么访问新指针就会返回整型变量 i,将指针移动 8 个字节之后就能获取下一个成员变量 j

  • Go 语言指针参数

将指针作为参数传入某个函数时,函数内部会复制指针,也就是会同时出现两个指针指向原有的内存空间,所以 Go 语言中传指针也是传值

当我们验证了 Go 语言中大多数常见的数据结构之后,其实能够推测出 Go 语言在传递参数时使用了传值的方式,接收方收到参数时会对这些参数进行复制;了解到这一点之后,在传递数组或者内存占用非常大的结构体时,我们应该尽量使用指针作为参数类型来避免发生数据拷贝进而影响性能

接口

Go 语言中接口的实现都是隐式的,实现接口的所有方法就隐式地实现了接口

结构体和指针实现接口

使用指针实现接口,使用结构体初始化变量无法通过编译

当我们使用指针实现接口时,只有指针类型的变量才会实现该接口;当我们使用结构体实现接口时,指针类型和结构体类型都会实现该接口

使用结构体实现接口带来的开销会大于使用指针实现,而动态派发在结构体上的表现非常差,这也提醒我们应当尽量避免使用结构体类型实现接口

动态派发:以接口类型调用时会产生

调用接口类型的方法时,如果编译期间不能确认接口的类型,Go 语言会在运行期间决定具体调用该方法的哪个实现

使用结构体实现接口带来的开销会大于使用指针实现,而动态派发在结构体上的表现非常差,这也提醒我们应当尽量避免使用结构体类型实现接口

使用结构体带来的巨大性能差异不只是接口带来的问题,带来性能问题主要因为 Go 语言在函数调用时是传值的,动态派发的过程只是放大了参数拷贝带来的影响

数据结构

  • eface
type eface struct { // 16 字节
	_type *_type
	data  unsafe.Pointer
}

结构体表示不包含任何方法的 interface{} 类型

  • iface

结构体表示包含方法的接口

type iface struct { // 16 字节
	tab  *itab
	data unsafe.Pointer
}
  • 类型结构体
type _type struct {
	size       uintptr // 类型占用的内存空间,为内存空间的分配提供信息
	ptrdata    uintptr 
	hash       uint32 // 快速确定类型是否相等
	tflag      tflag
	align      uint8
	fieldAlign uint8
	kind       uint8
	equal      func(unsafe.Pointer, unsafe.Pointer) bool // 判断当前类型的多个对象是否相等,该字段是为了减少 Go 语言二进制包大小
	gcdata     *byte
	str        nameOff
	ptrToThis  typeOff
}
type itab struct { // 32 字节
	inter *interfacetype // 成接口类
	_type *_type // 具体类型
	hash  uint32
	_     [4]byte
	fun   [1]uintptr
}

hash _type.hash 的拷贝,当我们想将 interface 类型转换成具体类型时,可以使用该字段快速判断目标类型和具体类型 runtime._type 是否一致

fun 动态大小的数组,它是一个用于动态派发的虚函数表,存储了一组函数指针。虽然该变量被声明成大小固定的数组,但是在使用时会通过原始指针获取其中的数据,所以 fun 数组中保存的元素数量是不确定的

反射

使用反射来动态修改变量、判断类型是否实现了某些接口以及动态调用方法等功能

三大法则

  • 从 interface{} 变量可以反射出反射对象

当我们执行 reflect.ValueOf(1) 时,虽然看起来是获取了基本类型 int 对应的反射类型,但是由于 reflect.TypeOf、reflect.ValueOf 两个方法的入参都是 interface{} 类型,所以在方法执行的过程中发生了类型转换

基本类型 int 会转换成 interface{} 类型

reflect.TypeOf 能获取类型信息
reflect.ValueOf 能获取数据的运行时表示(值)

如果我们知道了一个变量的类型和值,那么就意味着我们知道了这个变量的全部信息

从接口值到反射对象:

1、从基本类型到接口类型的类型转换

2、从接口类型到反射对象的转换

  • 从反射对象可以获取 interface{} 变量

调用 reflect.Value.Interface 方法只能获得 interface{} 类型的变量,如果想要将其还原成最原始的状态还需要经过如下所示的显式类型转换

v := reflect.ValueOf(1)
v.Interface().(int)

从反射对象到接口值:

1、反射对象转换成接口类型

2、通过显式类型转换变成原始类型

  • 要修改反射对象,其值必须可设置

下面代码报错

func main() {
	i := 1
	v := reflect.ValueOf(i)
	v.SetInt(10)
	fmt.Println(i)
}

由于 Go 语言的函数调用都是传值的,所以我们得到的反射对象跟最开始的变量没有任何关系,那么直接修改反射对象无法改变原始变量,程序为了防止错误就会崩溃

想要修改原变量只能使用如下的方法:

func main() {
	i := 1
	v := reflect.ValueOf(&i)
	v.Elem().SetInt(10)
	fmt.Println(i)
}

类型和值

type emptyInterface struct {
	typ  *rtype // 变量的类型
	word unsafe.Pointer // 指向内部封装的数据
}

reflect.TypeOf 函数将传入的变量隐式转换成 reflect.emptyInterface 类型并获取其中存储的类型信息 reflect.rtype

判断类型是否实现接口

想要获得接口类型需要通过以下方式:

reflect.TypeOf((*<interface>)(nil)).Elem()

判断如下代码中的 CustomError 是否实现了 Go 语言标准库中的 error 接口:

type CustomError struct{}

func (*CustomError) Error() string {
	return ""
}

func main() {
	typeOfError := reflect.TypeOf((*error)(nil)).Elem()
	customErrorPtr := reflect.TypeOf(&CustomError{})
	customError := reflect.TypeOf(CustomError{})

	fmt.Println(customErrorPtr.Implements(typeOfError)) // true
	fmt.Println(customError.Implements(typeOfError)) // false
}

方法调用

通过 reflect 包利用反射在运行期间执行方法不是一件容易的事情

func Add(a, b int) int { return a + b }

func main() {
	v := reflect.ValueOf(Add)
	if v.Kind() != reflect.Func {
		return
	}
	t := v.Type()
	argv := make([]reflect.Value, t.NumIn())
	for i := range argv {
		if t.In(i).Kind() != reflect.Int {
			return
		}
		argv[i] = reflect.ValueOf(i)
	}
	result := v.Call(argv)
	if len(result) != 1 || result[0].Kind() != reflect.Int {
		return
	}
	fmt.Println(result[0].Int()) // 1
}
  • 通过 reflect.ValueOf 获取函数 Add 对应的反射对象;
  • 调用 reflect.rtype.NumIn 获取函数的入参个数;
  • 多次调用 reflect.ValueOf 函数逐一设置 argv 数组中的各个参数;
  • 调用反射对象 Add 的 reflect.Value.Call 方法并传入参数列表;
  • 获取返回值数组、验证数组的长度以及类型并打印其中的数据;

转载自

https://draveness.me/golang/


文章作者: 江湖义气
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 江湖义气 !
  目录