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 的类型声明主要有两种:

  1. 类型定义;
  2. 类型别名。

它们都使用 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
转换后的整数: 100

UserID 的底层表示和 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-7

UserID 是当前包定义的本地类型,所以可以声明 String 方法。别名不创建新的类型身份:

复制代码
type UserID = int

// func (id UserID) String() string { ... }
// 编译错误:不能给非本地类型 int 添加方法

如果需要一个拥有方法的整数类型,应该使用类型定义,而不是类型别名。

底层类型

每个定义类型都有一个底层类型。比如:

复制代码
type UserID int
type Score int
type Name string

UserID 和 Score 的底层类型都是 int,Name 的底层类型是 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 的底层类型是 int,Count 的底层类型是 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
40

Double 不需要为 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 的另一个名字。对于类型检查、方法集和赋值规则来说,它们完全相同。

可赋值不等于可以做所有操作

两个值可以赋值,不代表它们可以参与所有运算。接口、无类型常量和组合类型都有自己的规则。遇到编译器报错时,应该先问三个问题:

  1. 两边是不是同一个类型;
  2. 其中一边是不是无类型常量或接口;
  3. 是否需要显式转换。

类型转换

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
65

string(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
rose

First(numbers) 会推断 T 为 int,First(names) 会推断 T 为 string。必要时也可以显式提供类型参数:

复制代码
value := First[int]([]int{10, 20})

复合类型和零值

类型不仅包括整数和字符串,也包括由其他类型组合出的类型:

类型 例子 零值
数组 3int 所有元素为零值
结构体 struct{ Name string } 每个字段为零值
切片 \[\]int nil
map mapstringint nil
指针 *int nil
函数 func() nil
接口 io.Reader nil
channel chan int nil

零值是 Go 类型设计的重要部分。数组和结构体可以直接使用;nil 切片可以读取长度并 append;nil 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 = User

User 和 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, Name="UserID", Kind=int
类型=[]int, Name="", Kind=slice
类型=main.User, Name="User", Kind=struct

Name 表示定义类型的名称。未命名的 \[\]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 key,T 必须满足 comparable:

复制代码
type Set[T comparable] map[T]struct{}

comparable 表示类型支持 == 和 !=,也就是可以作为 map 的 key。切片、map 和函数不满足 comparable,因为它们不能直接比较。

结构体是否满足 comparable,取决于所有字段是否可比较。包含切片字段的结构体不能作为 Set 的类型参数。

实现一个泛型集合

接下来实现一个完整的数据结构:基于 map 的泛型集合 Set。

集合只保存"某个值是否存在",因此使用 mapTstruct{}:

复制代码
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 map,nil 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 类型系统的几个关键点:

  1. UserID 是 int 的定义类型,但仍然满足 comparable;
  2. SetT 的 T 被约束为可以作为 map key 的类型;
  3. Set 是定义类型,因此可以拥有 Add、Has、Delete 等方法;
  4. Unique 可以同时处理 int、string、UserID 等可比较类型;
  5. 切片不能满足 comparable,所以不能直接作为 Set 的元素类型;
  6. map 的 key 规则在泛型代码中通过类型约束提前表达出来。

Set 的时间复杂度

操作 平均时间复杂度 说明
Add O(1) map 写入
Has O(1) map 查询
Delete O(1) map 删除
Len O(1) 读取 map 长度
Unique O(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 当成空的可写 map

nil 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-2

Catalog 使用 ProductID 作为 map key。ProductID 不只是一个 int,它通过定义类型和 String 方法表达了"商品编号"的业务含义,同时仍然可以满足 comparable 约束。

总结

本章我们学习了 Go 类型系统的完整基础:

  1. 类型决定一组值以及这些值可以执行的操作和方法;
  2. type T U 会创建新的定义类型;
  3. type T = U 只会创建类型别名;
  4. 定义类型和底层类型相同的其他类型仍然拥有不同的类型身份;
  5. 底层类型影响操作和转换,但不等于类型本身;
  6. 无类型常量会根据上下文确定目标类型;
  7. Go 通常要求显式类型转换,不会自动混合不同数值类型;
  8. string 和整数转换、浮点数和整数转换都有容易忽略的语义;
  9. 零值是类型设计的一部分,好的类型应该尽量让零值有合理含义;
  10. 反射可以查看运行时类型,但应该在真正需要动态处理时使用;
  11. 泛型类型参数的约束决定允许哪些类型和可以执行哪些操作;
  12. ~T 表示底层类型为 T 的类型集合;
  13. comparable 适合约束 map key 和需要 == 的泛型类型;
  14. 定义类型可以拥有方法,也可以作为泛型集合的元素;
  15. 类型系统和泛型结合后,可以实现既安全又可复用的数据结构。

真正理解 Go 类型之后,就不会把类型只看成变量后面的一个单词。更准确的理解是:类型是编译器用来划定值、操作、方法和组合边界的规则;定义类型保护业务含义,别名帮助兼容迁移,泛型约束把这些边界推广到可复用的数据结构中。

官方资料:

相关推荐
梅雅达编程笔记1 小时前
02_BeautifulSoup网页数据提取
开发语言·爬虫·python·beautifulsoup·数据采集
ClouGence1 小时前
开源数据库管理工具 CloudDM 4.3.0 发布,支持 SQL 收藏,多数据源语法支持大幅增强
数据库·oracle·sql server
Escalating_xu1 小时前
【C 语言】深入理解指针(3):字符指针、数组指针、二维数组传参、函数指针与转移表
java·c语言·开发语言
可涵不会debug1 小时前
C++ std::bind 实战教程:线程池、回调场景全覆盖
linux·开发语言·c++
不正经的码狗1 小时前
Java 开发环境搭建与 Eclipse 使用速通教程
java·开发语言·eclipse
阿钱真强道2 小时前
24 嵌入式操作系统 | 项目启动与环境准备(PyQt5 / Flask 两个“最小可跑“)
开发语言·qt·flask·架构设计·协议契约
霸道流氓气质2 小时前
AI模型幻觉检测与抑制完全指南:从规则引擎到RAG对比的Java生产级实战
java·开发语言·人工智能
前端 贾公子2 小时前
LangChain核心组件 == 提示词(Prompts)
开发语言·c#
Summer-Bright2 小时前
深度 | 壁仞1.74亿中标智算中心:国产GPU从能用跨进商用,但4.5亿的大单却因凑不齐三家废标了
数据库·ai·国产gpu