Featured image of post Go 核心语法精要:指针、make/new、接口、闭包与函数式编程

Go 核心语法精要:指针、make/new、接口、闭包与函数式编程

Go 必学核心语法:指针与值传递、make vs new、接口多态、闭包陷阱、函数式编程范式

前置知识

  • 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)

底层真相

  • slicereflect.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 对比表

维度makenew
用途初始化内置数据结构分配内存给任意类型
适用类型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)
}

mn 是不同 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())  // 喵喵喵
}

DogCat 都没显式说"我实现了 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递归
状态依赖外部不依赖
并发需锁无共享,无需锁

函数式优势

  1. 单元测试容易——纯函数 = 输入输出对
  2. 并发安全——不共享状态,无 race condition
  3. 惰性求值——表达式不计算直到使用
  4. 模式匹配——代数数据类型 + case 表达
  5. 错误容易复现——不依赖运行历史

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
}

分析

  • AnotherExFuncn++ 后定义闭包,所以闭包捕获的是 21——后续调用不修改 n
  • ExFuncn++ 放在闭包里——每次调用都 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)
  • Contextcontext.Context 控制 goroutine 生命周期
  • 反射reflect 包做通用编程
  • unsafeunsafe.Pointer 底层操作

参考资料

使用 Hugo 构建
主题 StackJimmy 设计