本节包括 Go 的数据类型、类型定义与别名、泛型等内容。
Go 的类型系统设计的非常精炼,既提供了底层内存控制的能力,又保证了高级语言的安全性。在 Go 1.18 引入**泛型(Generics)**之后,这个类型系统变得更加完整,既保留了静态类型的严谨,又具备了处理通用数据的灵活性。
1 数据类型h2
Go 的数据类型大致可分为三类:基础类型、复合类型、接口类型。
1.1 基础类型h3
基础类型是 Go 预定义的、不可再分的原子类型。包括布尔型、数值类型和字符串类型。
布尔型h4
bool:取值只有true和false。
数值型h4
Go 中对数值型的划分非常细致,确保在不同架构下的性能。
- 整型:
- 有符号:
int8int16int32int64以及int(长度取决于系统架构,32位或64位); - 无符号:
uint8(即byte),uint16uint32uint64以及uint; - 特殊:
uintptr(足以容纳指针的无符号整数);
- 有符号:
- 浮点型:
float32float64(Go 默认使用float64); - 复数:
complex64complex128; - 字符:
rune(等价于int32,用于表示一个 Unicode 码点)。
字符串型h4
string:在 Go 中,字符串是不可变的字节序列,默认以 UTF-8 编码存储。
Go 不允许类型之间进行隐式转换,所有的类型转换都必须显式进行。即使是 int 和 int64,也必须写成 int64(myInt)。
当基础类型变量被声明而未被初始化时,将被自动赋予零值:
- 布尔型:
false; - 数值类型:
0; - 字符串类型:
""。
1.2 复合类型h3
复合类型是由基础类型或其他复合类型组合而成的复杂结构。
| 类型 | 说明 | 特点 |
|---|---|---|
| 指针 (Pointer) | *T | 存储变量的内存地址。 |
| 数组 (Array) | [n]T | 固定长度、连续内存的同类型序列。 |
| 切片 (Slice) | []T | 动态数组,是数组的视图,包含指针、长度和容量。 |
| 映射 (Map) | map[K]V | 无序的键值对集合(哈希表)。 |
| 结构体 (Struct) | struct{...} | 不同类型字段的聚合。 |
| 管道 (Channel) | chan T | Goroutine 之间通信的管道,实现并发同步的关键。 |
| 函数 (Function) | func(...) | Go 语言的一等公民,函数本身也是一种类型。 |
当复合类型被声明而未被初始化时,将被自动赋予零值。
- 指针、切片、映射、管道、函数:
nil; - 数组:按照数组存储的元素的类型,赋予每个元素该类型的零值。例如对于
[3]int,其零值为 [0, 0, 0]; - 结构体:所有字段都被赋予其类型的零值。
1.3 接口类型h3
接口类型在 Part 4 Go 接口中已有较为详细的阐述。
2 类型定义与别名h2
在 Go 中,你可以基于已有类型定义新类型。为此有两种完全不同的实现方式。
2.1 类型定义h3
我们可以使用类型定义创建一个全新的类型:
type MyInt int这个操作在 int 的基础上创建了一个全新的类型 MyInt。虽然它们的底层结构都是一样的(int),但是它们是完全不同的两个类型,不能相互赋值或计算,必须显式转换。
如果我们想要在基础类型上加一点新功能,就可以使用自定义类型。因为自定义类型允许被绑定专属方法:
func (m MyInt) IsPositive() bool { return m > 0}
func main() { var a MyInt = 10 a.IsPositive() // true}似乎和实现接口的方式有点类似?是的,在 Go 中,“为类型绑定方法”和“实现接口”其实是同一件事的两面。
我们注意到,无论是为类型绑定方法,还是实现接口,都是在定义函数。不过在函数名前面加了个 (m MyInt) 。(m MyInt) 就是接收者(Receiver)。
其中,
MyInt表示这个方法属于MyInt类型;m叫做接收者参数,代表这个方法所属的实例本身,类似于其他语言中的this或self。
按照接收者是值类型还是指针类型,又分为值接收者和指针接收者:
func (m MyInt) doSomething(){}func (m *MyInt) doSomething(){}这与我们之前说的值传递和引用传递的区别类似。对于值接收者,方法内部拿到的是 m 的一份拷贝,修改 m 不会影响原始变量;而对于指针接收者,方法内部拿到的是内存地址,修改 m 会直接改变原始变量的值。
2.2 类型别名h3
更温和一点的方式是类型别名。就像给类型起了个外号,二者是完全等价的:
type MyInt = int我们所熟知的 any 就是空接口 interface {} 的别名。
3 泛型h2
Go 在 1.18 版本引入了泛型(Generics),这是该语言自发布以来最大的语法特性更新。它允许你在编写代码时不预先指定具体的类型,而是使用类型参数,在调用时才确定具体类型。
泛型的核心意义在于:在保证类型安全(编译时检查)的同时,极大地减少了代码重复。
3.1 核心语法h3
泛型主要由三个部分组成:类型参数(Type Parameters)、类型约束(Type Constraints)和类型实例化(Type Instantiation)。
假设我们要写一个返回切片中最大值的函数。如果没有泛型,你需要为 int、float64 各写一份;有了泛型,只需一份:
func GetMax[T int | float64](s []T) T { var max T for _, v := range s { if v > max { max = v } } return max}
func main() { intSlice := []int{1, 3, 2, 5, 4} floatSlice := []float64{1.1, 3.3, 2.2, 5.5, 4.4}
fmt.Println("Max int:", GetMax(intSlice)) fmt.Println("Max float64:", GetMax(floatSlice))}在这个函数中到处出现的 T 就是类型参数,它代表着一个尚未确定的类型。只有真正向这个函数传递参数后,才能知道这个 T 究竟是什么类型。
为了避免函数参数造成误解,一般使用约定的以下类型参数。你当然可以不遵守约定,但是不建议不遵守约定。
T:表示类型;K:通常在键值对中出现,表示键的类型;V:通常在键值对中出现,表示值的类型;E:通常在数组、切片等中出现,表示元素的类型。
我们还注意到这个函数相较于普通函数,多了一个 [T int | float64] 的结构。这个结构说明这个函数使用了泛型。对于 T,我们知道它是类型参数。而 int | float64 则是对 T 的约束,规定 T 只能是 int 或 float64 类型的其中之一。
我们可以使用以下类型约束:
any:最宽松的约束,等价于interface {},可以接受任何类型;comparable:Go 中内置的约束,只接受支持==和!=操作的类型。如数值、字符串、指针、数组等,不包括切片和映射;- 联合约束:使用
|运算符表示可以接受指定的类型,如int | float64。
事实上,Go 1.18 对接口的定义进行了扩展。从此接口不再是方法的集合,也可以是类型的集合:
type Number interface { ~int | ~int64 | ~float64}当然也能用于约束:
type Number interface { ~int | ~int64 | ~float64}
func GetMax[T Number](s []T) T { var max T for _, v := range s { if v > max { max = v } } return max}这说明类型约束本身也是一种接口。
~符号~ 表示包含底层类型是这些类型的衍生类型。例如,我们刚才定义的 MyInt,其底层类型是 int。如果不带 ~,那么 MyInt 就不符合 int 约束;如果带了 ~,那么只要底层是 int 就可以。
3.2 泛型数据结构h3
除了函数,你还可以定义泛型的结构体、切片或 Map。这在实现通用的数据结构(如:链表、栈、队列、堆)时非常有用。
type Stack[T any] struct { elements []T}
func (s *Stack[T]) Push(v T) { s.elements = append(s.elements, v)}
func main() { intStack := Stack[int]{} intStack.Push(10)
strStack := Stack[string]{} strStack.Push("Hello")}