From babd8f2793b552f0ab863692bc54184564916cf0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E8=82=B2=E6=96=B0?= <2635131263@qq.com> Date: Thu, 24 Sep 2026 14:16:17 +0800 Subject: [PATCH] =?UTF-8?q?wip:=20=E4=BF=9D=E5=AD=98=20Go=20=E7=BB=83?= =?UTF-8?q?=E4=B9=A0=E5=92=8C=E6=8A=80=E6=9C=AF=E7=AC=94=E8=AE=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/plans/2026-09-18-casbin-v3-notes.md | 39 +++ go/algorithms/linkedlist/linkedlist.go | 100 ++++++ go/algorithms/main.go | 29 ++ go/basics/goroutine/leak/main.go | 50 +++ .../{MemoryEscape => memoryEscape}/main.go | 0 go/basics/pprof/main.go | 56 ++++ go/basics/session/main.go | 116 +++++++ go/go.mod | 48 ++- go/go.sum | 118 +++++-- go/testing/crypto/bcrypt_test.go | 149 +++++++++ go/testing/json/json_test.go | 68 ++++ go/testing/normal_test.go | 61 ++++ .../MySQL的字符集坑.md} | 0 .../MySQL空表JOIN导致排序索引失效-实验笔记.md | 0 note/资源/编程/Golang/标准库.md | 7 +- note/资源/编程/Golang/版本变化/1.27.md | 294 ++++++++++++++++++ note/资源/编程/避坑/squirrel的坑.md | 45 +++ note/随手记/casbin学习笔记.md | 61 ++++ note/随手记/mcp.md | 5 + note/随手记/常见图片格式区别.md | 12 + 20 files changed, 1229 insertions(+), 29 deletions(-) create mode 100644 docs/plans/2026-09-18-casbin-v3-notes.md create mode 100644 go/algorithms/linkedlist/linkedlist.go create mode 100644 go/algorithms/main.go create mode 100644 go/basics/goroutine/leak/main.go rename go/basics/{MemoryEscape => memoryEscape}/main.go (100%) create mode 100644 go/basics/pprof/main.go create mode 100644 go/basics/session/main.go create mode 100644 go/testing/crypto/bcrypt_test.go create mode 100644 go/testing/json/json_test.go rename note/资源/数据库/SQL/{mysql的字符集坑.md => 避坑/MySQL的字符集坑.md} (100%) rename note/{随手记 => 资源/数据库/SQL/避坑}/MySQL空表JOIN导致排序索引失效-实验笔记.md (100%) create mode 100644 note/资源/编程/Golang/版本变化/1.27.md create mode 100644 note/资源/编程/避坑/squirrel的坑.md create mode 100644 note/随手记/casbin学习笔记.md create mode 100644 note/随手记/mcp.md create mode 100644 note/随手记/常见图片格式区别.md diff --git a/docs/plans/2026-09-18-casbin-v3-notes.md b/docs/plans/2026-09-18-casbin-v3-notes.md new file mode 100644 index 0000000..0bb65ce --- /dev/null +++ b/docs/plans/2026-09-18-casbin-v3-notes.md @@ -0,0 +1,39 @@ +# Casbin v3 笔记更新方案 + +## 范围 + +- 仅更新 `note/随手记/casbin学习笔记.md`,不修改项目 Go 依赖。 +- 本文件记录方案;笔记实施前等待用户确认。 + +## 已核实依据 + +- GitHub 最新正式 release 为 v3.11.0(2026-08-20)。 +- v3.11.0 的 module 仍为 `github.com/casbin/casbin/v3`,尽管源码仓库已迁移至 apache/casbin。 +- 本次查询 Go proxy 的旧模块路径 `@latest` 返回 v3.10.0;不能将 GitHub 最新发布与代理解析结果混为一谈。 +- 来源:https://github.com/apache/casbin/releases/tag/v3.11.0 +- 来源:https://raw.githubusercontent.com/apache/casbin/v3.11.0/go.mod + +## 实施顺序 + +1. 保留原有学习笔记结构,新增版本基线、v2 到 v3 的迁移注意事项与官方来源。 +2. 修改示例 import;纠正 CSV 加载错误处理、上下文生命周期、Watcher/自动重载、延迟加载、ABAC 能力边界等过时或绝对化描述。不要把既有能力或新增示例误写成 v3 新能力。 +3. 完善最小 RBAC 示例错误处理;检查差异、代码块和引用,条件允许时在临时目录验证示例,不修改仓库依赖。 + +## 职责与验收 + +- librarian:只读核实官方事实(已完成)。 +- fixer:用户确认后仅实施目标笔记,编辑前确认编码和换行,保持原有中文笔记风格。 +- 主会话:核对最终 diff 和关键事实,说明实际验证情况及外部依赖限制。 + +## 完成状态 + +- [x] 已更新 `note/随手记/casbin学习笔记.md`(2026-09-18):头部版本基线(v3.11.0、module path、官方来源、release 与 proxy `@latest` 区别)、三元组只是典型模型、g 的角色继承表述、SyncedEnforcer 并发与 matcher 编译缓存、adapter 表结构非核心标准、LoadPolicyArray/Line 校验责任、只读 adapter、ctx 生命周期与 ContextAdapter/ContextEnforcer、Watcher/Dispatcher/StartAutoLoadPolicy、快照时序保护与去性能硬承诺、最小示例改 `/v3` 并完整处理 error、ABAC 能力边界、govaluate 来源与 GetPolicy 签名、延迟加载顺序。 +- [x] 示例已抽取到 `/tmp/opencode/casbin-v3-verify` 独立 module(`github.com/casbin/casbin/v3 v3.11.0`)验证:`go run .` 输出 `true false false`;`NewSyncedEnforcer(m)` → `SetAdapter(a)` → `LoadPolicy()` 顺序 `go run ./delayed` 输出 `delayed-load: true`。 +- 未改动仓库 Go 依赖,未提交 git;最终 diff 与事实由主会话复核。 + +## 精简方案(2026-09-18 二次修订) + +- 目标:从 213 行压到约 60–85 行(原长度约 1/3),定位为中文概念速记,不是教程;不写/少写具体 API 使用方法。 +- 保留:简短版本基线与 import 路径;“授权而非认证”一句话;PERM 四段简洁解释 + role_definition 扩展;核心组件 Model/Policy/Enforcer/Adapter/RoleManager/Watcher 职责表;一条判定链与 `alice/admin/report/read` 极短策略示意(无完整 Go 代码);关键提醒(deny/error 均 fail-closed、并发优先 SyncedEnforcer、内存策略不自动跨实例同步、ABAC 不自动 SQL 过滤、迁移核对 adapter `/v3` 依赖);2–3 条官方来源。 +- 删除:加载入口对比、SQL 表结构、接口签名、CSV/context 细节、完整程序、只读消费与快照架构、MaxStale/性能讨论、proxy `@latest` 偏差等非核心展开及其历史修订细节。 +- 约束:避免过度简化造成事实错误;不改代码与依赖;不提交;改动仅限本方案与目标笔记。 diff --git a/go/algorithms/linkedlist/linkedlist.go b/go/algorithms/linkedlist/linkedlist.go new file mode 100644 index 0000000..a24973b --- /dev/null +++ b/go/algorithms/linkedlist/linkedlist.go @@ -0,0 +1,100 @@ +package linkedlist + +import ( + "fmt" + "strings" +) + +// Node 是单向链表的一个节点,持有数据与指向下一个节点的指针。 +type Node[T comparable] struct { + Data T + Next *Node[T] +} + +// LinkedList 是持有头指针的单向链表,零值即为空表。 +type LinkedList[T comparable] struct { + head *Node[T] +} + +// InsertHead 将新节点插入表头。 +func (l *LinkedList[T]) InsertHead(data T) { + l.head = &Node[T]{Data: data, Next: l.head} +} + +// InsertTail 将新节点追加到表尾;空表时新节点直接成为表头。 +func (l *LinkedList[T]) InsertTail(data T) { + newNode := &Node[T]{Data: data} + if l.head == nil { + l.head = newNode + return + } + current := l.head + for current.Next != nil { + current = current.Next + } + current.Next = newNode +} + +// DeleteHead 删除表头节点;空表时无操作。 +func (l *LinkedList[T]) DeleteHead() { + if l.head != nil { + l.head = l.head.Next + } +} + +// DeleteTail 删除表尾节点;空表时无操作,仅剩一个节点时表变空。 +func (l *LinkedList[T]) DeleteTail() { + switch { + case l.head == nil: + case l.head.Next == nil: + l.head = nil + default: + current := l.head + for current.Next.Next != nil { + current = current.Next + } + current.Next = nil + } +} + +// Search 返回首个 Data 等于 data 的节点;不存在时返回 nil。 +func (l *LinkedList[T]) Search(data T) *Node[T] { + for current := l.head; current != nil; current = current.Next { + if current.Data == data { + return current + } + } + return nil +} + +// Len 返回链表的节点个数。 +func (l *LinkedList[T]) Len() int { + count := 0 + for current := l.head; current != nil; current = current.Next { + count++ + } + return count +} + +// Reverse 原地反转链表。 +func (l *LinkedList[T]) Reverse() { + var prev, current *Node[T] + current = l.head + for current != nil { + next := current.Next + current.Next = prev + prev = current + current = next + } + l.head = prev +} + +// String 以 "1 -> 2 -> nil" 的格式输出整条链,便于打印与调试。 +func (l LinkedList[T]) String() string { + var sb strings.Builder + for current := l.head; current != nil; current = current.Next { + fmt.Fprintf(&sb, "%v -> ", current.Data) + } + sb.WriteString("nil") + return sb.String() +} diff --git a/go/algorithms/main.go b/go/algorithms/main.go new file mode 100644 index 0000000..ed8a4a5 --- /dev/null +++ b/go/algorithms/main.go @@ -0,0 +1,29 @@ +package main + +import ( + "fmt" + + "lab/algorithms/linkedlist" +) + +func main() { + var list linkedlist.LinkedList[int] // 零值即为空表 + list.InsertTail(3) + list.InsertTail(5) + list.InsertHead(1) + list.InsertHead(0) + fmt.Println(&list) // 0 -> 1 -> 3 -> 5 -> nil + + list.DeleteHead() + list.DeleteTail() + fmt.Println(&list) // 1 -> 3 -> nil + + found := list.Search(3) + if found != nil { + fmt.Printf("找到节点,Data = %d\n", found.Data) + } + + list.Reverse() + fmt.Println(&list) // 3 -> 1 -> nil + fmt.Printf("链表长度: %d\n", list.Len()) +} diff --git a/go/basics/goroutine/leak/main.go b/go/basics/goroutine/leak/main.go new file mode 100644 index 0000000..5e02394 --- /dev/null +++ b/go/basics/goroutine/leak/main.go @@ -0,0 +1,50 @@ +package main + +import ( + "context" + "fmt" + "math/rand" + "net/http" + _ "net/http/pprof" + "time" +) + +// 来自文章 https://www.cnblogs.com/hwnd/p/20682814 +func simulateRequest() { + timeout := 500 * time.Millisecond + ctx, cancel := context.WithDeadline(context.Background(), time.Now().Add(timeout)) + defer cancel() + resultCh := make(chan string) // 无缓冲! + + // 启动 worker goroutine 处理"请求" + go func() { + time.Sleep(time.Duration(rand.Intn(500)+300) * time.Millisecond) + select { + case <-time.After(timeout): + fmt.Println("worker 超时,放弃处理") + case <-ctx.Done(): + case resultCh <- fmt.Sprintf("处理完成,耗时 %dms", rand.Intn(500)+300): + + } + }() + + // TODO: 模拟请求取消或超时的情况(比如 500ms 后调用方不再关心结果) + // 如果 worker 还没完成,它就 leak 了 + select { + case resultCh := <-resultCh: + fmt.Println(resultCh) + case <-ctx.Done(): + fmt.Println("请求方取消了请求") + } +} + +func main() { + for i := 0; i < 5; i++ { + simulateRequest() + fmt.Printf("第 %d 个请求处理完毕\n\n", i+1) + time.Sleep(200 * time.Millisecond) + } + fmt.Println("所有请求处理完毕") + // 运行后观察:goroutine 数量只增不减 + http.ListenAndServe(":6060", nil) +} diff --git a/go/basics/MemoryEscape/main.go b/go/basics/memoryEscape/main.go similarity index 100% rename from go/basics/MemoryEscape/main.go rename to go/basics/memoryEscape/main.go diff --git a/go/basics/pprof/main.go b/go/basics/pprof/main.go new file mode 100644 index 0000000..66eac67 --- /dev/null +++ b/go/basics/pprof/main.go @@ -0,0 +1,56 @@ +package main + +import ( + "log" + "net/http" + _ "net/http/pprof" // 注册 pprof 相关 handler + "time" +) + +// memoryBlocks 保留一部分内存块,方便内存剖析观察。 +// 这个全局变量会一直存活,便于分析堆上的对象。 +var memoryBlocks [64][]byte + +func main() { + // 启动 pprof HTTP 服务 + go func() { + log.Println("pprof server started at http://localhost:6060/debug/pprof/") + if err := http.ListenAndServe("localhost:6060", nil); err != nil { + log.Fatal(err) + } + }() + + // 模拟 CPU 密集型业务 + go cpuIntensiveTask() + + // 模拟内存分配型业务 + go memoryIntensiveTask() + + // 阻塞主协程,让程序持续运行 + select {} +} + +// fib 递归计算斐波那契数列,非常耗费 CPU +func fib(n int) int { + if n <= 1 { + return n + } + return fib(n-1) + fib(n-2) +} + +// cpuIntensiveTask 循环执行 CPU 密集型计算 +func cpuIntensiveTask() { + for { + _ = fib(30) + } +} + +// memoryIntensiveTask 循环分配内存并保留在全局切片中 +func memoryIntensiveTask() { + for i := 0; ; i++ { + idx := i % len(memoryBlocks) + // 每次分配 1MB,替换掉旧的内存块 + memoryBlocks[idx] = make([]byte, 1<<20) // 1 << 20 = 1MB + time.Sleep(100 * time.Millisecond) + } +} \ No newline at end of file diff --git a/go/basics/session/main.go b/go/basics/session/main.go new file mode 100644 index 0000000..21c0bee --- /dev/null +++ b/go/basics/session/main.go @@ -0,0 +1,116 @@ +package main + +import ( + "net/http" + "time" + + "github.com/gin-contrib/sessions" + "github.com/gin-contrib/sessions/cookie" + "github.com/gin-gonic/gin" +) + +var ( + mockUsers = map[string]string{ + "zhangsan": "123456", + "lisi": "654321", + } + // secretKey = "your-secret-key" + hmacSecretKey = "5KiinhGcvoXFmgOttvgMWd9ugadE0KVP" + blockSecretKey = "OLZmfTWHGyjCfJoH1Y8SjbSY" +) + +const sessionTTL = 60 * 5 + +func sessionOptions(maxAge int) sessions.Options { + return sessions.Options{ + Path: "/", + Domain: "", // 设置为 "" 表示当前域名 + MaxAge: maxAge, // 单位为秒。0 表示不写Max-Age(浏览器会话cookie); -1 表示删除 cookie + Secure: false, // 生产走 HTTPS 时改 true + HttpOnly: true, // true 表示禁止 JS 读取 cookie + SameSite: http.SameSiteStrictMode, // 严格模式,仅在同源请求中发送 cookie + } +} + +// 代码来自文章 https://www.cnblogs.com/XY-Heruo/p/22864951 +func main() { + r := gin.Default() + + // Create cookie-based session store with a secret key + // 传单个密钥时, 只做签名认证,不加密cookie内容. 认证密钥建议 32 或 64 字节 + // 传一对密钥时,第一个用于认证(HMAC),第二个用于加密(AES-128, AES-192, AES-256)。加密密钥必须是16、24或32字节长度,对应 AES-128/192/256。 + // 多对密钥用于密钥轮换,第一对用于当前加密和认证,后续的用于解密和认证旧的cookie。Example: cookie.NewStore([]byte("newAuth"), []byte("newEnc"), []byte("oldAuth"), []byte("oldEnc")) + store := cookie.NewStore([]byte(hmacSecretKey), []byte(blockSecretKey)) + store.Options(sessionOptions(sessionTTL)) // 全局设置 cookie 过期时间为 5 分钟 + r.Use(sessions.Sessions("mysession", store)) + + r.POST("/login", login) + + auth := r.Group("/", authRequired()) + + auth.GET("/profile", profile) + auth.POST("/logout", logout) + + r.Run(":8080") +} + +func login(c *gin.Context) { + session := sessions.Default(c) + username := c.PostForm("username") + password := c.PostForm("password") + + if username == "" || password == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "username or password is empty"}) + return + } + + if mockUsers[username] != password { + c.JSON(http.StatusUnauthorized, gin.H{"error": "invalid username or password"}) + return + } + + session.Set("user", username) + session.Set("expire_at", time.Now().Add(time.Second*sessionTTL).Unix()) // 设置过期时间 + if err := session.Save(); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "save session failed"}) + return + } + + c.JSON(http.StatusOK, gin.H{"message": "logged in"}) +} + +func logout(c *gin.Context) { + session := sessions.Default(c) + session.Clear() + session.Options(sessionOptions(-1)) // 让浏览器删除 cookie + if err := session.Save(); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "save session failed"}) + return + } + c.JSON(http.StatusOK, gin.H{"message": "logged out"}) +} + +func profile(c *gin.Context) { + session := sessions.Default(c) + user := session.Get("user").(string) + c.JSON(http.StatusOK, gin.H{"user": user}) +} + +func authRequired() gin.HandlerFunc { + return func(c *gin.Context) { + session := sessions.Default(c) + if session.Get("user") == nil { + c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "unauthorized"}) + return + } + + if expireAt, ok := session.Get("expire_at").(int64); !ok || time.Now().Unix() > expireAt { + session.Clear() + session.Options(sessionOptions(-1)) + session.Save() + c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"error": "expired session"}) + return + } + c.Next() + } +} diff --git a/go/go.mod b/go/go.mod index da5e791..f44fce0 100644 --- a/go/go.mod +++ b/go/go.mod @@ -1,15 +1,43 @@ module lab -go 1.26.2 +go 1.27.0 require ( - github.com/richardlehane/mscfb v1.0.7 // indirect - github.com/richardlehane/msoleps v1.0.6 // indirect - github.com/tiendc/go-deepcopy v1.7.2 // indirect - github.com/xuri/efp v0.0.1 // indirect - github.com/xuri/excelize/v2 v2.11.0 // indirect - github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9 // indirect - golang.org/x/crypto v0.53.0 // indirect - golang.org/x/net v0.56.0 // indirect - golang.org/x/text v0.38.0 // indirect + github.com/gin-contrib/sessions v1.1.2 + github.com/gin-gonic/gin v1.12.0 + golang.org/x/crypto v0.56.0 +) + +require ( + github.com/bytedance/gopkg v0.1.4 // indirect + github.com/bytedance/sonic v1.15.2 // indirect + github.com/bytedance/sonic/loader v0.5.1 // indirect + github.com/cloudwego/base64x v0.1.7 // indirect + github.com/gabriel-vasile/mimetype v1.4.13 // indirect + github.com/gin-contrib/sse v1.1.1 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.30.3 // indirect + github.com/goccy/go-json v0.10.6 // indirect + github.com/goccy/go-yaml v1.19.2 // indirect + github.com/gorilla/context v1.1.2 // indirect + github.com/gorilla/securecookie v1.1.2 // indirect + github.com/gorilla/sessions v1.4.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.4.0 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/mattn/go-isatty v0.0.23 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pelletier/go-toml/v2 v2.4.3 // indirect + github.com/quic-go/qpack v0.6.0 // indirect + github.com/quic-go/quic-go v0.60.0 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.3.1 // indirect + go.mongodb.org/mongo-driver/v2 v2.8.0 // indirect + golang.org/x/arch v0.29.0 // indirect + golang.org/x/net v0.57.0 // indirect + golang.org/x/sys v0.47.0 // indirect + golang.org/x/text v0.41.0 // indirect + google.golang.org/protobuf v1.36.11 // indirect ) diff --git a/go/go.sum b/go/go.sum index 44cf10f..c84f951 100644 --- a/go/go.sum +++ b/go/go.sum @@ -1,18 +1,100 @@ -github.com/richardlehane/mscfb v1.0.7 h1:oeoiM0WE79vHwE8RpIYYvIAc8ajTH2mb6UZm55/+EB0= -github.com/richardlehane/mscfb v1.0.7/go.mod h1:pe0+IUIc0AHh0+teNzBlJCtSyZdFOGgV4ZK9bsoV+Jo= -github.com/richardlehane/msoleps v1.0.6 h1:9BvkpjvD+iUBalUY4esMwv6uBkfOip/Lzvd93jvR9gg= -github.com/richardlehane/msoleps v1.0.6/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg= -github.com/tiendc/go-deepcopy v1.7.2 h1:Ut2yYR7W9tWjTQitganoIue4UGxZwCcJy3orjrrIj44= -github.com/tiendc/go-deepcopy v1.7.2/go.mod h1:4bKjNC2r7boYOkD2IOuZpYjmlDdzjbpTRyCx+goBCJQ= -github.com/xuri/efp v0.0.1 h1:fws5Rv3myXyYni8uwj2qKjVaRP30PdjeYe2Y6FDsCL8= -github.com/xuri/efp v0.0.1/go.mod h1:ybY/Jr0T0GTCnYjKqmdwxyxn2BQf2RcQIIvex5QldPI= -github.com/xuri/excelize/v2 v2.11.0 h1:HxaEFl6sRN2+8J5a8HaKq+0M4FsjBGMnWWtjOCPSG88= -github.com/xuri/excelize/v2 v2.11.0/go.mod h1:jxFLbzaIwGQ5ufFNvYfUOHqXhfPaNmP14KWfmNz2Uak= -github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9 h1:+C0TIdyyYmzadGaL/HBLbf3WdLgC29pgyhTjAT/0nuE= -github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9/go.mod h1:WwHg+CVyzlv/TX9xqBFXEZAuxOPxn2k1GNHwG41IIUQ= -golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto= -golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio= -golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o= -golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec= -golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE= -golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4= +github.com/bytedance/gopkg v0.1.4 h1:oZnQwnX82KAIWb7033bEwtxvTqXcYMxDBaQxo5JJHWM= +github.com/bytedance/gopkg v0.1.4/go.mod h1:v1zWfPm21Fb+OsyXN2VAHdL6TBb2L88anLQgdyje6R4= +github.com/bytedance/sonic v1.15.2 h1:90H+rcF/FwLXwfB1cudOLq/je83n683Utf4Cbp0xHCo= +github.com/bytedance/sonic v1.15.2/go.mod h1:mT2NbXunuaEbnZ+mRIX/vYqKISmgEuHFDI4UzmKx2SA= +github.com/bytedance/sonic/loader v0.5.1 h1:Ygpfa9zwRCCKSlrp5bBP/b/Xzc3VxsAW+5NIYXrOOpI= +github.com/bytedance/sonic/loader v0.5.1/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo= +github.com/cloudwego/base64x v0.1.7 h1:NppS+Fgzg5ovhn4NkUXaDT3x9jldgH5ToMCqzBSi2zI= +github.com/cloudwego/base64x v0.1.7/go.mod h1:Cu1PV9zfrSf7ET2tIbWbbEy7jO7HHJ13q4X2SQ8aWYg= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM= +github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= +github.com/gin-contrib/sessions v1.1.2 h1:utWRqOHT/kbqxxqF/mtutV9wfGLi1+uu5YuvWUhj/Ow= +github.com/gin-contrib/sessions v1.1.2/go.mod h1:qRxYwA/TYJw0wv28oFzs4gY/8G8z69id862SNgX9Gmw= +github.com/gin-contrib/sse v1.1.1 h1:uGYpNwTacv5R68bSGMapo62iLTRa9l5zxGCps4hK6ko= +github.com/gin-contrib/sse v1.1.1/go.mod h1:QXzuVkA0YO7o/gun03UI1Q+FTI8ZV/n5t03kIQAI89s= +github.com/gin-gonic/gin v1.12.0 h1:b3YAbrZtnf8N//yjKeU2+MQsh2mY5htkZidOM7O0wG8= +github.com/gin-gonic/gin v1.12.0/go.mod h1:VxccKfsSllpKshkBWgVgRniFFAzFb9csfngsqANjnLc= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.30.3 h1:4MU6YkEwx7GbcPJOZxrtbu+QfF3pJLJuaYTeAH0DYy8= +github.com/go-playground/validator/v10 v10.30.3/go.mod h1:4Axh7oCNGcoGkqLoE4YWt6n20mcEIsPRlB7vPk3lpyc= +github.com/goccy/go-json v0.10.6 h1:p8HrPJzOakx/mn/bQtjgNjdTcN+/S6FcG2CTtQOrHVU= +github.com/goccy/go-json v0.10.6/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM= +github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/gorilla/context v1.1.2 h1:WRkNAv2uoa03QNIc1A6u4O7DAGMUVoopZhkiXWA2V1o= +github.com/gorilla/context v1.1.2/go.mod h1:KDPwT9i/MeWHiLl90fuTgrt4/wPcv75vFAZLaOOcbxM= +github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= +github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= +github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ= +github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/klauspost/cpuid/v2 v2.4.0 h1:S6Hrbc7+ywsr0r+RLapfGBHfyefhCTwEh3A0tV913Dw= +github.com/klauspost/cpuid/v2 v2.4.0/go.mod h1:19jmZ9mjzoF//ddRSUsv0zfBTJWh3QJh9FNxZTMrGxU= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/mattn/go-isatty v0.0.23 h1:cYwCQTQf3HB6xUC+BtyCLZNr7IzbOmoZbmssVNzSyiQ= +github.com/mattn/go-isatty v0.0.23/go.mod h1:nMCL3Zebbrt45jsMDgnfIwz6ydEQApk5oEI3HqDio6A= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/pelletier/go-toml/v2 v2.4.3 h1:GTRvJQutkOSftxIFD5xw9aepkYNuPWmVJpffdDPYVpY= +github.com/pelletier/go-toml/v2 v2.4.3/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/quic-go/go-ossfuzz-seeds v0.1.0 h1:APacT+iIaNF6fd8AGEiN3bT/Jtkd2jz4v4TzM7MFjy0= +github.com/quic-go/go-ossfuzz-seeds v0.1.0/go.mod h1:3IOHRbJIc+L6YKMwfDtJAM9Vj9k0YY4muhuyUYk5tbk= +github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= +github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= +github.com/quic-go/quic-go v0.60.0 h1:xcQioE8OM66UQLeUMHltK1CCcOu3JbVB4JAQdDQSB+0= +github.com/quic-go/quic-go v0.60.0/go.mod h1:wpKpjmPpftl30sL6pFh7REVpjbcCVy4zt2vDyK1TuJk= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY= +github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= +go.mongodb.org/mongo-driver/v2 v2.8.0 h1:CxWDGQYY8QQwNjAl/aq2sfWakdnWZynnqJ9F4DhHbP8= +go.mongodb.org/mongo-driver/v2 v2.8.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0= +go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= +go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= +golang.org/x/arch v0.29.0 h1:8sSET5wB0+exBm0FGmOtdHMqjlRdV2DRD3/IV6OZgho= +golang.org/x/arch v0.29.0/go.mod h1:0X+GdSIP+kL5wPmpK7sdkEVTt2XoYP0cSjQSbZBwOi8= +golang.org/x/crypto v0.56.0 h1:GUh5Ii4J5jtcseSMiRqr1jXCNHoxjeV9Fmekc2oLy6Y= +golang.org/x/crypto v0.56.0/go.mod h1:OMW5y6CY9l38uPLmxU6l6pwcXp1obtLo3e6gT7gQR2I= +golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE= +golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU= +golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= +golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8= +golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/go/testing/crypto/bcrypt_test.go b/go/testing/crypto/bcrypt_test.go new file mode 100644 index 0000000..cd671c7 --- /dev/null +++ b/go/testing/crypto/bcrypt_test.go @@ -0,0 +1,149 @@ +package crypto + +import ( + "errors" + "testing" + "time" + + "golang.org/x/crypto/bcrypt" +) + +var ( + ErrInvalidPassword = errors.New("invalid password") + ErrInvalidHash = errors.New("invalid password hash") +) + +type Hasher struct { + cost int // cost 可以理解成 bcrypt 的“计算难度”参数,cost 越高,计算 bcrypt hash 就越慢,攻击者暴力破解密码也越贵 +} + +func NewHasher(cost int) (*Hasher, error) { + if cost < bcrypt.MinCost || cost > bcrypt.MaxCost { + return nil, errors.New("invalid bcrypt cost") + } + + return &Hasher{ + cost: cost, + }, nil +} + +// Hash 用于注册、重置密码、修改密码。 +func (h *Hasher) Hash(password string) (string, error) { + if len(password) == 0 { + return "", ErrInvalidPassword + } + + // bcrypt 只使用密码前 72 bytes。 + // 生产环境建议显式拒绝,而不是静默截断。 + if len([]byte(password)) > 72 { + return "", ErrInvalidPassword + } + + hash, err := bcrypt.GenerateFromPassword( + []byte(password), + h.cost, + ) + if err != nil { + return "", err + } + + return string(hash), nil +} + +// Verify 用于登录时验证密码。 +func (h *Hasher) Verify(password, encodedHash string) (bool, error) { + if len(password) == 0 || len([]byte(password)) > 72 { + return false, ErrInvalidPassword + } + + if encodedHash == "" { + return false, ErrInvalidHash + } + + err := bcrypt.CompareHashAndPassword( + []byte(encodedHash), + []byte(password), + ) + + switch { + case err == nil: + return true, nil + + case errors.Is(err, bcrypt.ErrMismatchedHashAndPassword): + return false, nil + + default: + // 例如数据库里的 hash 已损坏或格式不合法。 + return false, ErrInvalidHash + } +} + +// NeedsRehash 判断数据库中的 hash 是否使用了较旧的 cost。 +func (h *Hasher) NeedsRehash(encodedHash string) bool { + cost, err := bcrypt.Cost([]byte(encodedHash)) + if err != nil { + return true + } + + return cost != h.cost +} + +func TestBcrypt(t *testing.T) { + oldHasher, err := NewHasher(10) + if err != nil { + t.Fatal(err) + } + newHasher, err := NewHasher(12) + if err != nil { + t.Fatal(err) + } + password := "password" + oldHash, err := oldHasher.Hash(password) + if err != nil { + t.Fatal(err) + } + t.Log(oldHash) + newHash, err := newHasher.Hash(password) + if err != nil { + t.Fatal(err) + } + t.Log(newHash) + err = bcrypt.CompareHashAndPassword([]byte(oldHash), []byte(password)) + if err != nil { + t.Fatal(err) + } + +} + +func TestHashPassword(t *testing.T) { + hasher, err := NewHasher(12) + if err != nil { + t.Fatal(err) + } + password := "password" + hash, err := hasher.Hash(password) + if err != nil { + t.Fatal(err) + } + t.Log(hash) + if ok, err := hasher.Verify(password, hash); err != nil || !ok { + t.Fatal("verify failed") + } + if ok := hasher.NeedsRehash(hash); !ok { + t.Fatal("needs rehash failed") + } + +} + +func TestGenerateTime(t *testing.T) { + start := time.Now() + + _, err := bcrypt.GenerateFromPassword( + []byte("TestPassword123!"), + 14, + ) + if err != nil { + t.Fatal(err) + } + t.Log(time.Since(start), err) +} diff --git a/go/testing/json/json_test.go b/go/testing/json/json_test.go new file mode 100644 index 0000000..edba1ac --- /dev/null +++ b/go/testing/json/json_test.go @@ -0,0 +1,68 @@ +package main + +import ( + "fmt" + "testing" + "time" + + // 引入两个包进行对比 + jsonV1 "encoding/json" + jsonV2 "encoding/json/v2" +) + +func TestV2Null(t *testing.T) { + type Product struct { + Name string `json:"name"` + Tags []string `json:"tags"` + } + p := Product{ + Name: "product1", + } + data1, _ := jsonV1.Marshal(p) + data2, _ := jsonV2.Marshal(p) + t.Log(string(data1)) + t.Log(string(data2)) +} + +func TestOmitZeroVsEmpty(t *testing.T) { + // ===== V1 结构体:使用 omitempty ===== + type EventV1 struct { + Title string `json:"title"` + CreatedAt time.Time `json:"created_at,omitempty"` // ⚠️ omitempty 对 time.Time 无效 + } + + // ===== V2 结构体:使用 omitzero ===== + type EventV2 struct { + Title string `json:"title"` + CreatedAt time.Time `json:"created_at,omitzero"` // ✅ omitzero 能识别 IsZero() + } + // 都不给 CreatedAt 赋值,保持 Go 的零值 time.Time{} + + // 1. 测试 v1 + omitempty + e1 := EventV1{Title: "Go Meetup"} + data1, _ := jsonV1.Marshal(e1) + fmt.Println("v1 + omitempty 输出:", string(data1)) + + // 2. 测试 v2 + omitzero + e2 := EventV2{Title: "Go Meetup"} + data2, _ := jsonV2.Marshal(e2) + fmt.Println("v2 + omitzero 输出:", string(data2)) +} + +func TestMultiStyle(t *testing.T) { + type MultiStyleUser struct { + // 1. 严格匹配:JSON key 必须精确是 "user_id" + ID string `json:"user_id,case:strict"` + + // 2. 忽略大小写:JSON key 是 "Name"、"NAME"、"name" 都能成功赋值 + Name string `json:"name,case:ignore"` + } + // 假设第三方传来的 JSON 风格混杂 + jsonData := []byte(`{ + "user_id": "1001", + "NAME": "Gopher" +}`) + var m MultiStyleUser + jsonV2.Unmarshal(jsonData, &m) + t.Log(m) +} diff --git a/go/testing/normal_test.go b/go/testing/normal_test.go index 345e618..7d8e35d 100644 --- a/go/testing/normal_test.go +++ b/go/testing/normal_test.go @@ -1,6 +1,7 @@ package testing import ( + "fmt" "sort" "testing" "time" @@ -46,3 +47,63 @@ func TestChannel(t *testing.T) { var c chan bool // 一个零值nil通道 <-c // 永久阻塞在此 } + +func TestFeature1(t *testing.T) { + type Person struct { + Sex string + Name string + } + type Student struct { + StudentID int + Person + } + + s1 := Student{ + StudentID: 1, + Sex: "male", + Name: "张三", + } + t.Log(s1) +} + +func TestFeature2(t *testing.T) { + // Go 1.27 之前:必须手动写出类型实参 [int],否则编译报错 + fn1 := IntToString(FormatToString[int]) + fn2 := IntToString(FormatToString) + + oldFormat := []IntToString{FormatToString[int]} + newFormat := []IntToString{FormatToString} + + t.Log(fn1(1)) + t.Log(fn2(2)) + for i, fn := range oldFormat { + t.Log(fn(i)) + } + for i, fn := range newFormat { + t.Log(fn(i)) + } + +} + +// 一个泛型格式化函数 +func FormatToString[T any](v T) string { + return fmt.Sprintf("value: %v", v) +} + +// 具名函数类型:接收一个 int,返回 string +type IntToString func(int) string + +func TestSort(t *testing.T) { + slice := []int{8, 2, 4, 4} + Sort1(slice) + t.Log(slice) +} +func Sort1(slice []int) { + for i := 0; i < len(slice)-1; i++ { + for j := 0; j < len(slice)-1-i; j++ { + if slice[j] > slice[j+1] { + slice[j], slice[j+1] = slice[j+1], slice[j] + } + } + } +} diff --git a/note/资源/数据库/SQL/mysql的字符集坑.md b/note/资源/数据库/SQL/避坑/MySQL的字符集坑.md similarity index 100% rename from note/资源/数据库/SQL/mysql的字符集坑.md rename to note/资源/数据库/SQL/避坑/MySQL的字符集坑.md diff --git a/note/随手记/MySQL空表JOIN导致排序索引失效-实验笔记.md b/note/资源/数据库/SQL/避坑/MySQL空表JOIN导致排序索引失效-实验笔记.md similarity index 100% rename from note/随手记/MySQL空表JOIN导致排序索引失效-实验笔记.md rename to note/资源/数据库/SQL/避坑/MySQL空表JOIN导致排序索引失效-实验笔记.md diff --git a/note/资源/编程/Golang/标准库.md b/note/资源/编程/Golang/标准库.md index b800a35..92bfd95 100644 --- a/note/资源/编程/Golang/标准库.md +++ b/note/资源/编程/Golang/标准库.md @@ -1,4 +1,6 @@ -## `errors.AsType` +## `errors` + +### `errors.AsType` > *用泛型把* `errors.As` *的“先声明变量再传指针”简化为“直接返回* `(E, bool)`*”,是* `errors.As` *的语法糖升级* @@ -20,3 +22,6 @@ if mysqlErr, ok := errors.AsType[*mysql.MySQLError](err); ok { } ``` + + +## `net/http/pprof` diff --git a/note/资源/编程/Golang/版本变化/1.27.md b/note/资源/编程/Golang/版本变化/1.27.md new file mode 100644 index 0000000..d3bb54e --- /dev/null +++ b/note/资源/编程/Golang/版本变化/1.27.md @@ -0,0 +1,294 @@ +## 1.27版本变化 + +### 支持泛型方法 + +Go 1.27 虽然开放了泛型方法,但有明确的限制: + +1.接口定义中不允许包含泛型方法。 + +2.拥有泛型方法的方法,不能用于实现接口。 + +```go + // 普通结构体,本身没有任何泛型参数 + type Serializer struct{} + + // T 属于方法 Serialize,每个调用可以独立拥有不同的 T + func (s *Serializer) Serialize[T any](data T) string { + return fmt.Sprintf("%v", data) + } + + // 使用: + sz := Serializer{} + + // 同一个 sz 实例,每次调用都可以处理完全不同的类型! + str1 := sz.Serialize("hello") // 自动推导 T = string + str2 := sz.Serialize(123) // 自动推导 T = int + str3 := sz.Serialize(true) // 自动推导 T = bool +``` + +### 结构体字面量:字段选择器当 key + +嵌套或匿名结构体中的字段,现在可以直接初始化,不必再一层层嵌套字面量 + +```go + type Person struct { + Sex string + Name string + } + type Student struct { + StudentID int + Person + } + +// Go 1.27 之前:必须显式嵌套 Person + s1 := Student{ + StudentID: 1, + Person: Person{ + Sex: "male", + Name: "张三", + }, + } +// Go 1.27:嵌套结构体Person的字段可以直接当 key 用 + s1 := Student{ + StudentID: 1, + Sex: "male", + Name: "张三", + } +``` + +### 函数类型推断:从“手动挡”到“自动挡” + +```go + // Go 1.27 之前:必须手动写出类型实参 [int],否则编译报错 + fn := IntToString(FormatToString[int]) + formatter:= []IntToString{FormatToString[int]} + + // Go 1.27 后:自动推导,不用手写 + fn := IntToString(FormatToString) + formatter := []IntToString{FormatToString} +``` + +### 标准库更新:`encoding/json/v2` + +> Go 1.27 标准库中的 v1 `encoding/json` 内部已经被改用 v2 引擎驱动,保留了 v1 的原始 API 签名和行为 + +新旧版本主要行为对比 + +| **场景** | **encoding/json (v1)** | **encoding/json/v2** | +| ------------------------------- | -------------------------- | -------------------------------------------- | +| **`nil` 切片/Map 序列化** | 转为 `null` | 转为 `[]` 和 `{}` *(符合语义,防前端空指针)* | +| **重复键处理 (Duplicate Keys)** | 允许,后面的覆盖前面的 | **直接报错** *(增强安全性,防 JSON 注入)* | +| **非法 UTF-8 字符** | 格式化替换,静默忽略 | **直接报错** *(要求严格符合规范)* | +| **字段名匹配 (大小写敏感)** | 默认**忽略**大小写匹配 | 默认**严格匹配**大小写 *(提升解析性能)* | +| **固定长度数组解包** | 长度不一致时自动截断或补零 | 必须与 JSON 数组长度严格相等,否则报错 | + +支持可选项 + +| Options | 作用 | +| -------------------------------------- | ----------------------- | +| `json.FormatNilSliceAsNull(true)` | 允许使用 v1 的 nil 行为 | +| `json.MatchCaseInsensitiveNames(true)` | 允许忽略大小写 | + +新增标签`omitzero` + +| **维度** | **omitempty** | **omitzero** | +| ----------------------- | ------------------------------------------------------------ | ------------------------------------------------------------ | +| **判断标准** | **是否为“空 (Empty)”** (如:空字符串 `""`、长度为 0 的切片/Map、`false`、`0`、指针 `nil`) | **是否为“ Go 类型的零值 (Zero Value)”** (满足 `reflect.Value.IsZero()` 或实现 `IsZero() bool` 接口) | +| **结构体/时间类型处理** | **有重大缺陷**,对非指针的结构体(如 `time.Time`)无效!依然会被序列化。 | **完美支持** 未初始化的 `time.Time{}` 或任何嵌套结构体零值均会被正确跳过。 | +| **自定义类型零值** | 不支持自定义零值判断逻辑。 | 支持只要实现 `IsZero() bool` 方法即可自定义是否跳过。 | + + + +## 官方扩展库 `bcrypt` + +### `bcrypt` 是什么 + +`bcrypt` 是专门用于**密码哈希**的算法,不是可逆加密。 + +```text +password +↓ +bcrypt +↓ +hash +``` + +#### 核心原则 + +```text +数据库只存 hash +bcrypt 自动处理 salt +使用 CompareHashAndPassword 验证 +cost 根据服务器性能决定 +不要自己用 MD5 / SHA256 存密码 +``` + +------ + +#### cost + +`cost` 是 bcrypt 的计算难度。 + +生产环境通常可以从 `10 ~ 12` 开始测试,具体值应根据服务器性能决定。单次 bcrypt 计算控制在大约 `100ms ~ 300ms` 是比较常见的工程范围。 + +`cost` 每增加 1,计算量大约接近翻倍。 + +cost 越高: + +- 计算越慢 +- 暴力破解成本越高 +- 服务器 CPU 消耗也越高 + +bcrypt 生成的 hash 本身会记录当时使用的 `cost`。 + +因此,即使生产环境将当前配置从: + +```text +cost = 10 +``` + +升级为: + +```text +cost = 12 +``` + +数据库中原本使用 `cost = 10` 生成的旧 hash,仍然可以通过: + +```go +bcrypt.CompareHashAndPassword(...) +``` + +正常验证。 + +`CompareHashAndPassword` 会根据已有 hash 中记录的信息进行密码验证,不依赖当前系统配置的 cost。 + +登录成功后,可以通过 `bcrypt.Cost()` 获取旧 hash 使用的 cost。如果低于当前配置,可以重新生成 hash 并更新数据库。 + +------ + +#### 72-byte 限制 + +bcrypt 密码输入最多支持约 `72 bytes`。 + +如果只允许 ASCII: + +```text +72 bytes ≈ 72 个字符 +``` + +通常已经足够。 + +建议业务层主动限制: + +```go +if len([]byte(password)) > 72 { + return errors.New("password too long") +} +``` + +------ + +### 基础使用 + +#### 依赖导入 + +```go +import "golang.org/x/crypto/bcrypt" +``` + +`golang.org/x/crypto/bcrypt` 属于 Go 官方维护的扩展库,但不属于 Go 标准库。 + +------ + +#### 生成 hash + +```go +hash, err := bcrypt.GenerateFromPassword( + []byte(password), + 12, +) +``` + +生成的 hash 类似: + +```text +$2a$12$xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx +``` + +其中大致包含: + +```text +$2a$ → bcrypt 版本 +12$ → cost +后续内容 → 编码后的 salt + hash +``` + +bcrypt 会自动生成随机 salt,因此不需要自己生成或额外保存 salt。 + +------ + +#### 验证密码 + +```go +err := bcrypt.CompareHashAndPassword( + []byte(passwordHash), + []byte(password), +) + +if err == nil { + // 密码正确 +} +``` + +------ + +#### 判断密码是否相同 + +不能直接比较两个 bcrypt hash,因为 bcrypt 每次都会生成新的随机 salt。 + +即使密码完全相同: + +```text +hash1 != hash2 +``` + +判断某个密码是否和旧密码相同时,应使用: + +```go +err := bcrypt.CompareHashAndPassword( + []byte(oldHash), + []byte(password), +) + +if err == nil { + // password 与 oldHash 对应的密码相同 +} +``` + +------ + +#### 获取 hash 的 cost + +```go +cost, err := bcrypt.Cost( + []byte(user.PasswordHash), +) +``` + +通常用于**用户登录成功后**,判断当前密码 hash 是否使用了旧的 cost。 + +例如: + +```text +数据库 hash cost = 10 +当前系统 cost = 12 +↓ +登录成功 +↓ +重新生成 cost = 12 的 hash +↓ +更新数据库 +``` + +这样可以在用户正常登录过程中逐步升级旧密码 hash。 \ No newline at end of file diff --git a/note/资源/编程/避坑/squirrel的坑.md b/note/资源/编程/避坑/squirrel的坑.md new file mode 100644 index 0000000..2edd639 --- /dev/null +++ b/note/资源/编程/避坑/squirrel的坑.md @@ -0,0 +1,45 @@ +# squirrel 的坑 + +> `github.com/Masterminds/squirrel`,纯 SQL 查询构建器(query builder),2026-09 查证。 + +## 坑 1:Like 家族多 key 不排序,SQL 输出不确定(真雷) + +`Eq` / `NotEq` / `Lt` / `Gt` 等 map 条件生成 SQL 前会对 key 做 `sort.Strings` 字典序排序(v1.1.0 起,`expr.go` 的 `getSortedKeys`),输出确定。 + +但 `Like` / `NotLike` / `ILike` / `NotILike` 的 `toSql` 是直接 `for key, val := range lk`——跟随 Go map 随机遍历序,**多 key 时每次生成的 SQL 条件顺序随机**: + +``` +第1次: WHERE nickname LIKE ? AND mobile LIKE ? | args=[%张% %138%] +第3次: WHERE mobile LIKE ? AND nickname LIKE ? | args=[%138% %张%] ← 顺序翻转 +``` + +### 后果 + +- 查询结果没错(AND 交换律),但 **SQL 字符串 + args 顺序在变** +- 如果用「SQL 字符串 + 参数」生成缓存 key(hash 后当 Redis key 之类),同一查询会打出多个 key: + - 命中率暴跌:n 个 Like 条件最多 n! 种排列 → n! 个缓存 key + - 同一份数据存多份副本,TTL 内占内存 +- 依赖 SQL diff 的单测会随机失败 + +### 规避 + +- `Like` 只放**单个 key** +- 多模糊条件拆成多次 `.Where(...)`(内部是 append 切片,天然保序)或用 `sq.And{...}` 显式控制顺序 + +### 上游状态 + +- 无任何 issue 报告过此事(全量搜索过,含 closed),作者从未回应 +- 当年修 Eq 排序的 PR #141 只改了 Eq/Lt 两处,紧邻的 Like 一行未动,讨论里也没人发现——纯遗漏,非有意设计(作者在 #135 明确说过支持排序) +- 库近乎停滞:最新 release v1.5.4(2023-03),master 最后 commit 2024-02,**别指望上游修** + +## 坑 2:Eq 排序按字典序,与联合索引列序无关(知道即可) + +`Eq{"b":2, "a":1}` 永远生成 `WHERE a = ? AND b = ?`,与你书写的顺序无关(issue #321)。 + +对主流数据库**基本无影响**:MySQL / PostgreSQL / SQL Server / Oracle 的 CBO 都会重排谓词,索引选择看的是「条件覆盖了哪些列(最左前缀)、选择度、类型是否隐式转换」,不是 WHERE 里的书写顺序。只有对谓词顺序敏感的引擎/老优化器才会踩到。 + +## 顺带:这个库的亮点 + +- builder 是**值语义不可变**的(基于 lann/builder 持久化数据结构),每次链式调用返回新副本,可安全分叉复用,不像 GORM 那样共享 Statement、分叉前要 `Session()` 克隆 +- `Eq` 家族的确定性输出有源码保证(`expr.go` 排序逻辑),可放心用于缓存 key 场景 +- 子查询:`FromSelect(builder, alias)`,或任何 builder 作为 `Sqlizer` 嵌进 `Where/Having/Join` diff --git a/note/随手记/casbin学习笔记.md b/note/随手记/casbin学习笔记.md new file mode 100644 index 0000000..63237f7 --- /dev/null +++ b/note/随手记/casbin学习笔记.md @@ -0,0 +1,61 @@ +# Casbin 学习笔记 + +> 版本基线:casbin **v3.11.0**;Go import 路径 `github.com/casbin/casbin/v3`(源码仓库 `apache/casbin`)。本文是概念速记,不是教程。 +> Casbin 负责**授权**(能不能做某事),不负责**身份认证**(你是谁)。 + +## 1. 心智模型:PERM + +| 段 | 作用 | +|---|---| +| `[request_definition]` `r = sub, obj, act` | 一次访问请求长什么样 | +| `[policy_definition]` `p = sub, obj, act` | 一条策略规则长什么样 | +| `[policy_effect]` `e = some(where (p.eft == allow))` | 多条策略如何合成结论 | +| `[matchers]` `m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act` | `r` 与 `p` 如何配对 | + +- `sub, obj, act` 只是最典型的字段,不是硬编码:可加 `dom`、资源属性,也可用对象做 ABAC。 +- RBAC 再加 `[role_definition] g = _,_`,表示主体与角色的继承关系;`g(a, b)` 意为“a 属于 b”,角色链会递归解析。 +- 模型只描述“怎么判”,策略才决定“允许谁”,两者可分开存储与更新。 + +## 2. 核心组件 + +Casbin 运行时由以下组件构成,多数可在不同程度上替换或选配: + +| 组件 | 职责 | +|---|---| +| Model | PERM 四段组成的规则模型,描述匹配逻辑,不含具体策略 | +| Policy | 具体策略规则的集合,可在内存或外部存储 | +| Enforcer | 判定入口:把请求套进模型与策略,得到 true/false | +| Adapter | 策略的存储读写层(DB/文件等),由应用选配 | +| RoleManager | 维护角色继承关系,支撑 `g()` | +| Watcher | 感知其他实例的变更并触发重载;Dispatcher 负责派发变更事件 | + +组件协作:Model + Policy 交给 Enforcer 判定;Adapter 负责把 Policy 读入/写出;RoleManager 供 `g()` 解析继承;Watcher/Dispatcher 让多实例感知变更。 + +## 3. 判定链与策略示意 + +请求 → matcher 匹配 → 命中的策略 effect 合成 → 结果。 + +``` +g, alice, admin # alice 属于 admin +p, admin, report, read # admin 可读 report +Enforce(alice, report, read) => true # 经角色继承 +Enforce(bob, report, read) => false # 无授权 +``` + +- 判定按请求逐条扫描策略:matcher 命中后,由 effect 合成最终结论。 +- `some(where (p.eft == allow))` 是白名单语义:只要有一条 allow 就放行。 + +## 4. 关键提醒 + +- **deny 与 error 都 fail-closed**:`false` 是判定为拒,`err != nil` 是判定失败;安全场景两者都当拒绝,别把错误当放行。 +- **并发优先 `SyncedEnforcer`**:常用 API 有锁;普通 `Enforcer` 需自行加锁,也不要假设所有接口都并发安全。 +- **内存策略不会自动跨实例同步**:Casbin 核心不内置同步,多副本要自己配置 Watcher 通知或周期重载/对账。 +- **ABAC 能判断单条对象的属性/所有权,但不会自动生成 SQL 查询过滤**,列表与行级数据范围仍需业务层实现。 +- **迁移 v2 → v3**:只确认 adapter 自身路径带 `/v3` 不够,要核对其 `go.mod` 是否依赖 `github.com/casbin/casbin/v3`;v2 与 v3 的 `model.Model` 是不同 Go 包类型。 +- `Enforce` 的参数要与 `r` 定义一致,类型/语义由模型与 matcher 决定,不是固定三元组。 + +## 官方来源 + +- v3.0.0 release(module path 变更): +- v3.11.0 `persist/adapter.go`: +- ABAC 文档: diff --git a/note/随手记/mcp.md b/note/随手记/mcp.md new file mode 100644 index 0000000..d22a7fc --- /dev/null +++ b/note/随手记/mcp.md @@ -0,0 +1,5 @@ +opencode添加mcp + +```bash +opencode mcp add postgres -- npx -y @microsoft/postgres-mcp run +``` \ No newline at end of file diff --git a/note/随手记/常见图片格式区别.md b/note/随手记/常见图片格式区别.md new file mode 100644 index 0000000..9161723 --- /dev/null +++ b/note/随手记/常见图片格式区别.md @@ -0,0 +1,12 @@ +常见图片格式的核心区别,主要在于 **是否压缩、是否支持透明、是否支持矢量、文件大小、适合什么场景** + +| 格式 | 类型 | 透明背景 | 动画 | 放大是否模糊 | 文件大小 | 常见用途 | +| -------------- | ---- | ---------- | ------ | ------------ | -------- | ------------------------ | +| **JPG / JPEG** | 位图 | ❌ | ❌ | ✅ 会模糊 | 小 | 照片、摄影、电商图 | +| **PNG** | 位图 | ✅ | ❌ | ✅ 会模糊 | 中~大 | Logo、截图、透明图片、UI | +| **SVG** | 矢量 | ✅ | 可实现 | ❌ 不模糊 | 通常很小 | Logo、图标、网页插画 | +| **WebP** | 位图 | ✅ | ✅ | ✅ 会模糊 | 很小 | 网站图片、App 图片 | +| **GIF** | 位图 | ✅但有限 | ✅ | ✅ 会模糊 | 通常较大 | 简单动图、表情包 | +| **AVIF** | 位图 | ✅ | ✅ | ✅ 会模糊 | 很小 | 新一代网页图片 | +| **BMP** | 位图 | 通常不支持 | ❌ | ✅ 会模糊 | 非常大 | 较少使用 | +| **TIFF** | 位图 | 视情况 | ❌ | ✅ 会模糊 | 很大 | 印刷、扫描、专业摄影 | \ No newline at end of file