hankmo.com

HANKMO.COM

🧑‍💻潜心研技术,积极品人生!🌱

191
文章
16
分类
249
标签

📝 最新文章

Go高级教程:其他并发工具

Go 高级教程:其他并发工具 如果说 Goroutine 和 Channel 是 Go 并发的“常规武器”,那么 sync 包里的工具就是“特种装备”。虽然不常用,但关键时刻能救命(榨干 CPU 的最后一点性能)。 除了 这里 介绍的诸多基础并发工具外,Go 标准库还提供了一些高级并发工具,下面介绍几个比较常用的。 1. 减轻 GC 压力:sync.Pool 我们在讲 GC 的时候提过,如果你频繁申请和销毁大对象(比如 HTTP Response 对象,或者大的 byte buffer),GC 会鸭梨山大。 sync.Pool 就是为了对象复用而生的。 1.1 示例代码 1package main 2 3import ( 4 "fmt" 5 "sync" 6) 7 8// 定义池子 9var bufPool = sync.Pool{ 10 // New 函数:当池子里没存货时,调用它创建一个新的 11 New: func() interface{} { 12 fmt.Println("Creating new buffer") 13 return make([]byte, 1024) 14 }, 15} 16 17func main() { 18 // 1.Get(): 借一个对象 19 buf := bufPool.Get().([]byte) 20 21 // 用完它... 22 23 // 2. Put(): 还回去,下次给别人用 24 // 注意:还之前最好重置一下状态(比如清空) 25 bufPool.Put(buf) 26 27 // 再次 Get,就不会触发 New,而是直接复用刚才那个 28 buf2 := bufPool.Get().([]byte) 29 _ = buf2 30} 1.2 注意事项 sync.Pool 里的对象随时可能被 GC 回收!所以绝对不要用它存数据库连接、Socket 连接这种必须长久保持的资源。它只适合存“临时垃圾”。 ...

Go高级教程:深入理解 GMP 调度器

Go 高级教程:深入理解 GMP 调度器 为什么 Go 语言能轻松支撑百万并发? 为什么 Goroutine 切换成本这么低? 这一切的背后,都站着一位神秘的大管家 —— GMP 调度器。 1. 为什么需要 GMP? 在很久很久以前(其实也就几十年前),我们写代码都是直接跟 线程 (Thread) 打交道。线程是操作系统(OS)调度的最小单位。 但是,线程这玩意儿太“贵”了: 内存占用高:一个线程栈大概要几 MB。 切换成本大:线程切换需要陷入内核态,保存寄存器、上下文,这简直就是“劳民伤财”。 这时候,Go 语言的设计师们拍案而起:“我们要造一种更轻量的线程!” 于是,Goroutine (协程) 诞生了。它初始只要几 KB,切换成本极低。 这就带来了一个问题:操作系统只认识线程,不认识 Goroutine。谁来负责把成千上万个 Goroutine 分配给 CPU 跑呢? 这就需要一个“中间商” —— Go 运行时调度器 (Scheduler)。 图示: Thread 与 Goroutine 的区别 2. GMP 模型大揭秘 GMP 其实是三个角色的缩写: G (Goroutine):我们写的代码任务,也就是协程。 M (Machine):工作线程(Thread),对应操作系统的真实线程。它是真正的干活人(搬砖工)。 P (Processor):逻辑处理器(Context),可以理解为“调度上下文”或“资源”。它是包工头,负责管理 G,并把 G 交给 M 去执行。 形象的比喻 想象一个大型搬砖工地: G (砖头):待搬运的任务。 M (工人):负责搬砖的劳动力。 P (手推车):工人必须推着车才能搬砖(因为车里装着搬砖工具和任务清单)。 如果没有 P(手推车),M(工人)就不知道该干啥。 ...

Go 项目工程结构最佳实践

好的项目结构能让代码更易理解、易维护、易扩展。Go 社区有一套被广泛认可的标准布局。 1. 标准项目布局 参考:golang-standards/project-layout 1myproject/ 2├── cmd/ # 主程序入口 3│ ├── server/ 4│ │ └── main.go # 服务端入口 5│ └── cli/ 6│ └── main.go # 命令行工具入口 7├── internal/ # 私有代码(不可被外部导入) 8│ ├── handler/ 9│ ├── service/ 10│ └── repository/ 11├── pkg/ # 公共库(可被外部导入) 12│ ├── util/ 13│ └── validator/ 14├── api/ # API 定义(OpenAPI/Swagger) 15│ └── openapi.yaml 16├── web/ # Web 静态资源 17│ ├── static/ 18│ └── templates/ 19├── configs/ # 配置文件 20│ ├── config.yaml 21│ └── config.prod.yaml 22├── scripts/ # 脚本(构建、部署等) 23│ ├── build.sh 24│ └── deploy.sh 25├── test/ # 额外的测试数据 26│ └── fixtures/ 27├── docs/ # 文档 28│ └── API.md 29├── go.mod 30├── go.sum 31├── Makefile # 构建脚本 32├── Dockerfile 33└── README.md 2. 核心目录详解 2.1 cmd/ 存放主程序入口,每个子目录对应一个可执行文件。 ...

环境变量与配置管理

环境变量与配置管理 同一份代码需要在开发、测试、生产等不同环境运行。配置管理让我们能够灵活切换环境,而不需要修改代码。 1. 环境变量基础 1.1 读取环境变量 1import ( 2 "fmt" 3 "os" 4) 5 6func main() { 7 // 读取环境变量 8 dbHost := os.Getenv("DB_HOST") 9 if dbHost == "" { 10 dbHost = "localhost" // 默认值 11 } 12 13 fmt.Println("DB Host:", dbHost) 14 15 // 检查环境变量是否存在 16 port, exists := os.LookupEnv("PORT") 17 if !exists { 18 port = "8080" 19 } 20} 1.2 设置环境变量 1// 在程序中设置(仅影响当前进程) 2os.Setenv("API_KEY", "secret123") 3 4// 在 shell 中设置 5// export DB_HOST=localhost 6// export DB_PORT=3306 2. godotenv:.env 文件 2.1 安装 1go get -u github.com/joho/godotenv 2.2 使用 创建 .env 文件: 1DB_HOST=localhost 2DB_PORT=3306 3DB_USER=root 4DB_PASSWORD=secret 5API_KEY=your-api-key 6DEBUG=true 加载配置: ...

模糊测试入门 (Fuzzing)

单元测试只能测试你想到的情况,而模糊测试能帮你发现你没想到的边界情况。 1. 什么是模糊测试? 模糊测试 (Fuzzing) 是一种自动化测试技术,通过生成大量随机或半随机的输入数据来测试程序,寻找崩溃、panic、死循环等异常。 传统测试 vs 模糊测试: 1// 传统单元测试:测试已知的输入 2func TestAdd(t *testing.T) { 3 if Add(2, 3) != 5 { 4 t.Error("2 + 3 should be 5") 5 } 6} 7 8// 模糊测试:测试大量随机输入 9func FuzzAdd(f *testing.F) { 10 f.Fuzz(func(t *testing.T, a, b int) { 11 result := Add(a, b) 12 // 检查属性而不是具体值 13 if result < a && result < b { 14 t.Errorf("Add(%d, %d) = %d, should be >= both", a, b, result) 15 } 16 }) 17} 2. 编写 Fuzz 测试 2.1 基础示例 假设我们有一个解析 URL 的函数: 1// url.go 2package myurl 3 4import ( 5 "fmt" 6 "strings" 7) 8 9func ParseURL(rawURL string) (string, error) { 10 if !strings.HasPrefix(rawURL, "http://") && !strings.HasPrefix(rawURL, "https://") { 11 return "", fmt.Errorf("invalid protocol") 12 } 13 return rawURL, nil 14} 编写 Fuzz 测试: ...

日志管理:从 log 到 zap

日志是排查问题的第一手段。一个好的日志系统应该:结构化、高性能、可配置、易于检索。 1. 标准库 log 1.1 基础使用 1package main 2 3import ( 4 "log" 5) 6 7func main() { 8 log.Println("This is a log message") 9 log.Printf("User %s logged in", "admin") 10 11 // log.Fatal 会调用 os.Exit(1) 12 // log.Fatal("Fatal error") 13 14 // log.Panic 会触发 panic 15 // log.Panic("Panic error") 16} 1.2 自定义 Logger 1import ( 2 "log" 3 "os" 4) 5 6func main() { 7 // 创建自定义 Logger 8 logger := log.New( 9 os.Stdout, // 输出目标 10 "[MyApp] ", // 前缀 11 log.Ldate|log.Ltime|log.Lshortfile, // 标志 12 ) 13 14 logger.Println("Custom logger message") 15 // 输出:[MyApp] 2025/12/18 10:00:00 main.go:15: Custom logger message 16} 1.3 写入文件 1func main() { 2 file, err := os.OpenFile("app.log", 3 os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666) 4 if err != nil { 5 panic(err) 6 } 7 defer file.Close() 8 9 log.SetOutput(file) 10 log.Println("This goes to file") 11} 2. slog:结构化日志 (Go 1.21+) 2.1 基础使用 1import ( 2 "log/slog" 3 "os" 4) 5 6func main() { 7 // 创建 JSON 格式的 Logger 8 logger := slog.New(slog.NewJSONHandler(os.Stdout, nil)) 9 10 logger.Info("User logged in", 11 "user_id", 123, 12 "username", "admin", 13 "ip", "192.168.1.1") 14 15 // 输出: 16 // {"time":"2025-12-18T10:00:00Z","level":"INFO","msg":"User logged in","user_id":123,"username":"admin","ip":"192.168.1.1"} 17} 2.2 日志级别 1func main() { 2 logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{ 3 Level: slog.LevelInfo, // 只输出 Info 及以上级别 4 })) 5 6 logger.Debug("Debug message") // 不会输出 7 logger.Info("Info message") // 会输出 8 logger.Warn("Warning message") // 会输出 9 logger.Error("Error message") // 会输出 10} 2.3 上下文日志 1func main() { 2 logger := slog.Default() 3 4 // 创建带有固定字段的子 Logger 5 requestLogger := logger.With( 6 "request_id", "abc123", 7 "user_id", 456, 8 ) 9 10 requestLogger.Info("Processing request") 11 requestLogger.Info("Request completed") 12 13 // 两条日志都会自动包含 request_id 和 user_id 14} 3. zap:高性能日志库 3.1 安装 1go get -u go.uber.org/zap 3.2 快速开始 1import ( 2 "go.uber.org/zap" 3) 4 5func main() { 6 // 开发环境:易读的格式 7 logger, _ := zap.NewDevelopment() 8 defer logger.Sync() // 刷新缓冲区 9 10 logger.Info("User logged in", 11 zap.String("username", "admin"), 12 zap.Int("user_id", 123)) 13 14 // 生产环境:JSON 格式 15 prodLogger, _ := zap.NewProduction() 16 defer prodLogger.Sync() 17 18 prodLogger.Info("Server started", 19 zap.String("port", "8080")) 20} 3.3 自定义配置 1func main() { 2 config := zap.NewProductionConfig() 3 4 // 设置日志级别 5 config.Level = zap.NewAtomicLevelAt(zap.InfoLevel) 6 7 // 设置输出路径 8 config.OutputPaths = []string{ 9 "stdout", 10 "./logs/app.log", 11 } 12 13 // 设置错误日志路径 14 config.ErrorOutputPaths = []string{ 15 "stderr", 16 "./logs/error.log", 17 } 18 19 logger, _ := config.Build() 20 defer logger.Sync() 21 22 logger.Info("Application started") 23} 3.4 性能对比 1// zap 提供了 SugaredLogger,牺牲一点性能换取更简洁的 API 2logger, _ := zap.NewProduction() 3sugar := logger.Sugar() 4 5// 结构化日志(最快) 6logger.Info("User logged in", 7 zap.String("username", "admin")) 8 9// 格式化日志(稍慢,但更方便) 10sugar.Infof("User %s logged in", "admin") 4. 日志分级 4.1 日志级别 从低到高: ...

文件与 IO 操作实战

文件操作是编程中最基础也最常用的需求。Go 语言的 io 包设计优雅,通过 io.Reader 和 io.Writer 接口实现了高度的抽象和复用。 1. 文件读取 1.1 一次性读取整个文件 1package main 2 3import ( 4 "fmt" 5 "os" 6) 7 8func main() { 9 // 方法一:os.ReadFile (推荐,Go 1.16+) 10 data, err := os.ReadFile("test.txt") 11 if err != nil { 12 panic(err) 13 } 14 fmt.Println(string(data)) 15 16 // 方法二:ioutil.ReadFile (已废弃,但仍可用) 17 // data, err := ioutil.ReadFile("test.txt") 18} 1.2 逐行读取(大文件) 1import ( 2 "bufio" 3 "fmt" 4 "os" 5) 6 7func main() { 8 file, err := os.Open("large.txt") 9 if err != nil { 10 panic(err) 11 } 12 defer file.Close() // 确保文件关闭 13 14 scanner := bufio.NewScanner(file) 15 for scanner.Scan() { 16 line := scanner.Text() 17 fmt.Println(line) 18 } 19 20 if err := scanner.Err(); err != nil { 21 panic(err) 22 } 23} 1.3 按块读取 1func main() { 2 file, _ := os.Open("data.bin") 3 defer file.Close() 4 5 buffer := make([]byte, 1024) // 每次读 1KB 6 for { 7 n, err := file.Read(buffer) 8 if err == io.EOF { 9 break // 文件读完 10 } 11 if err != nil { 12 panic(err) 13 } 14 fmt.Printf("Read %d bytes\n", n) 15 // 处理 buffer[:n] 16 } 17} 2. 文件写入 2.1 一次性写入 1func main() { 2 data := []byte("Hello, World!\n") 3 4 // 写入文件(会覆盖原文件) 5 // 0644 是文件权限:rw-r--r-- 6 err := os.WriteFile("output.txt", data, 0644) 7 if err != nil { 8 panic(err) 9 } 10} 2.2 追加写入 1func main() { 2 file, err := os.OpenFile("log.txt", 3 os.O_APPEND|os.O_CREATE|os.O_WRONLY, // 追加模式 4 0644) 5 if err != nil { 6 panic(err) 7 } 8 defer file.Close() 9 10 file.WriteString("New log entry\n") 11} 2.3 使用 bufio 缓冲写入 1func main() { 2 file, _ := os.Create("output.txt") 3 defer file.Close() 4 5 writer := bufio.NewWriter(file) 6 7 for i := 0; i < 1000; i++ { 8 writer.WriteString(fmt.Sprintf("Line %d\n", i)) 9 } 10 11 writer.Flush() // 重要:将缓冲区内容写入文件 12} 3. 文件与目录操作 3.1 检查文件是否存在 1func fileExists(path string) bool { 2 _, err := os.Stat(path) 3 return !os.IsNotExist(err) 4} 3.2 创建目录 1// 创建单层目录 2os.Mkdir("mydir", 0755) 3 4// 创建多层目录(类似 mkdir -p) 5os.MkdirAll("path/to/mydir", 0755) 3.3 遍历目录 1import ( 2 "fmt" 3 "os" 4 "path/filepath" 5) 6 7func main() { 8 // 方法一:filepath.Walk 9 filepath.Walk(".", func(path string, info os.FileInfo, err error) error { 10 if err != nil { 11 return err 12 } 13 if !info.IsDir() { 14 fmt.Println("File:", path) 15 } 16 return nil 17 }) 18 19 // 方法二:os.ReadDir (Go 1.16+) 20 entries, _ := os.ReadDir(".") 21 for _, entry := range entries { 22 fmt.Println(entry.Name(), entry.IsDir()) 23 } 24} 3.4 删除文件/目录 1// 删除文件 2os.Remove("file.txt") 3 4// 删除目录及其所有内容(类似 rm -rf) 5os.RemoveAll("mydir") 4. io.Copy 文件拷贝 4.1 复制文件 1func copyFile(src, dst string) error { 2 source, err := os.Open(src) 3 if err != nil { 4 return err 5 } 6 defer source.Close() 7 8 destination, err := os.Create(dst) 9 if err != nil { 10 return err 11 } 12 defer destination.Close() 13 14 // io.Copy 高效复制 15 _, err = io.Copy(destination, source) 16 return err 17} 4.2 显示进度的复制 1type ProgressReader struct { 2 reader io.Reader 3 total int64 4 read int64 5} 6 7func (pr *ProgressReader) Read(p []byte) (int, error) { 8 n, err := pr.reader.Read(p) 9 pr.read += int64(n) 10 11 // 打印进度 12 fmt.Printf("\rProgress: %.2f%%", float64(pr.read)/float64(pr.total)*100) 13 14 return n, err 15} 5. 网络文件下载 1import ( 2 "io" 3 "net/http" 4 "os" 5) 6 7func downloadFile(url, filepath string) error { 8 // 发起 HTTP GET 请求 9 resp, err := http.Get(url) 10 if err != nil { 11 return err 12 } 13 defer resp.Body.Close() 14 15 // 创建文件 16 out, err := os.Create(filepath) 17 if err != nil { 18 return err 19 } 20 defer out.Close() 21 22 // 将响应体复制到文件 23 _, err = io.Copy(out, resp.Body) 24 return err 25} 26 27func main() { 28 downloadFile("https://example.com/file.zip", "file.zip") 29} 6. io.Reader 和 io.Writer 接口 6.1 理解接口 1type Reader interface { 2 Read(p []byte) (n int, err error) 3} 4 5type Writer interface { 6 Write(p []byte) (n int, err error) 7} 实现了这两个接口的类型: ...

Go 模块管理入门

在 Go 1.11 之前,依赖管理是 Go 语言的一大痛点。GOPATH、vendor、dep 等方案各有缺陷。Go Modules 的出现彻底解决了这个问题,成为 Go 官方推荐的依赖管理方案。 1. 什么是 Go Modules? Go Modules 是 Go 语言的依赖管理系统,它解决了以下问题: 版本管理:明确指定依赖的版本 可重现构建:确保不同环境构建结果一致 依赖隔离:不同项目可以使用同一个包的不同版本 2. 初始化模块 2.1 创建新模块 1# 创建项目目录 2mkdir myproject 3cd myproject 4 5# 初始化模块 6# 模块路径通常是代码仓库地址 7go mod init github.com/username/myproject 这会生成 go.mod 文件: 1module github.com/username/myproject 2 3go 1.21 2.2 go.mod 文件结构 1module github.com/username/myproject // 模块路径 2 3go 1.21 // Go 版本 4 5require ( 6 github.com/gin-gonic/gin v1.9.1 // 直接依赖 7 gorm.io/gorm v1.25.5 8) 9 10require ( 11 github.com/gin-contrib/sse v0.1.0 // 间接依赖(由 gin 引入) 12 // ... 更多间接依赖 13) // indirect 14 15exclude ( 16 github.com/some/package v1.2.3 // 排除某个版本 17) 18 19replace ( 20 github.com/old/package => github.com/new/package v1.0.0 // 替换依赖 21) 3. 常用命令 3.1 添加依赖 1# 方法一:直接在代码中 import,然后运行 2go mod tidy 3 4# 方法二:手动添加 5go get github.com/gin-gonic/gin@v1.9.1 6 7# 获取最新版本 8go get github.com/gin-gonic/gin@latest 9 10# 获取特定版本 11go get github.com/gin-gonic/gin@v1.8.0 12 13# 获取某个 commit 14go get github.com/gin-gonic/gin@abc1234 3.2 go mod tidy 最常用的命令,它会: ...

GoLang教程——项目实战示例

学习了基础语法后,是时候动手写一个完整的项目了。本章将从零开始构建一个简单的 CLI 工具(简易计算器),重点讲解 Go Modules 依赖管理和标准的 项目目录结构。 1. 初始化项目 首先,创建一个文件夹并初始化 Go Module。go mod 是 Go 官方的依赖管理工具。 1mkdir my-calc 2cd my-calc 3# 初始化模块,模块名为 example.com/calc 4go mod init example.com/calc 执行后会生成一个 go.mod 文件,它声明了模块名称和 Go 版本。 2. 项目结构 虽然 Go 对目录结构没有强制要求,但社区约定俗成的标准布局(Standard Layout)如下: 1my-calc/ 2├── go.mod // 模块定义 3├── main.go // 程序入口 4└── pkg/ // 库代码目录 5 └── mathop/ // 子包:数学操作 6 └── add.go 3. 编写代码 编写子包逻辑 在 pkg/mathop/add.go 中编写核心业务逻辑: 1package mathop 2 3// Add 函数首字母大写,表示是可以导出的(Public) 4func Add(a, b int) int { 5 return a + b 6} 编写入口函数 在根目录 main.go 中调用我们的子包: ...

GoLang教程——泛型编程入门

在 Go 1.18 之前,由于缺乏泛型,我们想编写一个通用的 Min 函数(既能比对 int 也能比对 float),通常需要写两遍代码,或者使用性能较差的 interface{} 和反射。 Go 1.18 终于引入了 泛型 (Generics),允许我们在函数和类型定义中使用 类型参数 (Type Parameters)。这极大地提高了代码的复用性和类型安全性。 1. 泛型函数 先看一个传统的非泛型版本: 1func MinInt(a, b int) int { 2 if a < b { return a } 3 return b 4} 5// 如果要支持 float,还得再写一个 MinFloat... 使用泛型重写: 1import "fmt" 2 3// [T int | float64] 定义了类型参数 T 4// T 被限制为 int 或 float64(类型约束) 5func Min[T int | float64](a, b T) T { 6 if a < b { 7 return a 8 } 9 return b 10} 11 12func main() { 13 // 显式实例化 14 fmt.Println(Min[int](10, 20)) 15 16 // 隐式类型推导(推荐):编译器自动根据参数推导出 T 是 float64 17 fmt.Println(Min(3.14, 1.59)) 18} 2. 自定义泛型类型 除了函数,结构体也可以是泛型的。比如我们要实现一个通用的栈(Stack)。 ...

📚 文章分类