Go 进阶特性
指针与值语义、接口、错误处理、反射、泛型与测试,构建 Java / Python 工程师的 Go 中阶能力。
一、指针与值语义
1.1 指针是什么:& 与 *
Go 有指针,但没有 C 的指针算术(不能 p++ 移动指针)。指针保存变量内存地址,& 取地址,* 解引用:
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 / 函数 / 接口内部本就持有指向底层数据的引用,因此函数内修改"看起来像引用传递":
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 对照
| Go | Java | Python | 说明 |
|---|---|---|---|
*T / &x | 引用 | 引用 | Go 指针可显式传 nil |
| 值类型(int/struct/array) | 基本类型 / 不可变对象 | 不可变对象 | Go 结构体可整体复制 |
| slice / map | ArrayList / HashMap | list / dict | 均引用底层数据 |
| 无指针算术 | 无 | 无 | 安全性保证 |
二、struct 方法与 receiver
2.1 方法与 receiver
方法 = 函数 + 接收者(receiver)。接收者写在 func 与函数名之间:
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 接收者调用是合法的(只要方法内部不访问其字段):
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()) // 30type 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 则必须),因此第三方库类型天然可以补实现本地接口。
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,可传入 printAnytype 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,可传入 printAny3.2 空接口 interface{} 与 any
interface{}(Go 1.18 起有别名 any)不含任何方法,可装任意类型,等价于 Java Object / Python 任意对象:
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
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。
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 是一个接口
type error interface {
Error() string
}type error interface {
Error() string
}error 是普通值:可返回、可比较(与 nil)、可存储、可包装。函数结尾永远显式处理它:
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 保留原错误链:
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
var ErrNotFound = errors.New("record not found")
// errors.Is:判断错误链上是否"等于"目标错误(适合哨兵错误)
if errors.Is(err, ErrNotFound) {
// 处理未找到
}
// errors.As:把链上某一层错误断言到具体类型(适合携带上下文的错误)
var notFound *NotFoundError
if errors.As(err, ¬Found) {
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, ¬Found) {
fmt.Println("id =", notFound.ID)
}Java 对比:errors.Is 类似 instanceof 特定异常 + cause 链遍历,errors.As 类似 catch (XxxException e) 拿到异常对象;区别是 Go 没有自动栈回溯(错误链要手动 %w 一层层拼)。
4.4 自定义错误类型
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 的那一类:
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 三语言错误处理对比
| 维度 | Go | Java | Python |
|---|---|---|---|
| 显式性 | error 返回值,层层判断 | 异常隐式上抛 | 异常隐式上抛 |
| 追踪链 | %w 手动包装 | printStackTrace 自动 | traceback 自动 |
| 忽略成本 | 显式丢弃才忽略 | 不 catch 就上抛 | 不 catch 就上抛 |
| 自定义类型 | 实现 Error() 的结构体 | Exception 子类 | Exception 子类 |
| 风险 | 忘记处理 | 吞异常 / 空 catch | 裸 except |
Go 的哲学是"错误是值",逼你在每个边界想清楚;代价是代码噪声多,收益是控制流永远清晰。
五、反射 reflect
5.1 反射三法则
Go 反射基于 reflect.Value 与 reflect.Type 两个核心类型,三条法则:
- 接口值可反射成反射对象:
t := reflect.TypeOf(x)、v := reflect.ValueOf(x)。 - 反射对象可还原为接口值:
v.Interface()。 - 要修改反射对象,其值必须可设置(settable),即来自指针:
reflect.ValueOf(&x).Elem()。
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 典型用途:写一个迷你校验器
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 类型参数
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
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 两个泛型标准库包:
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)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:
// 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 并行测试与跳过
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
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,输出每次操作耗时与内存分配:
BenchmarkConcat-8 100000000 12.36 ns/op 0 B/op 0 allocs/opBenchmarkConcat-8 100000000 12.36 ns/op 0 B/op 0 allocs/opalloc/op 与 allocs/op 是 Go 性能优化的第一信号——减少堆分配往往是最大收益(见实践篇)。
7.4 httptest 与依赖替身
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 覆盖率与质量门禁
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} |
%#v | Go 语法表示(调试利器) | 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 围绕它们构建:
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:标准库即够用
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 用于在调用链上传递截止时间、取消信号与少量元数据:
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
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
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;需要原文输出时:goenc := 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/slog | Go 1.21 结构化日志标准库(轻量场景可替代 zap) |
本篇到此,基础语法之上最核心的语言机制已经打通。接下来进入 Go 最具特色的领域:Go 并发编程,理解 goroutine 与 channel 的调度模型。
Cao Note