Go基础篇:类型系统
目录
- 前言✨
- 一、什么是类型?
- 二、类型特性
- 1、静态类型检查
- 2、类型推断
- 三、类型别名和自定义类型
- 1、类型别名
- 2、自定义类型
- 3、类型别名和自定义类型的区别
- 四、类型底层结构
- 1、类型元数据
- 2、其他描述信息
- 3、uncommontype
- 五、小结
前言✨
前段时间忙着春招面试,现在也算告一段落,找到一家比较心仪的公司实习,开始慢慢回归状态,这后面几章我会学习go1.19版本的语言特性或者机制:类型系统、接口、断言以及反射的内容,也算是补上之前没有深入底层的内容。
一、什么是类型?
类型的概念在不同的编程语言之间是不同的,可以用许多不同的方式来表达,但都有一些相同点。
- 类型是用来定义变量、常量、函数参数、函数返回值等值的属性;
- 在定义的变量上可以执行一组操作,例如:int 类型可以执行 + 和 - 等运算,而对于 string 类型,可以执行连接、空检查等操作;
在Go语言中,类型是用来描述变量、常量、函数参数、函数返回值等值的属性。
它定义了变量或表达式可以存储的数据类型,以及可以对其执行的操作。
Go语言中的类型可以分为基本数据类型和引用类型两种。基本数据类型包括整型、浮点型、布尔型、字符串型等,而引用类型包括数组、切片、结构体、接口、channel等。
类型 | 说明 | 例子 |
---|---|---|
布尔型 | 表示真或假的值 | true, false |
整型 | 表示整数的值,有不同的位数和符号 | int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64 |
浮点型 | 表示小数的值,有不同的精度 | float32, float64 |
复数型 | 表示复数的值,由两个浮点数表示实部和虚部 | complex64, complex128 |
字符串型 | 表示文本的值,使用UTF-8编码 | string |
数组型 | 表示固定长度的元素序列,元素类型相同 | [3]int, [5]string |
切片型 | 表示可变长度的元素序列,元素类型相同 | []int, []string |
映射型 | 表示键值对的集合,键类型和值类型可以不同 | map[string]int, map[int]bool |
结构体型 | 表示一组有名字的字段,字段类型可以不同 | struct {name string; age int} |
接口型 | 表示一组方法的集合,用于实现多态和抽象 | interface {Read() error; Write() error} |
函数型 | 表示一段可执行的代码,可以有参数和返回值 | func(int) int, func(string) (string, error) |
通道型 | 表示用于在不同协程之间传递数据的管道,数据类型相同 | chan int, chan string |
这些都是 Go 语言的内置类型,给内置类型和接口定义方法是错误的,哪怕用类型别名也一样。
二、类型特性
1、静态类型检查
Go语言是一种静态类型的编程语言,这意味着每个变量都有一个明确的类型,不能随意改变。
类型具有静态类型检查的特性,这意味着在编译时就能够检查出类型错误,避免了在运行时出现类型不匹配的错误。
这种特性可以提高程序的可靠性和稳定性,减少调试和修复错误的时间和成本。
例如,如果一个函数需要接收一个整型参数,但是在调用该函数时传入了一个字符串类型的参数。
func IntToString(n int) {
fmt.Println("IntToString")
}
func main() {
var n string
IntToString(n)
}
我们编译一下:
➜ interfaceTest (main) ✗ go build ./main.go
# command-line-arguments
./main.go:13:14: cannot use n (variable of type string) as type int in argument to IntToString
编译器就会在编译时发现这个错误,并提示开发者进行修改。这样就可以避免在运行时出现类型不匹配的错误,提高了程序的可靠性。
2、类型推断
类型推断可以根据变量的值自动推断出其类型,简化了代码的书写。这种特性可以让开发者在不显式指定变量类型的情况下,编写更加简洁、易读的代码。
例如,可以使用以下代码声明一个整型变量:
var x int = 10 // 显式地声明x为int类型,并赋值为10
y := 20 // 语法糖,隐式地声明y为int类型,并赋值为20
在这个例子中,变量 y 的类型会被自动推断为整型。这样就可以避免在声明变量时重复书写类型信息,提高了代码的可读性和简洁性。
三、类型别名和自定义类型
1、类型别名
类型别名是在Go 1.9版本中引入的一个特性,它可以让你为一个已有的类型定义一个新的名称,但是这个新的名称并不是一个新的类型,它只是一个别名,它和原来的类型是完全相同的,可以互换使用。
你可以使用type xxx = type
关键字来定义一个类型别名,例如:
type MyInt = int // 定义一个int类型的别名MyInt
// 比如byte和rune就是类型别名,他们的定义如下:
type byte = uint8
type rune = int32
2、自定义类型
在Go语言中,自定义类型是在Go语言中使用type关键字来定义一个新的类型,它可以基于已有的类型或者引用类型来定义,但是它和原来的类型是不同的,它有自己的方法集合和行为。
可以使用type关键字来定义自定义类型。自定义类型可以是基本类型的别名,也可以是引用类型的新类型。
定义自定义类型的语法如下:
type TypeName BaseType // TypeName是自定义类型的名称,BaseType是基本类型或引用类型。
例如,可以使用以下代码定义一个自定义类型MyInt,它是int类型的别名:
type MyInt int
在这个例子中,MyInt类型和int类型具有相同的底层类型,但是它们是不同的类型。可以使用MyInt类型来声明变量、函数参数、函数返回值等。
除了基本类型的别名,还可以使用自定义类型来定义新的引用类型。例如,可以使用以下代码定义一个自定义类型Person,它是一个结构体类型:
type Person struct {
Name string
Age int
}
在这个例子中,Person类型是一个由Name和Age两个字段组成的结构体类型。可以使用Person类型来声明变量、函数参数、函数返回值等。
3、类型别名和自定义类型的区别
类型别名与自定义类型表面上看只有一个等号的差异,但他们的使用场景却完全不一样,我们通过下面的这段代码来理解它们之间的区别:
type test struct {
}
// 类型别名
type MyTest1 = test
type MyTest2 = MyTest1
// 自定义类型
type MyTest3 test
type MyTest4 MyTest3
func (test) Print(str string) {
fmt.Println("======", str)
}
func (MyTest1) Print1(str string) {
fmt.Println("-----", str)
}
func (MyTest2) Print2(str string) {
fmt.Println("+++++", str)
}
func (MyTest3) Print3(str string) {
fmt.Println("!!!!!", str)
}
func (MyTest4) Print4(str string) {
fmt.Println("、、、、、", str)
}
func TestTypeSystem(t *testing.T) {
var m1 MyTest1
fmt.Printf("type of MyTest1:%T\n", m1)
m1.Print("MyTest1")
m1.Print1("MyTest1")
m1.Print2("MyTest1")
var m2 MyTest2
fmt.Printf("type of MyTest2:%T\n", m2)
m2.Print("MyTest2")
m2.Print1("MyTest2")
m2.Print2("MyTest2")
var m3 MyTest3
fmt.Printf("type of MyTest3:%T\n", m3)
// m3.Print undefined (type MyTest3 has no field or method Print)
//m3.Print("MyTest3")
m3.Print3("MyTest3")
// m3.Print4 undefined (type MyTest3 has no field or method Print4)
//m3.Print4("MyTest3")
var m4 MyTest4
fmt.Printf("type of MyTest4:%T\n", m4)
// m4.Print undefined (type MyTest4 has no field or method Print)
//m4.Print("MyTest4")
// m4.Print3 undefined (type MyTest4 has no field or method Print3)
//m4.Print3("MyTest4")
m4.Print4("MyTest4")
}
测试运行一下
结果显示 m1 和 m2 的类型是 typeSystemTest.test,表示 typeSystemTest 包下定义的 test 类型。m3 的类型是 MyTest3。m4 的类型是 MyTest4。
MyTest1 和 MyTest2 类型只会在代码中存在,编译完成时并不会有MyTest1 和 MyTest2类型,他们底层都是类型。
我们可以看出他们之间最大的区别:
- 类型别名和原来的类型是完全相同的,可以互换使用。
- 自定义类型是一个新的类型,它和原来的类型是不同的,它有自己的方法集合和行为。
四、类型底层结构
1、类型元数据
在Go语言中无论是内置类型还是自定义类型他们都有类型描述,也就是类型元数据
,每种类型元数据都是全局唯一的,_type结构体包含了类型的名称、大小、对齐方式、哈希函数、比较函数等信息。
在runtime._type 中定义了类型元数据的结构体:
type _type struct {
size uintptr
ptrdata uintptr // size of memory prefix holding all pointers
hash uint32
tflag tflag
align uint8
fieldAlign uint8
kind uint8
// function for comparing objects of this type
// (ptr to object A, ptr to object B) -> ==?
equal func(unsafe.Pointer, unsafe.Pointer) bool
// gcdata stores the GC type data for the garbage collector.
// If the KindGCProg bit is set in kind, gcdata is a GC program.
// Otherwise it is a ptrmask bitmap. See mbitmap.go for details.
gcdata *byte
str nameOff
ptrToThis typeOff
}
_type结构体中的字段含义如下:
- size:类型大小,单位是字节;
- ptrdata:指针数据的大小,也就是类型中所有指针类型字段占用内存的大小;
- hash:类型的哈希值,用于运行时的快速比较;
- tflag:类型标志,记录了一些元信息,比如类型是否包含不导出的字段;
- align:类型的对齐方式,也就是类型在内存中的对齐倍数。看这篇文章《Go高性能编程-了解内存对齐以及Go中的类型如何对齐保证》深入了解这个字段的意义;
- fieldalign:类型作为结构体字段时的对齐方式;
- kind:类型的种类,也就是基本类型还是复合类型等;
- alg:类型算法表,包含了哈希函数和比较函数等;
- gcdata:垃圾回收数据,用于标记类型中哪些部分是指针;
- str:类型名称在字符串表中的偏移量;
- ptrToThis:指向该类型指针的类型在类型表中的偏移量。
2、其他描述信息
基本数据类型只需要一个 _type 类型描述结构体就足够,但是引用类型在自身_type 存储之后,还需要额外描述信息来补充。
比如 slicetype 类型元数据结构体:
type slicetype struct {
typ _type
elem *_type
}
在 slice 类型元数据后面还记录了一个 elem 的类型元数据,指向其存储元素的类型元数据。如果是 string 类型的 slice,那 elem 指针指向的就是 string 类型的元数据。
3、uncommontype
在Go语言中,自定义类型的类型元数据可以通过reflect包中的TypeOf函数获取。对于非常规类型(如struct、interface、chan等),其类型元数据可能包含在uncommontype结构体中。
uncommontype结构体中包含了一些非常规类型的元数据信息。
type uncommontype struct {
pkgpath nameOff
mcount uint16 // number of methods
xcount uint16 // number of exported methods
moff uint32 // offset from this uncommontype to [mcount]method
_ uint32 // unused
}
uncommontype结构体中的字段含义如下:
- pkgpath:包路径,记录了类型所在的包;
- mcount:方法数量,记录了类型关联的所有方法的个数;
- xcount:导出方法数量,记录了类型关联的可导出方法的个数;
- moff:方法偏移量,记录了从该uncommontype结构体到方法元数据数组的偏移量;
- _:未使用,占位符。
uncommontype结构体的作用是描述类型的元数据,比如包路径和方法信息。它只有在类型有以下情况之一时才会存在:
- 类型关联了至少一个方法;
- 类型是一个非导出类型(首字母小写);
- 类型是一个反射类型(实现了reflect.Type接口)。
uncommontype结构体通常紧跟在_type结构体后面,通过_type结构体中的tflag字段可以判断是否存在uncommontype结构体。
五、小结
我们知道了 uncommontype结构体定义了方法的个数和uncommontype结构体到方法元数据数组的偏移量。
现在通过实例了解类型系统以及如何找到他的方法数组。
method结构体的作用是描述类型关联的方法的元数据,比如方法名和方法签名。它是一个数组元素,由uncommontype结构体中的moff字段指向:
type method struct {
name nameOff
mtyp typeOff
ifn textOff
tfn textOff
}
method结构体中的字段含义如下:
- name:方法名,记录了方法的名称;
- mtyp:方法类型,记录了方法的类型信息,比如参数和返回值;
- ifn:接口函数,记录了方法在接口中的实现函数的地址;
- tfn:类型函数,记录了方法在类型中的实现函数的地址。
例如,我们基于 []string 定义一个新类型 MySlice,并定义两个方法:
type MySlice []string
func (ms MySlice) Len() {
fmt.Println(len(ms))
}
func (ms MySlice) Cap() {
fmt.Println(cap(ms))
}
func TestType(t *testing.T) {
var ms MySlice
// cannot use []string{…} (value of type []string) as type string in argument to append
// ms = append(ms, []string{"aaaa", "bbbb", "cccc"})
ms = append(ms, MySlice{"aaaa", "bbbb", "cccc"}...)
ms.Len()
ms.Cap()
fmt.Println(ms)
}