前置知识
- Go 基础语法(package、func、struct、if/for)
- 了解 C/C++ 的指针(Go 指针是无 C 那么灵活)
- 至少用一种 OOP 语言写过代码
为什么这篇文章很重要
Go 的语法设计经常让 Java/Python 转 Go 的人踩坑连连:
make vs new 到底用哪个- 指针不能算术运算,但又不像 Java 那样完全无指针
- map 是引用类型,传参居然"几乎"按引用传
- 闭包在 for 循环里捕获变量——经典坑
- “接口” 不是抽象类,不能有成员变量
这一篇把 Go 最容易"看文档一眼就懂、写代码就懵"的核心概念一次性厘清。
一、make 与 new 的区别
1.1 一句话总结
make:初始化 内置数据结构(slice、map、channel),返回引用类型本身new:分配内存 给任意类型,返回指向零值的指针
1.2 make 用法
1
2
3
4
5
6
7
8
| // 切片:长度 0,容量 100
slice := make([]int, 0, 100)
// map:初始容量 10
hash := make(map[int]bool, 10)
// channel:缓冲区 5
ch := make(chan int, 5)
|
底层真相:
slice 是 reflect.SliceHeader 结构体(含 data 指针、len、cap)map 实际是 *runtime.hmap 指针chan 实际是 *runtime.hchan 指针
1.3 new 用法
1
2
3
4
5
| i := new(int) // *int,指向 0
*p := 42 // 解引用赋值
v := new(int)
fmt.Println(*v) // 0
|
等价写法:
1
2
3
| i := new(int) // *int → 0
var v int // int → 0
i = &v // i 指向 v
|
1.4 对比表
| 维度 | make | new |
|---|
| 用途 | 初始化内置数据结构 | 分配内存给任意类型 |
| 适用类型 | slice、map、channel | 任意类型 |
| 返回 | 引用类型本身 | 指向类型零值的指针 |
| 是否清零 | 是(并初始化内部结构) | 是(清零) |
二、指针:Go 的"受限"指针
2.1 指针基础
1
2
3
4
5
6
| // 声明
var p *int // nil 指针
var m int = 100
p = &m // p 指向 m
*p = 1 // 通过指针改 m
fmt.Println(*p) // 1
|
2.2 用 new 初始化
1
2
3
| var p *int
p = new(int)
*p = 1
|
2.3 取消引用
1
2
3
4
5
6
7
8
9
| func demo3() {
a := 100
b := &a
fmt.Printf("a:%d\n", a) // a:100
fmt.Printf("b:%v\n", b) // b:0xc00000a098
fmt.Printf("*b:%v\n", *b) // *b:100
*b = 200
fmt.Printf("a:%d\n", a) // a:200
}
|
2.4 指向指针的指针
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| func demo4() {
a := 7.98
p := &a
pp := &p
fmt.Println("a =", a) // 7.98
fmt.Println("address of a =", &a) // 0xc00000a098
fmt.Println("p =", p) // 0xc00000a098
fmt.Println("address of p =", &p) // 0xc000006028
fmt.Println("pp =", pp) // 0xc000006028
// 双重解引用
fmt.Println("*pp =", *pp) // 0xc00000a098
fmt.Println("**pp =", **pp) // 7.98
}
|
2.5 Go 没有指针算术
1
2
3
| var x = 67
var p = &x
var p1 = p + 1 // 编译错误:invalid operation
|
好处:杜绝 C 那种野指针越界访问——安全!
代价:底层数据操作不够灵活,得用 unsafe.Pointer 绕。
2.6 指针比较
1
2
3
| p1 := &a
p2 := &a
fmt.Println(p1 == p2) // true:同一地址
|
2.7 简单类型的指针
1
2
3
4
5
6
7
8
9
| func main() {
p := 5
change(&p)
fmt.Println("p =", p) // p = 0
}
func change(p *int) {
*p = 0
}
|
2.8 值类型 vs 引用类型的"传参魔法"
Go 所有参数都是值传递——但 map、slice、channel 是引用类型的零值结构,传参相当于传了"指针副本"。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| func main() {
m := map[string]int{"one": 1, "two": 2}
n := m
fmt.Printf("%p\n", &m) // 0xc000074018
fmt.Printf("%p\n", &n) // 0xc000074020
fmt.Println(m) // map[one:1 two:2]
fmt.Println(n) // map[one:1 two:2]
changeMap(m)
fmt.Println(m) // map[one:1 two:2 three:3]
fmt.Println(n) // map[one:1 two:2 three:3] ← 跟着变
}
func changeMap(m map[string]int) {
m["three"] = 3
fmt.Printf("changeMap func %p\n", m)
}
|
m 和 n 是不同 map header(地址不同),但都指向同一份底层 hash 表——改一个,两个都变。
2.9 显式传指针
1
2
3
| func changeMap(m *map[string]int) {
(*m)["three"] = 3 // 必须先解引用
}
|
不推荐——多此一举,map 本身已经"几乎按引用传"了。
2.10 总结
- Go 不支持指针算术(安全)
- 指针传递只占 4 / 8 字节(高效)
- 函数方法的接受者可以是值或指针
- 复杂类型(map/slice/chan)传递的是"指针副本"
三、接口:Go 的"鸭子类型"
3.1 定义
1
2
3
| type MyInterface interface {
Error() string
}
|
特性:
- 只能有方法签名,不能有成员变量
- 隐式实现——只要类型实现了接口的所有方法,就自动满足接口
- 空接口
interface{} 等价于 Java 的 Object、C++ 的 void*
3.2 经典示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| type Speaker interface {
Speak() string
}
type Dog struct{}
func (d Dog) Speak() string { return "汪汪汪" }
type Cat struct{}
func (c Cat) Speak() string { return "喵喵喵" }
func main() {
var s Speaker
s = Dog{}
fmt.Println(s.Speak()) // 汪汪汪
s = Cat{}
fmt.Println(s.Speak()) // 喵喵喵
}
|
Dog 和 Cat 都没显式说"我实现了 Speaker"——只要方法对得上,编译就过。
3.3 常见接口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| // fmt.Stringer
type Stringer interface {
String() string
}
// error
type error interface {
Error() string
}
// io.Reader
type Reader interface {
Read(p []byte) (n int, err error)
}
// io.Writer
type Writer interface {
Write(p []byte) (n int, err error)
}
|
3.4 类型断言
1
2
3
4
| var s Speaker = Dog{}
if d, ok := s.(Dog); ok {
fmt.Println("这是 Dog:", d.Speak())
}
|
3.5 接口 vs 抽象类(Java/C++)
| 维度 | Go 接口 | Java interface / C++ 抽象类 |
|---|
| 成员变量 | 无 | 常量 / 静态变量(Java 8+) |
| 默认方法 | 无 | Java 8+ default method |
| 实现方式 | 隐式 | 显式 implements |
| 多继承 | 任意多 | 受限 |
| 嵌入接口 | 支持 | 不直接支持 |
四、函数式编程:命令式 vs 函数式
4.1 编程范式
1979 年图灵奖得主 Robert Floyd 在颁奖演说中使用"编程范式"一词。常见范式:
- 命令式 / 过程式(C、Pascal)
- 面向对象(Java、C++、Python)
- 声明式(SQL、HTML、CSS)
- 函数式(Haskell、Lisp、ML、Go 部分支持)
- 泛型编程(C++ STL、Go 1.18+)
4.2 命令式编程
1
2
3
4
5
| // 一行一行告诉计算机先做什么再做什么
sum := 0
for i := 1; i <= 100; i++ {
sum += i
}
|
思想:模拟冯·诺依曼机运行机制,按指令顺序排列。
适用:线性、按部就班的算法问题。
不足:非结构化、复杂算法时极其困难。
4.3 面向对象
1
2
3
4
5
6
7
| type Animal struct {
Name string
}
func (a *Animal) Speak() string {
return a.Name + " makes a sound"
}
|
三大特性:
- 封装:隐藏实现细节
- 继承:子类复用父类
- 多态:同一消息不同响应
4.4 声明式编程
1
| SELECT * FROM users WHERE age > 18
|
只描述"做什么"不描述"怎么做"。
4.5 函数式编程
1
2
3
4
5
6
7
8
| // 一等公民:函数可以当参数、返回值、变量
func Map[T, U any](ts []T, f func(T) U) []U {
us := make([]U, len(ts))
for i, t := range ts {
us[i] = f(t)
}
return us
}
|
核心思想:
- 函数是"一等公民"
- 没有"语句"只有"表达式"
- 无副作用——同一输入永远同一输出
- 不可变——不修改变量,返回新值
4.6 函数式 vs 命令式
| 维度 | 命令式 | 函数式 |
|---|
| 关注点 | 步骤(先做什么) | 关系(输入 → 输出) |
| 变量 | 可变(存储状态) | 不可变(代数变量) |
| 循环 | for/while | 递归 |
| 状态 | 依赖外部 | 不依赖 |
| 并发 | 需锁 | 无共享,无需锁 |
函数式优势:
- 单元测试容易——纯函数 = 输入输出对
- 并发安全——不共享状态,无 race condition
- 惰性求值——表达式不计算直到使用
- 模式匹配——代数数据类型 + case 表达
- 错误容易复现——不依赖运行历史
Go 的函数式特性:
- 函数是一等公民
- 高阶函数(函数当参数/返回值)
- 闭包
- 偏应用函数
- 柯里化(手动)
- ❌ 没有惰性求值、模式匹配、不可变集合
五、闭包:Go 里的"陷阱"与"利器"
5.1 闭包定义
闭包 = 函数 + 引用环境
1
2
3
4
5
6
7
| func closure() func(int) int {
var x int
return func(b int) int {
x++
return b + x
}
}
|
返回的匿名函数捕获了 x——x 逃逸到堆,闭包整个生命周期内都有效。
5.2 经典案例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
| func AnotherExFunc(n int) func() {
n++
return func() {
fmt.Println(n)
}
}
func ExFunc(n int) func() {
return func() {
n++
fmt.Println(n)
}
}
func main() {
myAnotherFunc := AnotherExFunc(20)
fmt.Println(myAnotherFunc) // 0x48e3d0
myAnotherFunc() // 21
myAnotherFunc() // 21
myFunc := ExFunc(10)
fmt.Println(myFunc) // 0x48e340
myFunc() // 11
myFunc() // 12
}
|
分析:
AnotherExFunc 在 n++ 后定义闭包,所以闭包捕获的是 21——后续调用不修改 nExFunc 把 n++ 放在闭包里——每次调用都 n++
5.3 for 循环闭包陷阱
1
2
3
4
5
6
7
8
9
| func main() {
s := []string{"a", "b", "c"}
for _, v := range s {
go func() {
fmt.Println(v)
}()
}
time.Sleep(time.Second)
}
|
输出:c c c(不是 a b c)——闭包捕获的是循环变量 v 本身(同一个地址),不是每次迭代的值。
解法 1:传参
1
2
3
4
5
| for _, v := range s {
go func(v string) {
fmt.Println(v)
}(v)
}
|
解法 2:Go 1.22+
Go 1.22 改进了 for 循环变量——每次迭代是独立变量:
1
2
3
4
5
6
| // Go 1.22+
for _, v := range s {
go func() {
fmt.Println(v) // a, b, c
}()
}
|
5.4 多次调用独立性
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| func intSeq() func() int {
i := 0
return func() int {
i++
return i
}
}
func main() {
nextInt := intSeq()
println(nextInt()) // 1
println(nextInt()) // 2
println(nextInt()) // 3
newInts := intSeq()
println(newInts()) // 1(独立闭包)
}
|
5.5 defer 与闭包
1
2
3
4
5
6
7
8
9
10
11
12
13
| func main() {
x, y := 1, 2
defer func(a int) {
fmt.Printf("x:%d,y:%d\n", a, y)
}(x) // 复制 x
x += 100
y += 100
fmt.Println(x, y) // 101 102
}
// 输出:
// 101 102
// x:1,y:102
|
关键:defer 注册时复制了 x 的值,但 y 是闭包引用——闭包持有 y 的地址,y 改了就跟着改。
5.6 闭包对 GC 的影响
闭包引用的变量逃逸到堆——比局部变量多一次 GC 开销。
1
2
| go build -gcflags "-N -l -m" closure.go
# moved to heap: x
|
六、判断结构体是否为空
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| // 方法 1:直接比较(不适用于含 slice/map/func 字段的 struct)
var st Person
if (Person{} == st) {
fmt.Println("空")
}
// 方法 2:reflect.DeepEqual
import "reflect"
reflect.DeepEqual(st, Person{})
// 方法 3:自定义 IsEmpty 方法
func (p Person) IsEmpty() bool {
return p.Name == "" && p.Age == 0
}
|
七、string 与 int 互转
1
2
3
4
5
6
7
8
9
10
11
12
13
| import "strconv"
// string → int
i, err := strconv.Atoi("123")
// string → int64
i64, err := strconv.ParseInt("123", 10, 64)
// int → string
s := strconv.Itoa(123)
// int64 → string
s := strconv.FormatInt(123, 10)
|
八、下一步
- 泛型(Go 1.18+)——
func Map[T, U any](ts []T, f func(T) U) []U - 错误处理:
errors.Is / errors.As / fmt.Errorf("%w", err) - Context:
context.Context 控制 goroutine 生命周期 - 反射:
reflect 包做通用编程 - unsafe:
unsafe.Pointer 底层操作
参考资料