## 正确关闭`HTTP`响应 > **注意事项** > > - 先检查 `err`,再 `defer close` > > - 必须消费完响应体,否则连接无法复用 > > - `defer` 会延迟到函数结束,如果需要在循环中处理大量响应,建议手动关闭(避免 `defer` 累积) ```go 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` 创建一份独立的、只包含所需元素的新切片。 ```go 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) ``` ### 常见误区澄清 | 误区 | 正解 | | ------------------ | -------------------------------- | | 指针一定在堆上 | ❌ 函数内创建和使用的指针可在栈上 | | 接口一定在堆上 | ❌ 不逃逸的接口内部值可在栈上 | | 结构体指针一定逃逸 | ❌ 小结构体 + 内联优化可能栈分配 | ### 验证方法 ```bash # 查看逃逸分析 go build -gcflags="-m" main.go # 更详细信息 go build -gcflags="-m -m" main.go ``` ### 优化建议 ```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 处理 ```go 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` 到目的地,或使用临时文件做中转。 ## map的坑 go的map内部是哈希表,元素会因 rehash(扩容)而移动到新的内存位置,如果允许 `&m[key]` 拿到一个指针,map 一旦扩容,这个指针就变成悬空指针(指向已失效的内存),为了避免这种内存安全问题,Go 直接禁止对 map 值取地址,下面是示例代码: ```go type User struct { Name string Age int } func main() { m := map[int]User{1: {Name: "张三", Age: 20}} // ❌ 编译错误:cannot assign to struct field m[1].Name in map m[1].Name = "李四" // ✅ 正确的替代写法 temp:=m[1] temp.Name = "李四" m[1]=temp } ``` > **可寻址(addressable)**:能通过 `&` 拿到它内存地址的操作数。 > > **不可寻址(not addressable)**:不能通过 `&` 拿到地址的操作数。 ## 句点引入 一个完整引入声明语句形式的引入名`importname`可以是一个句点(`.`)。 这样的引入称为句点引入。使用被句点引入的包中的导出代码要素时,限定标识符的前缀必须省略。 比如在下面这个例子中,`Println`和`Now`函数调用不需要带任何前缀。 ```go package main import ( . "fmt" . "time" ) func main() { Println("Current time:", Now()) } ``` > 一般来说,句点引入不推荐使用,因为它们会导致较低的代码可读性