docs(note): 添加笔记目录及 gitignore 规则
- 新增 note 目录包含 AI、SQL、Golang、网络等多领域学习笔记 - 更新 .gitignore 添加 note/assets/ 和 note/项目/ 忽略规则
This commit is contained in:
@@ -0,0 +1,123 @@
|
||||
## API
|
||||
|
||||
### goctl代码生成
|
||||
|
||||
可以在doc/genCode.md查看
|
||||
|
||||
```powershell
|
||||
# 生成多个types类型的话需要关闭这个实验性功能
|
||||
goctl env -w GOCTL_EXPERIMENTAL=off
|
||||
|
||||
# 代码生成,会自动处理引入的子api文件
|
||||
goctl api go -api background.api -dir . --home D:/zhangyuxin/template/1.8.3
|
||||
goctl api go -api login.api -dir . --home ../../../deploy/goctl
|
||||
goctl api go -api main.api -dir . --home ../../deploy/goctl
|
||||
```
|
||||
|
||||
### 参数校验
|
||||
|
||||
| 接收规则 | 说明 | 示例 |
|
||||
| -------- | ------------------------------------------ | ------------------------------- |
|
||||
| optional | 当前字段是可选参数,允许为零值(zero value) | `json:"foo,optional"` |
|
||||
| options | 当前参数仅可接收的枚举值 | `json:"gender,options=foo|bar"` |
|
||||
| default | 当前参数默认值 | `json:"gender,default=male"` |
|
||||
| range | 当前参数数值有效范围,支持开/闭区间 | `json:"age,range=[0:120]"` |
|
||||
|
||||
#### **`optional`** **与** **`omitempty`** **的区别**
|
||||
|
||||
两者都写在 json/form tag 的 options 里,但作用域完全不同,不可互相替代
|
||||
|
||||
##### `optional` —— goctl 专用,控制 Swagger 与请求校验
|
||||
|
||||
- **Swagger 生成**:标了 `optional` 的字段不进 `required` 数组,Apifox/Swagger UI 显示为可选;未标的字段进 `required` 数组,显示为必填
|
||||
- **请求校验**:goctl 生成的 logic 不会对标了 `optional` 的字段做必填校验
|
||||
- **判定逻辑**:goctl 遍历 tag options,找到 `optional` 关键字即判定为可选
|
||||
|
||||
##### `omitempty` —— Go 标准库指令,控制 JSON 序列化
|
||||
|
||||
- **JSON 序列化**:字段为零值(0、""、nil、false)时,响应 JSON 中**不输出**该字段
|
||||
- **Swagger 生成**:**goctl 完全不识别**,字段仍按未标 `optional` 处理,即判定为 required
|
||||
- **请求校验**:无影响
|
||||
|
||||
## RPC
|
||||
|
||||
### goctl代码生成
|
||||
|
||||
```powershell
|
||||
# 主proto文件代码生成
|
||||
goctl rpc protoc user.proto --go_out=. --go-grpc_out=. --zrpc_out=. --proto_path=. -m --home ../../../deploy/goctl
|
||||
|
||||
# 主proto文件中引入的proto代码生成
|
||||
protoc.exe -I ./ --go_out=. --go-grpc_out=. ./user_model.proto
|
||||
|
||||
```
|
||||
|
||||
当`proto`文件里面定义了多个 service的时候,需要额外注意,还要再main函数中手动注册一下
|
||||
|
||||
> 每个 proto 文件应该聚焦于一个核心概念/实体。一个文件里定义太多 service 通常是设计需要重新考虑的信号
|
||||
|
||||
注册示例
|
||||
|
||||
```go
|
||||
import (
|
||||
ebookServer "olive/services/product/rpc/internal/server/ebookservice"
|
||||
)
|
||||
|
||||
var configFile = flag.String("f", "etc/product.yaml", "the config file")
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
|
||||
var c config.Config
|
||||
conf.MustLoad(*configFile, &c)
|
||||
ctx := svc.NewServiceContext(c)
|
||||
|
||||
s := zrpc.MustNewServer(c.RpcServerConf, func(grpcServer *grpc.Server) {
|
||||
// 新服务
|
||||
productrpcmodel.RegisterEbookServiceServer(grpcServer, ebookServer.NewEbookServiceServer(ctx))
|
||||
})
|
||||
defer s.Stop()
|
||||
|
||||
fmt.Printf("Starting product RPC server at %s...\n", c.ListenOn)
|
||||
s.Start()
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
### proto代码生成错误:File not found错误
|
||||
|
||||
- 错误描述
|
||||
|
||||
执行 protoc 报 google/protobuf/timestamp.proto: File not found. 的错提示File not found
|
||||
|
||||
- 解决方式
|
||||
|
||||
重新下载[protoc](https://github.com/protocolbuffers/protobuf/releases),解压后,将整个include目录复制到protoc执行文件同一级目录下
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## model
|
||||
|
||||
可以在doc/genCode.md查看
|
||||
|
||||
**Model 层**:只做 **“业务循环”** + **“SQL 查询”** + **“结果封装”**,不关心字符串到时间的转换和 Protobuf 类型。
|
||||
|
||||
### goctl代码生成
|
||||
|
||||
```powershell
|
||||
# mysql根据远程数据库表生成
|
||||
goctl model mysql datasource --url="olive:xxvkikaDDx01&&99@tcp(127.0.0.1:3306)/ebming" --table="user_invitation_record" --dir="." -c --home ../../../../deploy/goctl/1.8.3
|
||||
|
||||
# mysql根据ddl生成
|
||||
goctl model mysql ddl --src h5.sql --dir . -c --home D:/zhangyuxin/template/1.8.3
|
||||
|
||||
# pgsql根据远程数据库表生成
|
||||
goctl model pg datasource -url "postgres://labman:tN5bWz2KpL9Fy4@127.0.0.1:15432/sern" -table user -dir ./user --cache --home ../../goctl_template/1.8.3
|
||||
|
||||
# pgsql根据远程数据库表生成,带模式(schema)
|
||||
goctl model pg datasource -url "postgres://admin:zhangyuxin@127.0.0.1:25432/test?sslmode=disable" -s "users" -table "profile" -dir ./go-zero/model/users/ --cache --home ../../deploy/goctl
|
||||
```
|
||||
|
||||
@@ -0,0 +1,212 @@
|
||||
|
||||
|
||||
## 正确关闭`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` 到目的地,或使用临时文件做中转。
|
||||
@@ -0,0 +1,55 @@
|
||||
## 交叉编译
|
||||
|
||||
### 基本用法
|
||||
|
||||
```bash
|
||||
# Linux/macOS
|
||||
GOOS=目标系统 GOARCH=目标架构 go build -o 输出文件名 源文件
|
||||
|
||||
# Windows (PowerShell)
|
||||
$env:GOOS="目标系统"; $env:GOARCH="目标架构"; go build -o 输出文件名 源文件
|
||||
|
||||
# Windows (CMD)
|
||||
set GOOS=目标系统 && set GOARCH=目标架构 && go build -o 输出文件名 源文件
|
||||
|
||||
```
|
||||
|
||||
查看所有支持的平台
|
||||
|
||||
```bash
|
||||
go tool dist list
|
||||
```
|
||||
|
||||
### 常用组合
|
||||
|
||||
| 目标平台 | GOOS | GOARCH |
|
||||
| ------------ | ------- | ------ |
|
||||
| Linux 64位 | linux | amd64 |
|
||||
| Linux ARM64 | linux | arm64 |
|
||||
| Windows 64位 | windows | amd64 |
|
||||
| macOS Intel | darwin | amd64 |
|
||||
| macOS M1/M2 | darwin | arm64 |
|
||||
|
||||
### 示例
|
||||
|
||||
```bash
|
||||
# Linux/Mac (Bash)
|
||||
# 编译 Linux 64位
|
||||
GOOS=linux GOARCH=amd64 go build -o app ./app/api/main.go
|
||||
# 编译 Windows 64位
|
||||
GOOS=windows GOARCH=amd64 go build -o app.exe ./app/api/main.go
|
||||
# 编译 macOS ARM64 (M1/M2)
|
||||
GOOS=darwin GOARCH=arm64 go build -o app ./app/api/main.go
|
||||
|
||||
# Windows (PowerShell)
|
||||
# 编译 Linux 64位
|
||||
$env:GOOS="linux"; $env:GOARCH="amd64"; go build -o app ./app/api/main.go
|
||||
# 编译 Windows 64位
|
||||
$env:GOOS="windows"; $env:GOARCH="amd64"; go build -o app.exe ./app/api/main.go
|
||||
# 编译 macOS ARM64 (M1/M2)
|
||||
$env:GOOS="darwin"; $env:GOARCH="arm64"; go build -o app ./app/api/main.go
|
||||
|
||||
```
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
|
||||
|
||||
### validator
|
||||
|
||||
[GitHub地址](github.com/go-playground/validator)
|
||||
|
||||
包验证器,可以用于验证参数是否符合规则,比如下方的比较验证,还有其他可以查看网址的详细介绍
|
||||
go-zero自带的有校验器,可以使用
|
||||
|
||||
| Tag | Description |
|
||||
| ---- | --------------------- |
|
||||
| eq | Equals |
|
||||
| gt | Greater than |
|
||||
| gte | Greater than or equal |
|
||||
| lt | Less Than |
|
||||
| lte | Less Than or Equal |
|
||||
| ne | Not Equal |
|
||||
|
||||
|
||||
|
||||
### goverter
|
||||
|
||||
[GitHub地址](https://github.com/jmattheis/goverter)
|
||||
|
||||
类型转换器,可以用于类型的转换,比如下面的代码
|
||||
|
||||
```go
|
||||
func (l *CreatePopupLogic) CreatePopup(in *backgroundrpcmodel.ResPopup) (*backgroundrpcmodel.ResResult, error) {
|
||||
_, err := l.svcCtx.PopupModel.Insert(l.ctx, u.Convert.SysPopupR2S(in.Info))
|
||||
if err != nil {
|
||||
return nil, rerr.NewFromSQL(err)
|
||||
}
|
||||
return &backgroundrpcmodel.ResResult{
|
||||
Result: true,
|
||||
}, nil
|
||||
}
|
||||
```
|
||||
|
||||
使用
|
||||
|
||||
```
|
||||
goverter gen .
|
||||
```
|
||||
|
||||
|
||||
|
||||
### asynq
|
||||
|
||||
[GitHub网址](https://github.com/hibiken/asynq)
|
||||
|
||||
- 入队流程
|
||||
|
||||
```go
|
||||
payload := &jobtype.xxxPayload{}
|
||||
payloadByte, err := jsoniter.Marshal(payload)
|
||||
if err != nil {
|
||||
logt.Error("xxx jsoniter.Marshal err:", err)
|
||||
return nil, zerr.New(rescode.DataFormatConversionError)
|
||||
}
|
||||
_, err = l.svcCtx.AsynqClient.Enqueue(asynq.NewTask(jobtype.xxx, payloadBytes, asynq.Queue(consts.QueueLow)))
|
||||
if err != nil {
|
||||
logt.Error("xxx enqueue err:", err)
|
||||
return nil, zerr.New(rescode.EnqueueErr)
|
||||
}
|
||||
```
|
||||
|
||||
- 自行定义了队列优先级,在入队的时候可以加参数进行选择
|
||||
|
||||
```
|
||||
consts.QueueCritical: 6,
|
||||
consts.QueueDefault: 3,
|
||||
consts.QueueLow: 1,
|
||||
```
|
||||
|
||||
- 处理前流程(反序列化,获取数据)
|
||||
|
||||
```go
|
||||
var payload jobtype.xxx
|
||||
err := json.Unmarshal(task.Payload(), &payload)
|
||||
if err != nil {
|
||||
logt.Error("xxx unmarshal payload err:", err)
|
||||
return zerr.New(rescode.DataFormatConversionError)
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
### gorm
|
||||
|
||||
[官网文档](https://gorm.io/zh_CN/docs/)
|
||||
|
||||
**使用gorm + sqlx,但是复用sql连接**
|
||||
|
||||
```go
|
||||
func NewServiceContext(c config.Config) *ServiceContext {
|
||||
logx.Infof("Cache config: %+v", c.CacheRedis)
|
||||
|
||||
gormDB, err := gorm.Open(postgres.Open(c.Database.DataSource), &gorm.Config{})
|
||||
if err != nil {
|
||||
msg := fmt.Sprintf("连接数据库失败 %s ", err.Error())
|
||||
logx.Error(msg)
|
||||
panic(msg)
|
||||
}
|
||||
sqlDB, err := gormDB.DB()
|
||||
if err != nil {
|
||||
msg := fmt.Sprintf("获取底层连接失败: %s", err.Error())
|
||||
logx.Error(msg)
|
||||
panic(msg)
|
||||
}
|
||||
sqlxConn := sqlx.NewSqlConnFromDB(sqlDB)
|
||||
return &ServiceContext{
|
||||
Config: c,
|
||||
UserAuthModel: models.NewUserAuthModel(sqlxConn, c.CacheRedis, gormDB),
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user