
让编译器在编译期帮你拦住尽可能多的错误。
Go 没有泛型(1.18 之前),没有继承,没有重载——它靠一套显式、精确、零隐式转换的类型系统,把运行时才会暴露的问题,提前到编译期解决。
理解了这个前提,你才能理解 Go 每种类型"为什么这样设计"。
boolGo 的 bool 只有两个值:true 和 false。没有 0 和 1 的隐式转换。
go// ❌ 编译报错:不能用数字当 bool
if 1 { // error: non-bool 1 used as if condition
}
// ✅ 必须显式比较
if x != 0 {
}这个设计看似啰嗦,实则救了无数人——C/C++ 里
if (x = 1)和if (x == 1)的悲剧,在 Go 里不可能发生。
类型 | 长度 | 有符号 | 取值范围 |
|---|---|---|---|
int8 | 1 字节 | ✅ | -128 ~ 127 |
int16 | 2 字节 | ✅ | -32768 ~ 32767 |
int32 | 4 字节 | ✅ | 约 ±21 亿 |
int64 | 8 字节 | ✅ | 约 ±922 亿亿 |
uint8 ~ uint64 | 同上 | ❌ | 0 ~ 对应最大值 |
int | 平台相关 | ✅ | 32位系统=int32,64位=int64 |
uint | 平台相关 | ❌ | 同上 |
uintptr | 平台相关 | ❌ | 存指针的无符号整数 |
float32 | 4 字节 | ✅ | IEEE-754 单精度 |
float64 | 8 字节 | ✅ | IEEE-754 双精度 |
complex64 | 8 字节 | - | 实部+虚部各 float32 |
complex128 | 16 字节 | - | 实部+虚部各 float64 |
int 不等于 int32govar a int = 1
var b int32 = 1
// ❌ 编译报错:不能直接赋值
a = b // error: cannot use b (type int32) as type int
// ✅ 必须显式转换
a = int(b)选型建议:
场景 | 推荐类型 | 原因 |
|---|---|---|
通用计数、索引 | int | 跟着平台走,性能最优 |
与外部系统交互(JSON/DB) | int64 | JSON 数字默认解码为 float64,转 int64 最安全 |
明确范围的字段(年龄、状态码) | int8/uint8 | 省内存,自带范围约束 |
浮点计算 | float64 | float32 精度损失大,除非你在写 GPU 程序 |
金额 | 别用 float | 用 int64 存分,或 shopspring/decimal |
== 不能用于 slice/map/funcgoa := []int{1, 2, 3}
b := []int{1, 2, 3}
// ❌ 编译报错:slice 不能用 == 比较
if a == b { // error: invalid operation: a == b
}
// ✅ 用 reflect.DeepEqual 或自己循环比string:Go 里最特殊的类型Go 的 string 是只读的字节序列,底层是 struct { ptr *byte; len int }。
gos := "hello"
s[0] = 'H' // ❌ 编译报错:cannot assign to s[0]要改?转成 []byte 或 []rune:
gob := []byte(s)
b[0] = 'H'
s = string(b) // "Hello"range 遍历的是 rune,不是 bytegos := "你好"
for i, c := range s {
fmt.Printf("索引 %d,字符 %c,码点 %U\n", i, c, c)
}
// 索引 0,字符 你,码点 U+4F60
// 索引 3,字符 好,码点 U+597D ← 注意:不是 1!UTF-8 编码下,一个汉字占 3 个字节,所以索引跳了 3。这是 Go 字符串最经典的坑。
遍历字符串的正确姿势:
需求 | 用什么 |
|---|---|
按字节遍历 | for i := 0; i < len(s); i++ |
按字符(rune)遍历 | for _, r := range s |
按字符遍历且需索引 | for i, r := range s(但索引是字节偏移,不是字符序号) |
需要字符序号 | 用 utf8.RuneCountInString 辅助 |
byte vs rune:别名不是随便起的gotype byte = uint8
type rune = int32类型 | 本质 | 用途 |
|---|---|---|
byte | uint8 | 处理原始二进制数据、文件 IO |
rune | int32 | 处理 Unicode 字符 |
go// byte 切片 = 字符串的底层表示
s := "abc"
b := []byte(s) // [97 98 99]
// rune 切片 = 字符串的字符表示
r := []rune(s) // [97 98 99]
s2 := "你好"
b2 := []byte(s2) // [228 189 160 229 165 189] → 6个字节
r2 := []rune(s2) // [20320 22909] → 2个字符Array:几乎没人直接用govar a [3]int = [3]int{1, 2, 3}特点:长度是类型的一部分。
govar a [3]int
var b [4]int
a = b // ❌ 编译报错:长度不同,类型不同数组在 Go 里主要用于:固定大小的缓存、函数返回多值、
make底层实现。日常开发中,99% 的场景应该用切片。
Slice:Go 里最常用也最容易误解的类型gos := make([]int, 3, 5) // len=3, cap=5gooriginal := []int{1, 2, 3}
modified := original
modified[0] = 99
fmt.Println(original) // [99 2 3] ← 原数组也被改了!因为切片内部是 {ptr, len, cap},赋值只是拷贝了这个结构体,底层数组是共享的。
append 可能改变底层数组gos1 := make([]int, 3, 3) // len=3, cap=3
s2 := append(s1, 4) // cap 不够,分配新数组
fmt.Println(s1) // [0 0 0] ← s1 没变
fmt.Println(s2) // [0 0 0 4]但如果 cap 够:
gos1 := make([]int, 3, 5)
s2 := append(s1, 4)
fmt.Println(s1) // [0 0 0 4] ← s1 也变了!原则:不确定 cap 够不够时,别假设
append不会影响原切片。需要独立副本就copy。
方式 | 零值 | 可用? | 场景 |
|---|---|---|---|
var s []int | nil | ✅ 可以 append | 延迟初始化 |
s := []int{} | 空切片,len=0 | ✅ 可以 append | 明确空集合 |
s := make([]int, 0, 10) | 空切片,cap=10 | ✅ 预分配,高性能 | 已知大致容量 |
性能建议:已知容量时,用 make([]T, 0, n) 预分配,避免 append 时反复扩容。
gom := make(map[string]int)
m["key"] = 42nil,不是空 mapgovar m map[string]int
m["key"] = 1 // ❌ panic:assignment to entry in nil map
// ✅ 必须先 make
m := make(map[string]int)
m["key"] = 1 // 正常gom := map[string]int{"a": 1, "b": 2, "c": 3}
for k, v := range m {
fmt.Println(k, v) // 每次运行顺序可能不同
}需要有序遍历?用
map存数据,遍历时用sort.Strings(keys)排序。
go// ❌ 多 goroutine 同时读写 → panic
go func() { m["key"] = 1 }()
go func() { v := m["key"] }()
// ✅ 方案1:加锁
var mu sync.Mutex
mu.Lock()
m["key"] = 1
mu.Unlock()
// ✅ 方案2:用 sync.Map(读多写少场景)
var sm sync.Map
sm.Store("key", 1)
v, _ := sm.Load("key")
// ✅ 方案3:每个 goroutine 用自己的 map,最后 mergeStruct:Go 的"类"gotype User struct {
ID int64 `json:"id"`
Name string `json:"name"`
Age int `json:"age,omitempty"` // omitempty: 为零值时不输出
}govar u User
fmt.Println(u) // {0 0}
// ID=0, Name="", Age=0 —— 不是 nil,是"全零"gotype Base struct {
ID int64
Name string
}
type Admin struct {
Base // 嵌入,相当于 Admin 有了 ID 和 Name 字段
Permission string
}
a := Admin{
Base: Base{ID: 1, Name: "admin"},
Permission: "full",
}
// 可以直接访问嵌入字段
fmt.Println(a.ID) // 1,不用 a.Base.IDgotype Logger struct{}
func (l Logger) Log(msg string) { fmt.Println(msg) }
type Service struct {
Logger // 匿名嵌入
}
s := Service{}
s.Log("hello") // ✅ 直接调用,像自己的方法一样这就是 Go "组合优于继承"的语言级实现。
Pointer:不是为了性能,是为了可变性gofunc update(u *User) {
u.Name = "new name" // 修改原对象
}
u := &User{Name: "old"}
update(u)
fmt.Println(u.Name) // "new name"nilgovar p *int
fmt.Println(p) // <nil>
fmt.Println(*p) // ❌ panic:nil pointer dereference
// ✅ 使用前判空
if p != nil {
fmt.Println(*p)
}场景 | 用值 | 用指针 |
|---|---|---|
小结构体(≤几个字段) | ✅ 拷贝成本低 | ❌ 没必要 |
大结构体(很多字段) | ❌ 拷贝贵 | ✅ 传引用 |
需要修改原对象 | ❌ 值拷贝改不到 | ✅ |
可能为 nil(可选字段) | ❌ 零值无法区分"未设置" | ✅ nil 表示未设置 |
并发共享 | ❌ 竞态 | ✅ 配合锁/atomic |
经验法则:如果不确定,优先用值传递。Go 的编译器和逃逸分析会帮你优化。
Function:Go 里的一等公民go// 函数可以作为变量
fn := func(x int) int { return x * 2 }
// 函数可以作为参数
func Apply(f func(int) int, x int) int {
return f(x)
}
// 函数可以作为返回值
func MakeAdder(n int) func(int) int {
return func(x int) int { return x + n }
}
add5 := MakeAdder(5)
fmt.Println(add5(3)) // 8defer 的执行顺序:LIFOgofunc demo() {
defer fmt.Println("3")
defer fmt.Println("2")
defer fmt.Println("1")
fmt.Println("0")
}
// 输出:0 1 2 3Interface:Go 类型系统的灵魂gotype Reader interface {
Read(p []byte) (n int, err error)
}接口的本质:一组方法签名的集合。任何类型,只要实现了这些方法,就自动实现了该接口——不需要声明。
gotype MyFile struct{}
func (f MyFile) Read(p []byte) (n int, err error) {
// 实现了 Read,就自动实现了 Reader 接口
return 0, nil
}
var r Reader = MyFile{} // ✅ 隐式实现,不需要 implementsinterface{}(any)不是万能的go// ✅ 边界处使用:JSON 反序列化、通用容器
var v any = map[string]int{"a": 1}
// ❌ 滥用:业务逻辑里到处用 any,等于放弃类型安全
func process(v any) {
// 你根本不知道 v 是什么,运行时才知道
}interface{} vs nil 接口govar i interface{} = nil
fmt.Println(i == nil) // true
fmt.Println(i.(int) == nil) // false!类型是 int,值是 nil
// ✅ 判断接口是否为 nil,要同时判断类型和值
func isNil(v interface{}) bool {
return v == nil || reflect.ValueOf(v).IsNil()
}goch := make(chan int, 2) // 带缓冲,容量为 2
ch <- 1 // 发送
v := <-ch // 接收
close(ch) // 关闭类型 | 行为 |
|---|---|
make(chan T) | 无缓冲,发送阻塞直到有接收者 |
make(chan T, n) | 有缓冲,满了才阻塞发送 |
nil channel | 永远阻塞,收发都会 panic |
for range 遍历 channelgoch := make(chan int, 3)
go func() {
ch <- 1
ch <- 2
close(ch)
}()
for v := range ch { // 自动检测 close,退出循环
fmt.Println(v)
}
// 输出:1 2select:多 channel 调度器goselect {
case v := <-ch1:
fmt.Println("from ch1:", v)
case ch2 <- 42:
fmt.Println("sent to ch2")
case <-time.After(5 * time.Second):
fmt.Println("timeout")
default:
fmt.Println("no channel ready")
}Go 不允许隐式类型转换,这是它和 C/Java 最大的区别之一。
转换 | 语法 | 示例 |
|---|---|---|
基础类型转换 | T(v) | int32(x) |
字符串 ↔ 字节切片 | []byte(s) / string(b) | []byte("hello") |
字符串 ↔ rune 切片 | []rune(s) / string(r) | []rune("你好") |
接口 → 具体类型 | v.(T) | x.(int) |
接口 → 具体类型(安全) | v.(T) + ok | x, ok := v.(int) |
go// ❌ 隐式转换不存在
var i int = 1
var f float64 = i // error: cannot use i (type int) as type float64
// ✅ 必须显式
var f float64 = float64(i)类型 | 零值 |
|---|---|
bool | false |
所有数字类型 | 0 |
string | ""(空字符串) |
pointer / func / interface / slice / map / chan | nil |
array / struct | 每个字段的零值 |
nil和零值不是一回事。var s []int是nil,s := []int{}是空切片(非 nil)。nilslice 可以 append,但 len=0;空 slice len=0,也可以 append。区别在于 JSON 序列化:nil→null,空切片 →[]。
需要存储数据?
├── 有序 + 随机访问 → []T(切片)
├── 有序 + 频繁中间插入 →链表(很少用)
├── Key-Value 查找 → map[K]V
├── 固定大小 → [N]T(数组)
└── 去重 + 有序 → 用 map 模拟 set,或第三方库
需要传递数据?
├── 小数据(≤3个字段)→ 值传递
├── 大数据 → 指针传递
├── 需要修改 → 指针传递
└── 可选字段 → 指针(nil=未设置)
需要抽象行为?
├── 一种行为 → interface(一个方法)
├── 多种行为组合 → 嵌入多个 interface
└── 不确定类型 → interface{}(仅限边界)Go 的类型系统不追求表达力,追求精确性。每一种类型都在告诉编译器和阅读代码的人:"我是什么,我能做什么,我不能做什么。"
夯实类型根基,不是背文档,而是养成三个习惯:
interface{},类型安全是 Go 给你最大的礼物。nil 就多想一秒——它是 nil 接口?nil 切片?还是 nil map?含义完全不同。类型不是束缚,是保障。理解了 Go 的类型,你才算真正入门了 Go。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。