ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

GO [ 类型 ]

GO [ 类型 ] 类型前面我们已经学习了 Go 的变量、常量、数据类型、输入输出、条件控制、切片、字符串、映射表、指针、结构体、函数、方法和接口。接下来开始从更底层的角度理解 Go 语言中的一个核心概念类型type。很多初学者会把类型理解成 int、string、bool 这些关键字的名称。这个理解只记住了表面。按照 Go 官方语言规范Types 的定义类型决定了一组值以及这些值可以执行的操作和拥有的方法。简单理解类型决定变量能保存什么值也决定程序允许对这个值做什么。Go 类型系统里最容易混淆的地方不是类型名称本身而是下面这些规则type UserID int 会创建一个新的定义类型type UserID int 只是创建一个别名两个底层类型相同的定义类型仍然是不同类型很多类型转换必须显式写出来无类型常量可以在赋值时根据上下文确定类型泛型中的类型约束描述的是一组允许使用的类型。本章和前面的“数据类型”不同重点不再是逐个介绍 int、string、slice而是解释 Go 类型系统如何判断“相同”“可以赋值”“可以转换”和“可以作为类型参数”。本章按照“官方定义 → 类型声明 → 定义类型与别名 → 底层类型 → 类型身份 → 类型转换 → 类型推断 → 泛型约束 → 反射查看类型 → 实现泛型集合”的顺序展开。本文代码在 go1.27.0 darwin/arm64 环境中实际编译运行。涉及 int 大小、类型名称和 map 遍历顺序的输出可能因环境不同而变化。Go 类型的基本分类Go 类型可以先分成下面几组分类例子主要作用预声明类型int、string、bool、any语言内置的基础能力定义类型type UserID int创建新的类型身份和业务语义类型别名type ID UserID在兼容旧名称时复用原类型复合类型数组、结构体、切片、map、指针、函数、channel用已有类型组合出新结构接口类型io.Reader、error描述方法或类型集合类型参数T、K、V泛型代码中的类型占位符Go 规范还会区分命名类型和未命名类型。int、UserID、结构体定义出来的名字都可以参与类型身份判断而 []int、struct{ Name string } 这类直接写出的类型字面量没有单独的类型名称。类型声明的两种形式Go 的类型声明主要有两种类型定义类型别名。它们都使用 type 关键字但含义完全不同。类型定义创建新类型type UserID int这句话创建了一个新的定义类型 UserID。它的底层类型是 int但 UserID 和 int 是两个不同的类型。完整示例package main import fmt type UserID int func main() { var id UserID 100 fmt.Printf(值: %d\n, id) fmt.Printf(类型: %T\n, id) var number int int(id) fmt.Println(转换后的整数:, number) }运行结果值: 100 类型: main.UserID 转换后的整数: 100UserID 的底层表示和 int 类似但它拥有独立的类型身份。这样做的价值是把业务含义写进类型里UserID 和普通数量都是整数但程序不应该随意把订单编号当用户编号使用。类型别名复用原类型类型别名使用等号type UserID int它不会创建新类型UserID 和 int 完全相同package main import fmt type UserID int func main() { var id UserID 100 var number int id fmt.Printf(UserID: %T\n, id) fmt.Printf(int: %T\n, number) fmt.Println(id number) }运行结果UserID: int int: int true类型别名常用于代码迁移和兼容旧 API。例如一个包原来公开了 OldUser 类型后来希望统一改名为 User可以暂时写成type OldUser User这样旧代码和新代码看到的是同一个类型调用者不需要立即修改所有文件。定义类型和别名的对比写法是否创建新类型是否需要转换是否可以拥有独立方法type UserID int是与 int 交互通常需要转换可以type UserID int否不需要不能脱离原类型单独增加方法最容易记忆的方式是没有等号是定义有等号是别名。定义类型可以绑定方法定义类型不仅能增加业务含义还可以绑定方法package main import fmt type UserID int func (id UserID) String() string { return fmt.Sprintf(user-%d, id) } func main() { id : UserID(7) fmt.Println(id) }运行结果user-7UserID 是当前包定义的本地类型所以可以声明 String 方法。别名不创建新的类型身份type UserID int // func (id UserID) String() string { ... } // 编译错误不能给非本地类型 int 添加方法如果需要一个拥有方法的整数类型应该使用类型定义而不是类型别名。底层类型每个定义类型都有一个底层类型。比如type UserID int type Score int type Name stringUserID 和 Score 的底层类型都是 intName 的底层类型是 string。底层类型决定了很多基础操作和转换规则但不会让定义类型失去独立身份。可以用下面的例子观察底层类型相同但类型不同的情况package main import fmt type UserID int type OrderID int func main() { userID : UserID(10) orderID : OrderID(10) fmt.Println(int(userID) int(orderID)) // fmt.Println(userID orderID) // 编译错误不同定义类型不能直接相加 fmt.Printf(UserID 类型: %T\n, userID) fmt.Printf(OrderID 类型: %T\n, orderID) }运行结果20 UserID 类型: main.UserID OrderID 类型: main.OrderID如果所有底层类型相同的定义类型都能直接混用类型系统就无法阻止大量业务错误。Go 保留了底层表示的相似性同时保留了类型身份的区别。底层类型和类型约束泛型约束中的 ~T 表示“底层类型是 T 的所有类型”。例如type Integer interface { ~int | ~int64 }Integer 不仅包含 int 和 int64也包含type UserID int type Count int64因为 UserID 的底层类型是 intCount 的底层类型是 int64。如果不写 ~只写 int那么通常只允许精确的 int 类型不包括底层类型相同的自定义类型type ExactInteger interface { int | int64 }自定义类型满足底层类型约束完整示例package main import fmt type Integer interface { ~int | ~int64 } type UserID int type Count int64 func Double[T Integer](value T) T { return value value } func main() { fmt.Println(Double(UserID(10))) fmt.Println(Double(Count(20))) }运行结果20 40Double 不需要为 UserID 和 Count 各写一份。只要类型的底层类型符合 Integer 约束就可以复用这段泛型代码。类型身份和可赋值性Go 编译器会判断两个值是否可以直接赋值。不同定义类型之间通常不能直接赋值即使它们底层类型相同type UserID int type OrderID int var userID UserID 10 // var orderID OrderID userID // 编译错误显式转换后才可以orderID : OrderID(userID)完整示例package main import fmt type UserID int type OrderID int func main() { var userID UserID 10 orderID : OrderID(userID) fmt.Println(用户编号:, userID) fmt.Println(订单编号:, orderID) }运行结果用户编号: 10 订单编号: 10显式转换是一种提醒这里的两个业务概念确实要互相转换调用者应该主动确认这种转换是合理的。别名可以直接赋值如果使用别名就不会产生新的类型身份type UserID int type LegacyUserID UserID func main() { var id UserID 10 var oldID LegacyUserID id fmt.Println(oldID) }LegacyUserID 只是 UserID 的另一个名字。对于类型检查、方法集和赋值规则来说它们完全相同。可赋值不等于可以做所有操作两个值可以赋值不代表它们可以参与所有运算。接口、无类型常量和组合类型都有自己的规则。遇到编译器报错时应该先问三个问题两边是不是同一个类型其中一边是不是无类型常量或接口是否需要显式转换。类型转换Go 不会自动把 int 转成 float64也不会自动把 UserID 转成 int。需要转换时使用目标类型包裹表达式package main import fmt func main() { number : 10 price : float64(number) id : UserID(number) fmt.Printf(number: %T %v\n, number, number) fmt.Printf(price: %T %.1f\n, price, price) fmt.Printf(id: %T %v\n, id, id) } type UserID int运行结果number: int 10 price: float64 10.0 id: main.UserID 10数值转换可能丢失信息整数转浮点数、浮点数转整数、较大整数转换到较小整数都可能改变值package main import fmt func main() { value : 3.9 integer : int(value) fmt.Println(原始值:, value) fmt.Println(转换后:, integer) var large int16 300 small : int8(large) fmt.Println(int16:, large) fmt.Println(int8:, small) }运行结果原始值: 3.9 转换后: 3 int16: 300 int8: 44浮点数转整数会截去小数部分。300 转成 int8 后溢出得到的结果取决于目标整数类型的表示规则。类型转换是明确的但不会自动帮你检查业务范围。字符串和整数转换把整数转换成 string不是把数字格式化成十进制文本而是把整数当作 Unicode 码点package main import ( fmt strconv ) func main() { codePoint : 65 fmt.Println(string(rune(codePoint))) fmt.Println(strconv.Itoa(codePoint)) }运行结果A 65string(65) 得到字符 A如果目标是得到文本 65应该使用 strconv.Itoa 或 fmt.Sprint。字符串与字节切片string 和 []byte 之间的转换会生成对应的值package main import fmt func main() { text : Go bytes : []byte(text) bytes[0] g fmt.Println(text) fmt.Println(string(bytes)) }运行结果Go go字符串本身不可变修改 bytes 不会改变原来的 text。类型转换的结果是新的字节切片。无类型常量和类型推断Go 的常量可以在确定上下文之前保持无类型状态const integer 10 const decimal 3.14 const letter A const text Go当常量赋值给变量时编译器会根据默认规则或目标类型确定它的类型package main import fmt const integer 10 const decimal 3.14 const letter A const text Go func main() { var a integer var b decimal var c letter var d text fmt.Printf(%T %v\n, a, a) fmt.Printf(%T %v\n, b, b) fmt.Printf(%T %v\n, c, c) fmt.Printf(%T %v\n, d, d) }运行结果int 10 float64 3.14 int32 65 string Go无类型 rune 常量的默认类型是 rune也就是 int32。无类型整数常量默认是 int浮点常量默认是 float64字符串常量默认是 string。同一个常量可以在不同上下文中表示不同的目标类型const value 10 var a int value var b int64 value var c float64 value因为 value 是无类型常量且 10 可以精确表示成 int、int64 和 float64所以这些赋值都合法。短变量声明的类型推断短变量声明会根据右侧表达式的静态类型推断变量类型package main import fmt func main() { number : 10 price : 19.9 name : rose passed : true fmt.Printf(number: %T\n, number) fmt.Printf(price: %T\n, price) fmt.Printf(name: %T\n, name) fmt.Printf(passed: %T\n, passed) }运行结果number: int price: float64 name: string passed: bool需要区分常量推断和变量推断。无类型常量可以根据目标类型转换变量一旦声明就有确定的静态类型。泛型函数的类型推断泛型函数调用时编译器通常可以根据参数推断类型参数package main import fmt func First[T any](values []T) T { return values[0] } func main() { numbers : []int{10, 20} names : []string{rose, tea} fmt.Println(First(numbers)) fmt.Println(First(names)) }运行结果10 roseFirst(numbers) 会推断 T 为 intFirst(names) 会推断 T 为 string。必要时也可以显式提供类型参数value : First[int]([]int{10, 20})复合类型和零值类型不仅包括整数和字符串也包括由其他类型组合出的类型类型例子零值数组[3]int所有元素为零值结构体struct{ Name string }每个字段为零值切片[]intnilmapmap[string]intnil指针*intnil函数func()nil接口io.Readernilchannelchan intnil零值是 Go 类型设计的重要部分。数组和结构体可以直接使用nil 切片可以读取长度并 appendnil map 可以读取但不能写入nil 函数不能调用nil 接口没有动态类型和动态值。package main import fmt type Profile struct { Name string Age int } func main() { var numbers []int var scores map[string]int var profile Profile var pointer *int var operation func() var value any fmt.Println(slice:, numbers nil, len(numbers)) fmt.Println(map:, scores nil) fmt.Println(struct:, profile) fmt.Println(pointer:, pointer nil) fmt.Println(function:, operation nil) fmt.Println(interface:, value nil) numbers append(numbers, 10) fmt.Println(append 后:, numbers) }运行结果slice: true 0 map: true struct: { 0} pointer: true function: true interface: true append 后: [10]设计自定义类型时应尽量让零值有合理含义。零值可用的类型更容易声明、更容易测试也更少依赖构造函数。类型别名的实际用途类型别名最重要的用途是 API 迁移和兼容。假设旧版本中公开了一个类型 OldConfig新版本希望使用 Config 这个名称type Config struct { Host string } type OldConfig Config这样两个名字指向完全相同的类型package main import fmt type Config struct { Host string } type OldConfig Config func printConfig(config Config) { fmt.Println(config.Host) } func main() { var oldConfig OldConfig Config{Host: localhost} printConfig(oldConfig) }运行结果localhost如果写成 type OldConfig Config那就会创建一个新的定义类型旧代码和新代码之间不再完全兼容。迁移公共 API 时等号是否存在会带来完全不同的结果。别名不是继承类型别名不会创建父子关系也不会复制字段和方法。它只是同一个类型的另一个名称type User struct { Name string } type Account UserUser 和 Account 在编译器看来是同一个类型。使用别名不能改变结构体字段、方法集或内存表示。类型和反射编译器在编译时知道类型运行时也可以通过 reflect 包读取部分类型信息package main import ( fmt reflect ) type UserID int type User struct { Name string } func main() { values : []any{ UserID(10), []int{1, 2}, User{Name: rose}, } for _, value : range values { typ : reflect.TypeOf(value) fmt.Printf(类型%T, Name%q, Kind%s\n, value, typ.Name(), typ.Kind()) } }运行结果类型main.UserID, NameUserID, Kindint 类型[]int, Name, Kindslice 类型main.User, NameUser, KindstructName 表示定义类型的名称。未命名的 []int 没有名称因此 Name 返回空字符串。Kind 表示更粗粒度的分类例如 int、slice、struct。反射适合做序列化、ORM、校验和通用工具但它绕过了部分编译期检查代码也更难理解。业务逻辑能使用静态类型完成时不应该为了追求“通用”而随意使用反射。泛型类型约束类型参数需要一个约束约束决定了哪些类型可以传入以及泛型函数体内允许使用哪些操作。type Number interface { ~int | ~int64 | ~float64 }这个约束允许底层类型是 int、int64 或 float64 的类型。因为这些类型都支持加法所以泛型函数中可以使用 func Add[T Number](a, b T) T { return a b }如果约束中包含的类型没有共同支持某个操作函数体就不能使用这个操作type Numeric interface { ~int | ~float64 } // func BitAnd[T Numeric](a, b T) T { // return a b // 编译错误float64 不支持按位与 // }类型约束不是运行时对象而是编译器判断类型参数是否合法的规则。comparable 约束如果泛型类型需要把 T 用作 map keyT 必须满足 comparabletype Set[T comparable] map[T]struct{}comparable 表示类型支持 和 !也就是可以作为 map 的 key。切片、map 和函数不满足 comparable因为它们不能直接比较。结构体是否满足 comparable取决于所有字段是否可比较。包含切片字段的结构体不能作为 Set 的类型参数。实现一个泛型集合接下来实现一个完整的数据结构基于 map 的泛型集合 Set。集合只保存“某个值是否存在”因此使用 map[T]struct{}Set[T] ├── key: T └── value: struct{} // 不需要额外数据struct{} 不占用有意义的数据空间适合表示集合中的占位值。由于 map 的 key 必须可比较Set 的类型参数约束为 comparable。定义 Set 类型type Set[T comparable] map[T]struct{}这不是给 map 起一个简单别名而是创建了一个新的泛型定义类型。Set 可以定义方法也可以在方法中使用类型参数 T。创建和添加元素func NewSet[T comparable](values ...T) Set[T] { set : make(Set[T]) for _, value : range values { set.Add(value) } return set } func (set Set[T]) Add(value T) { set[value] struct{}{} }NewSet 负责初始化底层 map。Set 的零值是 nil mapnil map 不能写入因此这里使用构造函数创建可写的 Set。读取、删除和 Len 可以安全地处理 nil Set但 Add 前必须保证 map 已初始化。查询、删除和去重func (set Set[T]) Has(value T) bool { _, ok : set[value] return ok } func (set Set[T]) Delete(value T) bool { if !set.Has(value) { return false } delete(set, value) return true } func (set Set[T]) Len() int { return len(set) } func Unique[T comparable](values []T) []T { seen : NewSet[T]() result : make([]T, 0, len(values)) for _, value : range values { if seen.Has(value) { continue } seen.Add(value) result append(result, value) } return result }Unique 使用 Set 记录已经出现过的值。因为 Set 的 key 是 T所以只有 comparable 类型可以使用这套去重算法。完整测试代码package main import fmt type UserID int func (id UserID) String() string { return fmt.Sprintf(user-%d, id) } type Set[T comparable] map[T]struct{} func NewSet[T comparable](values ...T) Set[T] { set : make(Set[T]) for _, value : range values { set.Add(value) } return set } func (set Set[T]) Add(value T) { set[value] struct{}{} } func (set Set[T]) Has(value T) bool { _, ok : set[value] return ok } func (set Set[T]) Delete(value T) bool { if !set.Has(value) { return false } delete(set, value) return true } func (set Set[T]) Len() int { return len(set) } func Unique[T comparable](values []T) []T { seen : NewSet[T]() result : make([]T, 0, len(values)) for _, value : range values { if seen.Has(value) { continue } seen.Add(value) result append(result, value) } return result } func main() { ids : NewSet(UserID(10), UserID(10), UserID(20)) fmt.Println(包含 user-10:, ids.Has(UserID(10))) fmt.Println(集合长度:, ids.Len()) fmt.Println(删除 user-10:, ids.Delete(UserID(10))) fmt.Println(删除后长度:, ids.Len()) fmt.Println(再次删除:, ids.Delete(UserID(10))) words : []string{go, rust, go, cpp, rust} fmt.Println(去重结果:, Unique(words)) // invalid : NewSet([]int{1, 2}) // 编译错误[]int 不满足 comparable }运行结果包含 user-10: true 集合长度: 2 删除 user-10: true 删除后长度: 1 再次删除: false 去重结果: [go rust cpp]这个实现体现了 Go 类型系统的几个关键点UserID 是 int 的定义类型但仍然满足 comparableSet[T] 的 T 被约束为可以作为 map key 的类型Set 是定义类型因此可以拥有 Add、Has、Delete 等方法Unique 可以同时处理 int、string、UserID 等可比较类型切片不能满足 comparable所以不能直接作为 Set 的元素类型map 的 key 规则在泛型代码中通过类型约束提前表达出来。Set 的时间复杂度操作平均时间复杂度说明AddO(1)map 写入HasO(1)map 查询DeleteO(1)map 删除LenO(1)读取 map 长度UniqueO(n)每个元素平均查询一次map 的平均复杂度是 O(1)但不代表任何情况下都严格是常数时间。哈希冲突、扩容和内存分配都会影响实际表现。这里的复杂度用于描述平均情况下的算法行为。常见错误和避坑提醒误区一把定义类型当成别名type UserID int 会创建新类型type UserID int 只是换名字。需要迁移 API 时用别名需要增加业务边界时用定义类型。误区二认为底层类型相同就可以直接赋值UserID 和 OrderID 都可能以 int 表示但它们是不同的定义类型。直接赋值会编译失败需要显式转换。误区三随意进行数值转换float64 转 int 会截断小数小整数类型可能溢出int 转 string 还可能被当成 Unicode 码点。转换前要确认目标语义和范围。误区四把 string(65) 当成十进制格式化string(65) 得到 A。想得到文本 65应使用 strconv.Itoa 或 fmt.Sprint。误区五把 nil map 当成空的可写 mapnil map 可以读取、删除和调用 len但写入会 panic。需要写入时使用 make 初始化。误区六用 any 代替所有具体类型any 会让编译器失去很多静态检查。能使用定义类型、接口或泛型约束时不要为了省事把所有参数都改成 any。误区七把类型约束接口当成普通接口变量包含 int、~int 或类型并集的接口主要服务于泛型约束不能直接声明运行时变量。误区八为了反射而反射反射可以读取运行时类型信息但会增加复杂度。优先用静态类型、接口和泛型表达规则只有在序列化、ORM、校验等动态场景使用反射。误区九泛型约束没有覆盖函数体里的操作如果约束允许的类型不都支持某个操作泛型函数体就不能使用该操作。约束不仅决定“谁能调用”也决定“函数里面能做什么”。一份可以直接复制的综合示例下面的例子把定义类型、类型转换、泛型约束和集合操作放到了一起package main import fmt type ProductID int func (id ProductID) String() string { return fmt.Sprintf(product-%d, id) } type Catalog[T comparable] struct { items map[T]string } func NewCatalog[T comparable]() *Catalog[T] { return Catalog[T]{ items: make(map[T]string), } } func (catalog *Catalog[T]) Put(id T, name string) { catalog.items[id] name } func (catalog Catalog[T]) Find(id T) (string, bool) { name, ok : catalog.items[id] return name, ok } func (catalog Catalog[T]) Len() int { return len(catalog.items) } func main() { catalog : NewCatalog[ProductID]() catalog.Put(ProductID(1), 键盘) catalog.Put(ProductID(2), 鼠标) name, ok : catalog.Find(ProductID(1)) fmt.Println(查询:, name, ok) fmt.Println(数量:, catalog.Len()) fmt.Println(编号:, ProductID(2)) }运行结果查询: 键盘 true 数量: 2 编号: product-2Catalog 使用 ProductID 作为 map key。ProductID 不只是一个 int它通过定义类型和 String 方法表达了“商品编号”的业务含义同时仍然可以满足 comparable 约束。总结本章我们学习了 Go 类型系统的完整基础类型决定一组值以及这些值可以执行的操作和方法type T U 会创建新的定义类型type T U 只会创建类型别名定义类型和底层类型相同的其他类型仍然拥有不同的类型身份底层类型影响操作和转换但不等于类型本身无类型常量会根据上下文确定目标类型Go 通常要求显式类型转换不会自动混合不同数值类型string 和整数转换、浮点数和整数转换都有容易忽略的语义零值是类型设计的一部分好的类型应该尽量让零值有合理含义反射可以查看运行时类型但应该在真正需要动态处理时使用泛型类型参数的约束决定允许哪些类型和可以执行哪些操作~T 表示底层类型为 T 的类型集合comparable 适合约束 map key 和需要 的泛型类型定义类型可以拥有方法也可以作为泛型集合的元素类型系统和泛型结合后可以实现既安全又可复用的数据结构。真正理解 Go 类型之后就不会把类型只看成变量后面的一个单词。更准确的理解是类型是编译器用来划定值、操作、方法和组合边界的规则定义类型保护业务含义别名帮助兼容迁移泛型约束把这些边界推广到可复用的数据结构中。官方资料Go 语言规范TypesGo 语言规范Type declarationsGo 语言规范Type identityGo 语言规范AssignabilityGo 语言规范ConversionsGo 语言规范Type inferenceGo 官方教程Getting started with genericsGo 官方博客An Introduction To GenericsGo 官方博客All your comparable typesGo 官方文档reflect 包Go 官方文档strconv 包
返回列表