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

213 lines
5.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

## 正确关闭`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` 到目的地,或使用临时文件做中转。