Skip to content

Go 进阶特性 ​

指针与值语义、接口、错误处理、反射、泛型与测试,构建 Java / Python 工程师的 Go 中阶能力。


一、指针与值语义 ​

1.1 指针是什么:& 与 * ​

Go 有指针,但没有 C 的指针算术(不能 p++ 移动指针)。指针保存变量内存地址,& 取地址,* 解引用:

go
func main() {
    n := 42
    p := &n        // p 是 *int,指向 n
    fmt.Println(*p) // 42,解引用取值
    *p = 100        // 通过指针修改原变量
    fmt.Println(n)  // 100
}
func main() {
    n := 42
    p := &n        // p 是 *int,指向 n
    fmt.Println(*p) // 42,解引用取值
    *p = 100        // 通过指针修改原变量
    fmt.Println(n)  // 100
}

零值指针是 nil,解引用 nil 指针会 panic。

1.2 值传递与"引用类型"辨析 ​

Go 的传参一律是值拷贝(Java 同规则)。但 slice / map / channel / 函数 / 接口内部本就持有指向底层数据的引用,因此函数内修改"看起来像引用传递":

go
func addOne(m map[string]int) { m["a"]++ } // 生效:map 头是引用
func addOneS(s []int) { s[0]++ }           // 生效:修改的是共享底层数组
func appendS(s []int) { s = append(s, 9) } // 不生效:len/cap 是拷贝,扩容后更是新数组
func addOne(m map[string]int) { m["a"]++ } // 生效:map 头是引用
func addOneS(s []int) { s[0]++ }           // 生效:修改的是共享底层数组
func appendS(s []int) { s = append(s, 9) } // 不生效:len/cap 是拷贝,扩容后更是新数组

结论:map / slice 传入函数可改元素,但改变 slice 长度必须返回新切片(s = appendX(s))。

1.3 何时该用指针 ​

场景用指针原因
修改调用方变量必须否则拷贝后改了白改
大结构体(> 几十字节)推荐避免每次拷贝
标识"可选 / 无值"可用指针可为 nil
小值类型 / 不可变数据不用int、bool、小 struct 值拷贝更安全
并发共享同一对象慎用指针 + 并发 = 数据竞争,需要同步

需要澄清一个流传甚广的误解:Java 面试里的"值传递",在 Go 里体现为 slice/map 传引用语义但 header 按值拷贝,理解底层结构才能写对。

1.4 与 Java / Python 对照 ​

GoJavaPython说明
*T / &x引用引用Go 指针可显式传 nil
值类型(int/struct/array)基本类型 / 不可变对象不可变对象Go 结构体可整体复制
slice / mapArrayList / HashMaplist / dict均引用底层数据
无指针算术无无安全性保证

二、struct 方法与 receiver ​

2.1 方法与 receiver ​

方法 = 函数 + 接收者(receiver)。接收者写在 func 与函数名之间:

go
type Rectangle struct{ W, H float64 }

// 值 receiver:操作的是副本
func (r Rectangle) Area() float64 { return r.W * r.H }

// 指针 receiver:可修改原对象
func (r *Rectangle) Scale(f float64) { r.W *= f; r.H *= f }

func main() {
    rc := Rectangle{W: 3, H: 4}
    fmt.Println(rc.Area()) // 12
    rc.Scale(2)
    fmt.Println(rc.W)      // 6
}
type Rectangle struct{ W, H float64 }

// 值 receiver:操作的是副本
func (r Rectangle) Area() float64 { return r.W * r.H }

// 指针 receiver:可修改原对象
func (r *Rectangle) Scale(f float64) { r.W *= f; r.H *= f }

func main() {
    rc := Rectangle{W: 3, H: 4}
    fmt.Println(rc.Area()) // 12
    rc.Scale(2)
    fmt.Println(rc.W)      // 6
}

Go 会自动取址/取值:值变量可调指针方法(自动 &rc),指针变量也可调值方法(自动 *p),但前提是变量可寻址。

2.2 值 receiver 与指针 receiver 的取舍 ​

  • 需要修改对象、或对象含需要共享的状态(如互斥锁、大切片)时,统一用指针 receiver。
  • 类型是 map / slice / 小型值类型,或期望不可变语义时用值 receiver。
  • 关键约束:同一类型的所有方法,receiver 类型应保持一致。若既有值又有指针方法,其方法集割裂会引发"对象是否实现接口"的困惑(见 2.4)。
  • 变更成本:receiver 从值改指针会破坏所有调用的接口满足性,接口设计期就应定好。

2.3 nil receiver 处理 ​

指针方法被 nil 接收者调用是合法的(只要方法内部不访问其字段):

go
type Config struct{ Timeout int }

func (c *Config) GetTimeout() int {
    if c == nil { // nil 保护,让零值也可用
        return 30
    }
    return c.Timeout
}

var c *Config
fmt.Println(c.GetTimeout()) // 30
type Config struct{ Timeout int }

func (c *Config) GetTimeout() int {
    if c == nil { // nil 保护,让零值也可用
        return 30
    }
    return c.Timeout
}

var c *Config
fmt.Println(c.GetTimeout()) // 30

这是 Go 生态中"nil 接收者提供默认值"的惯用技巧(标准库 error 的包装类型也这样用)。

2.4 方法集与接口 ​

Receiver值类型 T 的方法集指针类型 *T 的方法集
值方法 func (t T) M()含 M含 M
指针方法 func (t *T) M()不含 M含 M

值类型 T 只拥有值方法;只有 *T 才拥有全部方法。这导致:var _ Interface = T{} 若 Interface 声明了指针方法会编译失败。因此实现接口的统一写法是传指针:var _ Handler = &HandlerImpl{}。


三、interface 与类型系统 ​

3.1 接口定义与鸭子类型 ​

Go 接口是方法的集合,实现是隐式的:只要类型具备接口要求的全部方法,就自动满足该接口,无需 implements 关键字。类型实现接口不需要 import 接口所在包(Java 则必须),因此第三方库类型天然可以补实现本地接口。

go
type Stringer interface {
    String() string
}

type Point struct{ X, Y int }

func (p Point) String() string { return fmt.Sprintf("(%d,%d)", p.X, p.Y) }

func printAny(s Stringer) { fmt.Println(s.String()) }
// Point 自动满足 Stringer,可传入 printAny
type Stringer interface {
    String() string
}

type Point struct{ X, Y int }

func (p Point) String() string { return fmt.Sprintf("(%d,%d)", p.X, p.Y) }

func printAny(s Stringer) { fmt.Println(s.String()) }
// Point 自动满足 Stringer,可传入 printAny

3.2 空接口 interface{} 与 any ​

interface{}(Go 1.18 起有别名 any)不含任何方法,可装任意类型,等价于 Java Object / Python 任意对象:

go
var v any = 42
v = "str"
v = map[string]int{"a": 1}
var v any = 42
v = "str"
v = map[string]int{"a": 1}

注意:any 只是"类型擦除",使用时必须断言回具体类型,不会像 Python 那样自动鸭子调用方法。

3.3 类型断言与 type switch ​

go
var v any = "hello"

s, ok := v.(string) // 带 ok 的断言,失败不 panic
if ok { fmt.Println("是字符串:", s) }

// 单返回值断言 v.(T) 失败会 panic,推荐总是用双返回值写法

switch t := v.(type) { // type switch 专用语法
case string:
    fmt.Println("string:", t)
case int:
    fmt.Println("int:", t)
default:
    fmt.Printf("未知类型 %T\n", v)
}
var v any = "hello"

s, ok := v.(string) // 带 ok 的断言,失败不 panic
if ok { fmt.Println("是字符串:", s) }

// 单返回值断言 v.(T) 失败会 panic,推荐总是用双返回值写法

switch t := v.(type) { // type switch 专用语法
case string:
    fmt.Println("string:", t)
case int:
    fmt.Println("int:", t)
default:
    fmt.Printf("未知类型 %T\n", v)
}

3.4 interface 的两个经典陷阱 ​

陷阱一:接口为 nil 但内部指针非 nil。

go
var err error
var p *MyErr = nil
err = p        // 把 nil 指针装进接口,接口不为 nil!
if err != nil { // 永远为 true,判空失效
    fmt.Println("有错误") // 必走这里
}
var err error
var p *MyErr = nil
err = p        // 把 nil 指针装进接口,接口不为 nil!
if err != nil { // 永远为 true,判空失效
    fmt.Println("有错误") // 必走这里
}

原因:接口由 (type, value) 二元组构成,p 的 type 非空所以接口非 nil。判断返回值前先显式 if p == nil { return nil }。

陷阱二:大接口难以实现与 mock。 只声明自己需要的最小方法集(见 3.5)。

3.5 接口设计原则:小接口 ​

  • 接口属于消费者:在需要接口的地方声明,而不是"先定义一堆接口再实现"。
  • 接受接口,返回结构体(Accept interfaces, return structs)。
  • 依赖倒置常用自查断言:var _ Repository = (*MysqlRepo)(nil),编译期验证实现完整性。
  • 标准库榜样:io.Reader、io.Writer 都只有一个方法。

四、错误处理 ​

4.1 error 是一个接口 ​

go
type error interface {
    Error() string
}
type error interface {
    Error() string
}

error 是普通值:可返回、可比较(与 nil)、可存储、可包装。函数结尾永远显式处理它:

go
f, err := os.Open("a.txt")
if err != nil {
    // 处理:打印 + return,绝不 _ = err 吞掉
    return err
}
defer f.Close()
f, err := os.Open("a.txt")
if err != nil {
    // 处理:打印 + return,绝不 _ = err 吞掉
    return err
}
defer f.Close()

4.2 错误包装:fmt.Errorf + %w ​

Go 1.13 引入错误包装,用 %w 保留原错误链:

go
if err := biz(); err != nil {
    return fmt.Errorf("调用 biz 失败: %w", err) // %w 包装,err 链可追溯
}
if err := biz(); err != nil {
    return fmt.Errorf("调用 biz 失败: %w", err) // %w 包装,err 链可追溯
}

%v 则只是拼接字符串,不保留链。

4.3 errors.Is 与 errors.As ​

go
var ErrNotFound = errors.New("record not found")

// errors.Is:判断错误链上是否"等于"目标错误(适合哨兵错误)
if errors.Is(err, ErrNotFound) {
    // 处理未找到
}

// errors.As:把链上某一层错误断言到具体类型(适合携带上下文的错误)
var notFound *NotFoundError
if errors.As(err, &notFound) {
    fmt.Println("id =", notFound.ID)
}
var ErrNotFound = errors.New("record not found")

// errors.Is:判断错误链上是否"等于"目标错误(适合哨兵错误)
if errors.Is(err, ErrNotFound) {
    // 处理未找到
}

// errors.As:把链上某一层错误断言到具体类型(适合携带上下文的错误)
var notFound *NotFoundError
if errors.As(err, &notFound) {
    fmt.Println("id =", notFound.ID)
}

Java 对比:errors.Is 类似 instanceof 特定异常 + cause 链遍历,errors.As 类似 catch (XxxException e) 拿到异常对象;区别是 Go 没有自动栈回溯(错误链要手动 %w 一层层拼)。

4.4 自定义错误类型 ​

go
type BizError struct {
    Code int
    Msg  string
}

func (e *BizError) Error() string { return e.Msg }

func DoSomething() error {
    return &BizError{Code: 1001, Msg: "余额不足"}
}

// 使用方:
var be *BizError
if errors.As(err, &be) {
    httpCode := be.Code // 拿到结构化字段,用于响应封装
}
type BizError struct {
    Code int
    Msg  string
}

func (e *BizError) Error() string { return e.Msg }

func DoSomething() error {
    return &BizError{Code: 1001, Msg: "余额不足"}
}

// 使用方:
var be *BizError
if errors.As(err, &be) {
    httpCode := be.Code // 拿到结构化字段,用于响应封装
}

4.5 panic 与 recover ​

panic 用于"不可恢复的错误"(数组越界、断言失败、显式 panic),等价 Java RuntimeException 中不打算 catch 的那一类:

go
func safe(f func()) (err error) {
    defer func() { // recover 必须在 defer 中执行才生效
        if r := recover(); r != nil {
            err = fmt.Errorf("panic: %v", r)
        }
    }()
    f()
    return nil
}

func main() {
    if err := safe(func() { panic("boom") }); err != nil {
        fmt.Println("已恢复:", err)
    }
}
func safe(f func()) (err error) {
    defer func() { // recover 必须在 defer 中执行才生效
        if r := recover(); r != nil {
            err = fmt.Errorf("panic: %v", r)
        }
    }()
    f()
    return nil
}

func main() {
    if err := safe(func() { panic("boom") }); err != nil {
        fmt.Println("已恢复:", err)
    }
}

使用纪律:

  • 业务错误用 error 返回,不要用 panic/recover 做控制流(性能差且难维护)。
  • panic 只留给"程序状态已损坏"的场景;Web 层由框架统一 recover 转 500(Gin 自带 Recovery)。
  • Go 1.21+ 提供 panic(nil) 恢复后可用 recover() != nil 判断。

4.6 三语言错误处理对比 ​

维度GoJavaPython
显式性error 返回值,层层判断异常隐式上抛异常隐式上抛
追踪链%w 手动包装printStackTrace 自动traceback 自动
忽略成本显式丢弃才忽略不 catch 就上抛不 catch 就上抛
自定义类型实现 Error() 的结构体Exception 子类Exception 子类
风险忘记处理吞异常 / 空 catch裸 except

Go 的哲学是"错误是值",逼你在每个边界想清楚;代价是代码噪声多,收益是控制流永远清晰。


五、反射 reflect ​

5.1 反射三法则 ​

Go 反射基于 reflect.Value 与 reflect.Type 两个核心类型,三条法则:

  1. 接口值可反射成反射对象:t := reflect.TypeOf(x)、v := reflect.ValueOf(x)。
  2. 反射对象可还原为接口值:v.Interface()。
  3. 要修改反射对象,其值必须可设置(settable),即来自指针:reflect.ValueOf(&x).Elem()。
go
type User struct {
    Name string `validate:"required"`
    Age  int    `validate:"gte=0"`
}

func main() {
    u := User{}
    vt := reflect.TypeOf(u)
    for i := 0; i < vt.NumField(); i++ {
        f := vt.Field(i)
        fmt.Println(f.Name, "tag:", f.Tag.Get("validate"))
    }
}
type User struct {
    Name string `validate:"required"`
    Age  int    `validate:"gte=0"`
}

func main() {
    u := User{}
    vt := reflect.TypeOf(u)
    for i := 0; i < vt.NumField(); i++ {
        f := vt.Field(i)
        fmt.Println(f.Name, "tag:", f.Tag.Get("validate"))
    }
}

5.2 典型用途:写一个迷你校验器 ​

go
func Validate(x any) error {
    v := reflect.ValueOf(x)
    t := v.Type()
    for i := 0; i < v.NumField(); i++ {
        tag := t.Field(i).Tag.Get("validate")
        switch tag {
        case "required":
            if v.Field(i).IsZero() {
                return fmt.Errorf("字段 %s 必填", t.Field(i).Name)
            }
        case "gt=0":
            if v.Field(i).Int() <= 0 {
                return fmt.Errorf("字段 %s 必须大于 0", t.Field(i).Name)
            }
        }
    }
    return nil
}
func Validate(x any) error {
    v := reflect.ValueOf(x)
    t := v.Type()
    for i := 0; i < v.NumField(); i++ {
        tag := t.Field(i).Tag.Get("validate")
        switch tag {
        case "required":
            if v.Field(i).IsZero() {
                return fmt.Errorf("字段 %s 必填", t.Field(i).Name)
            }
        case "gt=0":
            if v.Field(i).Int() <= 0 {
                return fmt.Errorf("字段 %s 必须大于 0", t.Field(i).Name)
            }
        }
    }
    return nil
}

生产环境直接用 go-playground/validator,它内部就是这么用 Tag + 反射实现的。

5.3 反射的代价与替代 ​

  • 性能:反射调用比直接调用慢一到两个数量级,热点路径不要用。
  • 可读性:牺牲静态类型安全,能编译过不代表逻辑对。
  • 替代:优先用泛型(编译期解决)、代码生成(stringer / mockgen)、或明确的小接口设计,反射留作最后手段。标准库自身(json、fmt)重度使用反射,这是合理场景。

六、泛型(Go 1.18+) ​

6.1 类型参数 ​

go
func Map[T any, R any](s []T, fn func(T) R) []R {
    out := make([]R, 0, len(s))
    for _, v := range s {
        out = append(out, fn(v))
    }
    return out
}

// 调用时类型可自动推导
nums := []int{1, 2, 3}
doubled := Map(nums, func(v int) int { return v * 2 })
func Map[T any, R any](s []T, fn func(T) R) []R {
    out := make([]R, 0, len(s))
    for _, v := range s {
        out = append(out, fn(v))
    }
    return out
}

// 调用时类型可自动推导
nums := []int{1, 2, 3}
doubled := Map(nums, func(v int) int { return v * 2 })

6.2 约束与 comparable ​

go
type Number interface {
    ~int | ~int64 | ~float64 // ~ 允许底层类型为 int64 的自定义类型
}

func Sum[T Number](s []T) T {
    var total T
    for _, v := range s {
        total += v
    }
    return total
}
type Number interface {
    ~int | ~int64 | ~float64 // ~ 允许底层类型为 int64 的自定义类型
}

func Sum[T Number](s []T) T {
    var total T
    for _, v := range s {
        total += v
    }
    return total
}
  • any = 无约束;comparable 是内建约束(可 == 比较的类型),map 键、泛型集合必备。
  • 自定义约束用接口表达:interface{ ~int | ~string }。

6.3 泛型应用与标准库 ​

Go 1.21 新增 slices 与 maps 两个泛型标准库包:

go
import "slices"

s := []int{3, 1, 2}
slices.Sort(s)
idx := slices.Index(s, 2)      // 1
filtered := slices.Delete(s, 0, 1)
cloned := slices.Clone(s)
import "slices"

s := []int{3, 1, 2}
slices.Sort(s)
idx := slices.Index(s, 2)      // 1
filtered := slices.Delete(s, 0, 1)
cloned := slices.Clone(s)
go
import "maps"

m := map[string]int{"a": 1}
maps.DeleteFunc(m, func(k string, v int) bool { return v == 1 })
m2 := maps.Clone(m)
import "maps"

m := map[string]int{"a": 1}
maps.DeleteFunc(m, func(k string, v int) bool { return v == 1 })
m2 := maps.Clone(m)

6.4 泛型限制(相比 Java / C#) ​

  • 不支持泛型方法(方法不能额外声明类型参数),只能泛型类型 + 泛型函数。
  • 无协变/逆变,[]T 不能赋给 []any。
  • 类型推断能力有限,复杂场景需显式写类型参数。
  • 实参不支持类型参数本身的方法约束之外的运算(需要约束接口显式声明)。

Java 开发者视角:Go 泛型是"语法糖级"的实现,无法覆盖 Java 泛型容器的全部场景;官方建议优先"碰巧能用",不要为抽象而抽象。


七、单元测试与基准测试 ​

7.1 单元测试与表驱动 ​

测试文件 xxx_test.go,函数名 TestXxx(t *testing.T),错误用 t.Errorf / t.Fatal:

go
// add.go
func add(a, b int) int { return a + b }

// add_test.go
func TestAdd(t *testing.T) {
    cases := []struct {
        name string
        a, b int
        want int
    }{
        {"正数", 1, 2, 3},
        {"含零", 0, 5, 5},
        {"负数", -1, -1, -2},
    }
    for _, c := range cases {
        t.Run(c.name, func(t *testing.T) { // 子测试
            if got := add(c.a, c.b); got != c.want {
                t.Errorf("add(%d,%d) = %d, want %d", c.a, c.b, got, c.want)
            }
        })
    }
}
// add.go
func add(a, b int) int { return a + b }

// add_test.go
func TestAdd(t *testing.T) {
    cases := []struct {
        name string
        a, b int
        want int
    }{
        {"正数", 1, 2, 3},
        {"含零", 0, 5, 5},
        {"负数", -1, -1, -2},
    }
    for _, c := range cases {
        t.Run(c.name, func(t *testing.T) { // 子测试
            if got := add(c.a, c.b); got != c.want {
                t.Errorf("add(%d,%d) = %d, want %d", c.a, c.b, got, c.want)
            }
        })
    }
}

运行:go test ./...,-v 显示详情,-run TestAdd/负数 只跑指定子测试。

7.2 并行测试与跳过 ​

go
func TestSlow(t *testing.T) {
    t.Parallel() // 与其它 Parallel 测试并行
    time.Sleep(100 * time.Millisecond)
}

func TestOptional(t *testing.T) {
    if os.Getenv("RUN_E2E") == "" {
        t.Skip("未设置 RUN_E2E,跳过") // 标记跳过,不算失败
    }
}
func TestSlow(t *testing.T) {
    t.Parallel() // 与其它 Parallel 测试并行
    time.Sleep(100 * time.Millisecond)
}

func TestOptional(t *testing.T) {
    if os.Getenv("RUN_E2E") == "" {
        t.Skip("未设置 RUN_E2E,跳过") // 标记跳过,不算失败
    }
}

7.3 基准测试 benchmark ​

go
func BenchmarkConcat(b *testing.B) {
    for i := 0; i < b.N; i++ { // b.N 由框架自动调整
        _ = "a" + "b"
    }
}
func BenchmarkConcat(b *testing.B) {
    for i := 0; i < b.N; i++ { // b.N 由框架自动调整
        _ = "a" + "b"
    }
}

运行 go test -bench=. -benchmem,输出每次操作耗时与内存分配:

text
BenchmarkConcat-8   100000000   12.36 ns/op   0 B/op   0 allocs/op
BenchmarkConcat-8   100000000   12.36 ns/op   0 B/op   0 allocs/op

alloc/op 与 allocs/op 是 Go 性能优化的第一信号——减少堆分配往往是最大收益(见实践篇)。

7.4 httptest 与依赖替身 ​

go
func TestHandler(t *testing.T) {
    req := httptest.NewRequest("GET", "/ping", nil)
    w := httptest.NewRecorder()
    PingHandler(w, req)
    if w.Code != http.StatusOK || w.Body.String() != "pong" {
        t.Fatalf("响应异常: %d %s", w.Code, w.Body.String())
    }
}
func TestHandler(t *testing.T) {
    req := httptest.NewRequest("GET", "/ping", nil)
    w := httptest.NewRecorder()
    PingHandler(w, req)
    if w.Code != http.StatusOK || w.Body.String() != "pong" {
        t.Fatalf("响应异常: %d %s", w.Code, w.Body.String())
    }
}

对外部依赖(DB / Redis)用接口 + 假实现(stub)或 testify/mock,web 层可用 httptest.Server 起真实测试服。

7.5 覆盖率与质量门禁 ​

bash
go test -cover ./...
go test -coverprofile=cover.out ./...
go tool cover -html=cover.out   # 浏览器查看
go test -cover ./...
go test -coverprofile=cover.out ./...
go tool cover -html=cover.out   # 浏览器查看

建议:核心 service 层覆盖率 80%+;go test -race ./... 每次 CI 必跑(数据竞争检测,见并发篇)。


八、常用标准库速览 ​

8.1 fmt:格式化输出 ​

占位符含义示例输出
%v默认格式(结构体带字段名){1 zhang}
%+v结构体含字段名{ID:1 Name:zhang}
%#vGo 语法表示(调试利器)main.User{ID:1, Name:"zhang"}
%T类型名main.User
%d / %s / %f / %t十进制 / 字符串 / 浮点 / 布尔
%q带引号字符串(含转义)"a\nb"

fmt.Errorf 用于造错误;fmt.Sprintf 用于格式化字符串;fmt.Println 用于调试打印。生产日志不要用 fmt,交给 zap 等日志库。

8.2 io 与 bufio ​

io.Reader / io.Writer 是最重要的两个单方法接口,所有流式 I/O 围绕它们构建:

go
func copyFile(dst, src string) error {
    in, err := os.Open(src)
    if err != nil { return err }
    defer in.Close()

    out, err := os.Create(dst)
    if err != nil { return err }
    defer out.Close()

    _, err = io.Copy(out, in) // io.Copy 内部用 32KB 缓冲
    return err
}
func copyFile(dst, src string) error {
    in, err := os.Open(src)
    if err != nil { return err }
    defer in.Close()

    out, err := os.Create(dst)
    if err != nil { return err }
    defer out.Close()

    _, err = io.Copy(out, in) // io.Copy 内部用 32KB 缓冲
    return err
}

文本逐行读用 bufio.Scanner(比 ReadString 更省内存,注意其默认 token 上限 64KB,超长行需调 Buffer)。

8.3 net/http:标准库即够用 ​

go
func main() {
    http.HandleFunc("/ping", func(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("Content-Type", "application/json")
        w.Write([]byte(`{"msg":"pong"}`))
    })
    http.ListenAndServe(":8080", nil) // nil 用 DefaultServeMux
}
func main() {
    http.HandleFunc("/ping", func(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("Content-Type", "application/json")
        w.Write([]byte(`{"msg":"pong"}`))
    })
    http.ListenAndServe(":8080", nil) // nil 用 DefaultServeMux
}

标准库的 http.Server 是 Gin 等框架的地基;框架只负责路由与中间件,HTTP 底层能力全部来自 net/http(含 HTTP/2、TLS、连接池)。

8.4 context:超时与取消传递 ​

context 用于在调用链上传递截止时间、取消信号与少量元数据:

go
func handle(r *http.Request) {
    ctx := r.Context() // 请求取消时自动取消
    ctx, cancel := context.WithTimeout(ctx, 2*time.Second)
    defer cancel()

    result, err := queryDB(ctx, "select ...") // DB 操作监听 ctx.Done()
    _ = result
}
func handle(r *http.Request) {
    ctx := r.Context() // 请求取消时自动取消
    ctx, cancel := context.WithTimeout(ctx, 2*time.Second)
    defer cancel()

    result, err := queryDB(ctx, "select ...") // DB 操作监听 ctx.Done()
    _ = result
}
  • context 只应作为函数第一个参数传递,命名建议 ctx。
  • 不要存业务字段到 context(跨层隐藏依赖),只放请求 ID 之类横切信息。
  • 子 context 取消会级联,父级不受子级影响。

8.5 time:Layout 不是 Format ​

go
now := time.Now()
fmt.Println(now.Format("2006-01-02 15:04:05")) // 注意:不是 yyyy-MM-dd!
now := time.Now()
fmt.Println(now.Format("2006-01-02 15:04:05")) // 注意:不是 yyyy-MM-dd!

Go 的时间格式是参考时间 2006-01-02 15:04:05.999999999 -0700 MST(1月2日 3点4分5秒 06年 -7时区),每个数字都是固定语义。常用常量:time.RFC3339(2006-01-02T15:04:05Z07:00,JSON 默认输出格式)。存储与传输统一用 UTC,展示时转本地时区(见实践篇时区坑)。

8.6 encoding/json ​

go
type User struct {
    ID    int    `json:"id"`
    Name  string `json:"name"`
    Tags  []string `json:"tags,omitempty"`
}

u := User{ID: 1, Name: "zhang"}
data, _ := json.Marshal(u)   // 默认转义 HTML:< > & 变 \u003c...
var u2 User
json.Unmarshal(data, &u2)    // 解码必须传指针
type User struct {
    ID    int    `json:"id"`
    Name  string `json:"name"`
    Tags  []string `json:"tags,omitempty"`
}

u := User{ID: 1, Name: "zhang"}
data, _ := json.Marshal(u)   // 默认转义 HTML:< > & 变 \u003c...
var u2 User
json.Unmarshal(data, &u2)    // 解码必须传指针

关键点:

  • 解码必须传指针(&u2),传值会得到空结果或报错。
  • 字段名默认用结构体字段名(大驼峰);需要小写下划线 json 名就写 tag。
  • 中文默认被转义成 \uXXXX;需要原文输出时:
    go
    enc := json.NewEncoder(w)
    enc.SetEscapeHTML(false)
    enc.Encode(u)
    enc := json.NewEncoder(w)
    enc.SetEscapeHTML(false)
    enc.Encode(u)
  • 未知字段默认静默忽略;用 json.Decoder.DisallowUnknownFields() 可严格校验。
  • 大小写问题:json.Unmarshal 匹配 tag 后还会大小写不敏感匹配字段名,且数字会先解码为 float64(对 interface{} 场景),这是经典坑(见实践篇)。

8.7 其它值得掌握的库 ​

包用途
os / path/filepath文件与路径
sort / slices排序(1.21 起用 slices 泛型版)
regexp正则(RE2 语法,无回溯,与 PCRE 有差异)
crypto/*哈希、AES、RSA(JWT 底层)
sync并发原语(详见并发篇)
flag / os.Args命令行参数(简单场景)
log/slogGo 1.21 结构化日志标准库(轻量场景可替代 zap)

本篇到此,基础语法之上最核心的语言机制已经打通。接下来进入 Go 最具特色的领域:Go 并发编程,理解 goroutine 与 channel 的调度模型。