lab/note/资源/编程/Golang/使用注意.md
张育新 be711334fb docs(note): 添加笔记目录及 gitignore 规则
- 新增 note 目录包含 AI、SQL、Golang、网络等多领域学习笔记
- 更新 .gitignore 添加 note/assets/ 和 note/项目/ 忽略规则
2026-07-10 09:24:40 +08:00

5.6 KiB
Raw Blame History

正确关闭HTTP响应

注意事项

  • 先检查 err,再 defer close

  • 必须消费完响应体,否则连接无法复用

  • defer 会延迟到函数结束,如果需要在循环中处理大量响应,建议手动关闭(避免 defer 累积)

resp, err := http.Get("https://example.com")
if err != nil {
    // 处理请求错误(此时 resp 可能为 nil
    return err
}
// 确保在函数退出时关闭响应体
defer resp.Body.Close()

// 手动关闭,不用 defer
// io.Copy(io.Discard, resp.Body)
// resp.Body.Close()

// 读取或处理响应体(即使不需要内容也建议消费完)
body, err := io.ReadAll(resp.Body)
if err != nil {
    return err
}
// 使用 body ...

切片内存泄漏

即使原切片变量本身被置为 nil 或超出作用域,只要还存在任何一个指向其底层数组的子切片,整个底层数组就不会被回收

如果新切片需要长期保存,而原切片很大且即将被丢弃,那么直接使用原切片[:n] 会导致新切片底层仍然引用原切片的整个底层数组,使得那个大数组无法被 GC 回收,造成内存泄漏。此时应该用 copy 创建一份独立的、只包含所需元素的新切片。

func getFirstLines() []string {
    hugeLines := readHugeFile() // 假设这个切片有 100 万行,占用 100MB 内存
    // 错误做法:返回子切片
    return hugeLines[:3] // 虽然只返回3行但底层数组的 100MB 内存无法被回收!
}

// 正确做法
func getFirstLines() []string {
    hugeLines := readHugeFile()
    // 只拷贝需要的3行
    result := make([]string, 3)
    copy(result, hugeLines[:3])
    return result // 原来的 hugeLines 可以被GC回收只占用很小的内存
}

堆栈

核心区别

特性
分配/释放 移动指针,极快 需要查找 + GC 回收
生命周期 函数返回即销毁 GC 决定何时回收
内存布局 连续,无碎片 分散,有碎片
大小限制 较小MB级 较大GB级

逃逸判断规则

变量在以下情况逃逸到堆:

1. 返回指针        → func f() *int { x := 42; return &x }
2. 存入全局变量    → globalPtr = &x
3. 闭包捕获        → return func() { return x }
4. 发送到 channel   → ch <- &x
5. 存入外部结构体   → c.Value = &x
6. 接口装箱逃逸    → return interface{}(x)
7. 反射调用        → fmt.Println(x)
8. 大对象         → make([]int, 10000)

常见误区澄清

误区 正解
指针一定在堆上 函数内创建和使用的指针可在栈上
接口一定在堆上 不逃逸的接口内部值可在栈上
结构体指针一定逃逸 小结构体 + 内联优化可能栈分配

验证方法

# 查看逃逸分析
go build -gcflags="-m" main.go

# 更详细信息
go build -gcflags="-m -m" main.go

优化建议

// ✅ 推荐:小对象返回值
func getUser() User {
    return User{
        ID: 1
    }
}

// ❌ 避免:小对象返回指针(增加 GC 压力)
func getUser() *User {
    return &User{
        ID: 1
    }
}

// ✅ 推荐:传指针给只读函数
func read(p *Data) int {
    return p.Value
}

// ❌ 避免:在热路径使用 fmt.Print
// 用 strconv 或 log 代替

记忆口诀

生命周期越界 → 堆
外部有引用   → 堆
闭包捕获     → 堆
反射调用     → 堆

函数内使用   → 栈
返回值拷贝   → 栈
只读指针     → 栈

本质理解

= 函数的私有工作区,函数结束自动清理

= 共享仓库,需要 GC 定期打扫

逃逸分析 = 编译器判断"这个变量能不能安全地放在栈上"

类型转换/序列化

//int64转换string
strconv.FormatInt(num,10)

//序列化为string
jsoniter.ConfigCompatibleWithStandardLibrary.MarshalToString(input)

在 encode/decode JSON 数据并且数据类型是interface时Go 默认会将数值当做 float64 处理

type User struct {
	Name   string  `json:"name"`
	Age    int     `json:"age"`
	Height float64 `json:"height"`
}
type UserInter struct {
	Name   interface{} `json:"name"`
	Age    interface{} `json:"age"`
	Height interface{} `json:"height"`
}

func TestJson(t *testing.T) {
	one := &User{
		Name: "tom",
		Age:  18,
	}
	oneJson, err := jsoniter.Marshal(one)
	if err != nil {
		fmt.Println(err)
		return
	}
	var two UserInter
	var three map[string]interface{}
	err = jsoniter.Unmarshal(oneJson, &two)
	err = jsoniter.Unmarshal(oneJson, &three)
	if err != nil {
		fmt.Println(err)
		return
	}
	fmt.Println(reflect.TypeOf(two.Name))
	fmt.Println(reflect.TypeOf(two.Age))	//会打印float64
	fmt.Println(reflect.TypeOf(two.Height))	//会打印float64
	fmt.Println(reflect.TypeOf(three["name"]))
	fmt.Println(reflect.TypeOf(three["age"]))	//会打印float64
	fmt.Println(reflect.TypeOf(three["height"]))	//会打印float64

}

HTTP阅后即焚

处理 HTTP Body 只能读一次的问题时,根据你的数据量大小选择武器:

  1. 小请求配置、JSON:使用 io.ReadAll 存入内存,再通过 io.NopCloser 塞回 r.Body 或设置 r.GetBody
  2. 中等请求(签名校验):使用 io.TeeReader 边读边处理,节省一次全量扫描。
  3. 大请求(文件上传)禁止 ReadAll。直接流式 io.Copy 到目的地,或使用临时文件做中转。