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 连接这种必须长久保持的资源。它只适合存“临时垃圾”。 ...

2025-12-20 · 3 min · 475 words · Hank

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(工人)就不知道该干啥。 ...

2025-11-28 · 2 min · 289 words · Hank

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/ 存放主程序入口,每个子目录对应一个可执行文件。 ...

2025-10-29 · 3 min · 596 words · Hank

环境变量与配置管理

环境变量与配置管理 同一份代码需要在开发、测试、生产等不同环境运行。配置管理让我们能够灵活切换环境,而不需要修改代码。 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 加载配置: ...

2025-09-11 · 3 min · 620 words · Hank

模糊测试入门 (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 测试: ...

2025-08-20 · 3 min · 619 words · Hank

日志管理:从 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 日志级别 从低到高: ...

2025-07-18 · 3 min · 617 words · Hank

文件与 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} 实现了这两个接口的类型: ...

2025-06-19 · 4 min · 782 words · Hank

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 最常用的命令,它会: ...

2025-06-06 · 3 min · 461 words · Hank

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 中调用我们的子包: ...

2025-05-25 · 2 min · 228 words · Hank

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)。 ...

2025-03-22 · 2 min · 318 words · Hank

GoLang教程——Context上下文实战

在 Go 的并发编程中,context(上下文)包是绝对的核心。无论是处理 HTTP 请求、RPC 调用,还是数据库查询,Context 都扮演着控制 Goroutine 生命周期的角色。 简而言之,Context 主要解决三个问题: 取消信号:通知子 Goroutine 停止工作,释放资源。 超时控制:规定任务必须在多长时间内完成,否则强制取消。 数据传递:在调用链中传递请求范围内的元数据(如 UserID, TraceID)。 1. 为什么需要 Context? 假设你启动了一个 Goroutine 去查询数据库,如果用户突然关闭了浏览器,或者请求处理太慢超时了,你希望这個后台查询任务能立即停止,而不是继续浪费数据库资源。这就是 Context 的用武之地。 Go 的设计原则:谁启动了 Goroutine,谁就有责任(通过 Context)管理它的退出。 2. 超时控制 (WithTimeout) 这是 Context 最常用的场景。 1package main 2 3import ( 4 "context" 5 "fmt" 6 "time" 7) 8 9func main() { 10 // 1. 创建一个带超时的 Context 11 // 规定任务必须在 2 秒内完成,否则 ctx 会收到取消信号 12 ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) 13 // 关键:函数结束时必须调用 cancel,防止内存泄漏 14 defer cancel() 15 16 fmt.Println("Start working...") 17 18 // 2. 将 ctx 传递给耗时任务 19 doSlowWork(ctx) 20} 21 22func doSlowWork(ctx context.Context) { 23 // 模拟一个需要 3 秒才能完成的任务 24 // Select 会等待 done 信号或者 work 完成,以此来实现超时抢占 25 select { 26 case <-time.After(3 * time.Second): // 模拟业务逻辑 27 fmt.Println("Work done successfully!") 28 case <-ctx.Done(): // 监听 Context 的取消信号 29 // 如果超时了,ctx.Err() 会返回 DeadlineExceeded 30 fmt.Println("Work cancelled:", ctx.Err()) 31 } 32} 运行结果: 由于任务耗时 3秒,但 Context 限制 2秒,所以输出: Work cancelled: context deadline exceeded ...

2025-03-02 · 2 min · 359 words · Hank

GoLang教程——单元测试与基准测试

在 Go 语言中,测试不是“一等公民”,而是“超等公民”。Go 编译器自带了 go test 工具,标准库提供了 testing 包,这使得编写测试变得异常简单且规范。 本章将详细介绍: 单元测试:如何编写基础测试用例。 表格驱动测试:Go 社区推荐的最佳实践,如何用更少的代码覆盖更多的场景。 基准测试 (Benchmark):如何科学地测量代码的性能。 1. 基础单元测试 Go 的测试文件必须以 _test.go 结尾,测试函数必须以 Test 开头。 假设我们要测试一个简单的加法函数(保存为 math.go): 1package math 2 3func Add(a, b int) int { 4 return a + b 5} 对应的测试文件(保存为 math_test.go): 1package math 2 3import ( 4 "testing" 5) 6 7// 测试函数签名:func TestXxx(t *testing.T) 8func TestAdd(t *testing.T) { 9 got := Add(1, 2) 10 want := 3 11 12 if got != want { 13 // t.Errorf 输出错误信息,测试继续执行 14 // t.Fatalf 输出错误信息,并立即终止当前测试函数 15 t.Errorf("Add(1, 2) = %d; want %d", got, want) 16 } 17} 运行命令: ...

2025-02-19 · 2 min · 356 words · Hank

GoLang教程——标准库精选

Go 被誉为“自带电池”(Batteries Included)的语言,因为其标准库极其丰富,涵盖了开发中 80% 的需求。本章精选了几个最常用、最基础的标准库,展示它们的常规用法。 示例代码 1package main 2 3import ( 4 "encoding/json" 5 "fmt" 6 "strings" 7 "time" 8) 9 10type User struct { 11 ID int `json:"id"` 12 Name string `json:"name"` 13 // json:"-" 表示在 JSON 中忽略该字段 14 Email string `json:"-"` 15} 16 17func main() { 18 // 1. fmt - 格式化 19 age := 18 20 // Sprintf 返回字符串,Printf 直接打印 21 msg := fmt.Sprintf("I am %d years old", age) 22 fmt.Println(msg) 23 24 // 2. strings - 字符串处理 25 str := " hello world " 26 fmt.Println(strings.TrimSpace(str)) // 去除首尾空格 27 fmt.Println(strings.Contains(str, "world")) // true 28 fmt.Println(strings.Join([]string{"a", "b"}, "-")) // a-b 29 30 // 3. time - 时间处理 31 now := time.Now() 32 // Go 的独特日期格式化模板:2006-01-02 15:04:05 33 fmt.Println(now.Format("2006-01-02 15:04:05")) 34 35 // 时间计算 36 later := now.Add(time.Hour) 37 fmt.Println(later.Sub(now)) // 1h0m0s 38 39 // 4. encoding/json - JSON 编解码 40 user := User{ID: 1, Name: "Hank", Email: "admin@hankmo.com"} 41 42 // 序列化 Struct -> JSON 43 jsonData, _ := json.Marshal(user) 44 fmt.Println(string(jsonData)) // {"id":1,"name":"Hank"} 45 46 // 反序列化 JSON -> Struct 47 var u2 User 48 json.Unmarshal(jsonData, &u2) 49 fmt.Println(u2.Name) 50} 关键点解释 fmt 包 Print / Println:标准输出。 Printf:格式化输出,常用占位符 %s(字符串), %d(整数), %v(自动推导), %+v(显示结构体字段名)。 time 包 格式化陷阱:必须记住基准时间 2006-01-02 15:04:05(助记:1月2号3点4分5秒06年)。 time.Duration:表示时间段,单位是纳秒。常用 10 * time.Second。 strings 包 提供了高效的不可变字符串操作:Split, Replace, Count, HasPrefix 等。 频繁拼接字符串推荐使用 strings.Builder。 encoding/json 包 利用 Struct Tags(如 `json:"name"`)来控制序列化行为。 只会序列化结构体中的 导出字段(首字母大写)。 小结 熟练使用标准库可以避免重复造轮子。除了上面介绍的,还有 net/http(Web服务)、io/ioutil(文件读写)、os(系统操作)等也非常重要。 ...

2025-01-12 · 2 min · 238 words · Hank

GoLang教程——并发进阶

在掌握了 Goroutine 和 Channel 的基础知识后,我们需要了解 Go 标准库 sync 包提供的各种同步工具。这些工具能帮助我们更好地控制并发程序的执行流程,避免数据竞争,提高程序的可靠性。 sync.WaitGroup:等待一组 Goroutine 完成 基本用法 WaitGroup 用于等待一组 Goroutine 完成。它有三个方法: Add(delta int):增加计数器 Done():减少计数器(相当于 Add(-1)) Wait():阻塞直到计数器为 0 1package main 2 3import ( 4 "fmt" 5 "sync" 6 "time" 7) 8 9func worker(id int, wg *sync.WaitGroup) { 10 defer wg.Done() // 函数结束时调用 Done() 11 12 fmt.Printf("Worker %d starting\n", id) 13 time.Sleep(time.Second) 14 fmt.Printf("Worker %d done\n", id) 15} 16 17func main() { 18 var wg sync.WaitGroup 19 20 // 启动 5 个 worker 21 for i := 1; i <= 5; i++ { 22 wg.Add(1) // 每启动一个 goroutine,计数器 +1 23 go worker(i, &wg) 24 } 25 26 wg.Wait() // 等待所有 worker 完成 27 fmt.Println("All workers completed") 28} 注意事项 Add() 必须在 Wait() 之前调用 Add() 通常在启动 goroutine 之前调用,而不是在 goroutine 内部 必须传递 WaitGroup 的指针,而不是值拷贝 sync.Mutex:互斥锁 基本用法 Mutex(互斥锁)用于保护共享资源,确保同一时间只有一个 goroutine 可以访问。 ...

2024-12-05 · 5 min · 898 words · Hank

GoLang教程——并发基础

Go 语言之所以在云原生时代大放异彩,很大程度上归功于其原生支持的高效并发模型。Go 采用了 Goroutine(协程)和 Channel(通道)来实现并发,这种模式被称为 CSP(通信顺序进程)。本章将带你入门 Go 的并发世界。 示例代码 1package main 2 3import ( 4 "fmt" 5 "time" 6) 7 8// 一个模拟耗时任务的函数 9func worker(id int, jobs <-chan int, results chan<- int) { 10 for j := range jobs { 11 fmt.Printf("Worker %d started job %d\n", id, j) 12 time.Sleep(time.Second) // 模拟耗时 1 秒 13 fmt.Printf("Worker %d finished job %d\n", id, j) 14 results <- j * 2 // 将结果发送回 results 通道 15 } 16} 17 18func main() { 19 // 创建两个通道:任务通道和结果通道 20 // 设置缓冲区大小为 10,防止阻塞 21 jobs := make(chan int, 10) 22 results := make(chan int, 10) 23 24 // 启动 3 个 Goroutine (Worker) 25 // 它们会并发地从 jobs 通道抢任务做 26 for w := 1; w <= 3; w++ { 27 go worker(w, jobs, results) 28 } 29 30 // 发送 5 个任务 31 for j := 1; j <= 5; j++ { 32 jobs <- j 33 } 34 close(jobs) // 关闭任务通道,告知 Worker 没有新任务了 35 36 // 接收并打印结果 37 for a := 1; a <= 5; a++ { 38 <-results 39 } 40 41 fmt.Println("All jobs finished!") 42} 关键点解释 Goroutine (协程) 使用 go 关键字 即可启动一个新的协程。例如 go funcName()。 协程比线程更轻量,启动成本极低,Go 运行时可以在少数几个 OS 线程上调度成千上万个 Goroutine。 main 函数本身也是一个 Goroutine。当 main 结束时,所有其他 Goroutine 都会被强制终止。 Channel (通道) 通道是 Goroutine 之间通信的管道。遵循“不要通过共享内存来通信,而要通过通信来共享内存”的原则。 定义:make(chan Type, capacity)。 发送:ch <- value。 接收:value := <-ch。 关闭:close(ch)。关闭后不能再发送,但可以继续接收已有的数据。 Range:for x := range ch 可以不断从通道读取数据,直到通道被关闭。 Select 选择器 select 语句是 Go 并发编程中的重要工具,用于同时等待多个通道操作。它的语法类似于 switch,但专门用于通道。 ...

2024-11-19 · 3 min · 428 words · Hank

GoLang教程——错误处理

Go 对错误处理的态度非常直白:错误就是一种值(Values),不是异常(Exceptions)。我们通过函数返回值来传递错误,并显式地检查它们。只有在真正的不可恢复情况(如数组越界)下,才会使用 panic。 示例代码 1package main 2 3import ( 4 "errors" 5 "fmt" 6) 7 8// 定义一个除法函数,返回 result 和 error 9func divide(a, b int) (int, error) { 10 if b == 0 { 11 // 使用 errors.New 创建一个简单的错误对象 12 return 0, errors.New("cannot divide by zero") 13 } 14 return a / b, nil 15} 16 17// 演示 panic 和 recover 18func safeCall() { 19 // defer 必须在 panic 发生前定义 20 defer func() { 21 // recover() 捕获 panic,如果返回值不为 nil,说明发生了 panic 22 if r := recover(); r != nil { 23 fmt.Println("Recovered from panic:", r) 24 } 25 }() 26 27 panic("Something went wrong terribly!") 28 fmt.Println("This line will not execute") 29} 30 31func main() { 32 // 1. 标准错误处理 33 res, err := divide(10, 0) 34 if err != nil { 35 fmt.Println("Error:", err) 36 } else { 37 fmt.Println("Result:", res) 38 } 39 40 // 2. 演示从 panic 中恢复 41 fmt.Println("Starting safeCall...") 42 safeCall() 43 fmt.Println("Program continues...") 44} 关键点解释 error 接口 Go 内置的 error 是一个接口,只包含一个方法 Error() string。 ...

2024-10-29 · 2 min · 217 words · Hank

GoLang教程——结构体与接口

Go 语言没有传统的“类”和“继承”概念,而是通过 结构体 (Struct) 来封装数据,通过 接口 (Interface) 来定义行为。这种组合式设计(Composition over Inheritance)让代码更加灵活和解耦。 示例代码 1package main 2 3import "fmt" 4 5// 定义一个接口:只要实现了 Speak 方法的类型,都满足这个接口 6type Speaker interface { 7 Speak() string 8} 9 10// 定义结构体 Dog 11type Dog struct { 12 Name string 13} 14 15// Dog 实现 Speaker 接口 16// 注意:Go 中没有 implements 关键字,这是隐式实现的 17func (d Dog) Speak() string { 18 return "Woof!" 19} 20 21// 定义结构体 Cat 22type Cat struct { 23 Name string 24} 25 26// Cat 实现 Speaker 接口 27func (c Cat) Speak() string { 28 return "Meow!" 29} 30 31// 多态演示:接收任何 Speaker 32func introduce(s Speaker) { 33 fmt.Println(s.Speak()) 34} 35 36func main() { 37 d := Dog{Name: "Buddy"} 38 c := Cat{Name: "Kitty"} 39 40 introduce(d) // Woof! 41 introduce(c) // Meow! 42 43 // 接口类型断言 44 var s Speaker = d 45 if dog, ok := s.(Dog); ok { 46 fmt.Printf("It's a dog named %s\n", dog.Name) 47 } 48} 关键点解释 结构体 (Struct) 用户自定义的数据类型,是一组字段的集合。 匿名嵌入:可以在结构体中嵌入另一个结构体(不给字段名),从而直接访问其字段和方法,实现类似“继承”的效果。 1type Animal struct { Age int } 2type Dog struct { 3 Animal // 嵌入 4 Name string 5} 6// d := Dog{} 7// d.Age = 10 // 直接访问 接口 (Interface) 鸭子类型 (Duck Typing):如果一只鸟走起来像鸭子,叫起来像鸭子,那它就是鸭子。在 Go 中,只要一个类型实现了接口要求的所有方法,它就自动实现了该接口。 空接口 interface{}:不包含任何方法,因此任何类型都实现了空接口。类似于 Java 的 Object,常用于泛型容器。 类型断言 value, ok := interfaceVar.(Type):检查接口变量是否保存了特定类型的值。建议总是检查 ok 以避免 panic。 小结 结构体用于组织数据,接口用于抽象行为。掌握 Go 的隐式接口实现和组合模式,是编写地道 Go 代码的关键。 ...

2024-09-15 · 2 min · 227 words · Hank

GoLang教程——数组、切片与映射

在处理数据集合时,Go 提供了三种主要方式:数组(Array)、切片(Slice)和映射(Map)。其中,切片和映射是日常开发中使用频率最高的数据结构。本章将介绍它们的区别与用法。 示例代码 1package main 2 3import "fmt" 4 5func main() { 6 // 1. 数组 (Array) 7 // 长度是类型的一部分,固定不可变 8 var arr [3]int = [3]int{10, 20, 30} 9 fmt.Println("Array:", arr) 10 11 // 2. 切片 (Slice) 12 // 动态数组,引用类型 13 slice := []int{1, 2, 3, 4, 5} 14 15 // 操作:切取子集 [start:end] (左闭右开) 16 subSlice := slice[1:3] // 包含索引 1, 2 的元素 -> {2, 3} 17 fmt.Println("SubSlice:", subSlice) 18 19 // 操作:追加元素 20 // 当容量不足时,append 会自动扩容 21 slice = append(slice, 6) 22 fmt.Println("Appended Slice:", slice) 23 24 // 3. 映射 (Map) 25 // 键值对集合,类似 Python 的 dict 或 Java 的 HashMap 26 scores := make(map[string]int) 27 scores["Alice"] = 95 28 scores["Bob"] = 88 29 30 // 检查键是否存在 31 // val 是值,ok 是布尔值(存在为 true) 32 if score, ok := scores["Alice"]; ok { 33 fmt.Printf("Alice's score is %d\n", score) 34 } 35 36 // 删除键值对 37 delete(scores, "Bob") 38 39 // 遍历 Map (注意:遍历顺序是随机的) 40 for name, score := range scores { 41 fmt.Printf("%s: %d\n", name, score) 42 } 43} 关键点解释 数组 (Array) 声明:[Length]Type。例如 [5]int 和 [10]int 是完全不同的类型。 数组是 值类型,赋值或传递给函数时会发生拷贝(复制整个数组)。一般很少直接使用。 切片 (Slice) 声明:[]Type(不指定长度)。 切片本质上是对底层数组的一个“视窗”,包含三个属性:指针、长度 (len)、容量 (cap)。 推荐:使用 make([]Type, len, cap) 创建切片,或使用字面量 []Type{...}。 append 函数可能会返回一个新的切片引用(当发生扩容时),所以必须重新赋值:s = append(s, val)。 映射 (Map) 声明:map[KeyType]ValueType。 必须使用 make 初始化,或者使用字面量。未初始化的 map 是 nil,向其写入会导致 panic。 Key 必须是支持比较(==)的类型(如 int, string),切片不能作为 Key。 所有的 Map 操作都不是线程安全的(并发读写需要加锁)。 小结 数组长度固定,切片长度动态。优先使用切片。 Map 处理键值对,查找速度快。 切片和 Map 都是引用类型,传递给函数时不会拷贝底层数据,效率高。 练习题 创建一个包含 10 个元素的整型切片,使用 range 遍历并打印所有偶数。 统计一段英文文本中每个字符出现的次数,使用 map[rune]int 存储并打印结果。

2024-08-22 · 2 min · 230 words · Hank

GoLang教程——控制结构

Go 语言的控制结构非常精简。它没有 while 或 do-while,只有一个强大的 for 循环。同时,switch 更加智能,if 支持初始化语句。本章还将介绍 Go 独有的资源管理神器 —— defer。 示例代码 1package main 2 3import "fmt" 4 5func main() { 6 // 1. if 语句 7 x := 10 8 // 条件判断不需要小括号 () 9 if x > 5 { 10 fmt.Println("x is large") 11 } 12 13 // if 支持初始化语句:先执行初始化,再判断 14 if y := x * 2; y > 15 { 15 fmt.Println("y is", y) // y 的作用域仅限于 if 块 16 } 17 18 // 2. switch 语句 19 day := "Mon" 20 switch day { 21 case "Mon": 22 fmt.Println("Start of week") 23 // Go 默认不需要 break,自动终止 24 case "Fri": 25 fmt.Println("Weekend is coming") 26 default: 27 fmt.Println("Other day") 28 } 29 30 // 3. for 循环:Go 唯一的循环结构 31 // 形式一:类似于 C/Java 的 for 32 for i := 0; i < 3; i++ { 33 fmt.Print(i, " ") 34 } 35 fmt.Println() 36 37 // 形式二:类似于 while 38 count := 3 39 for count > 0 { 40 fmt.Print(count, " ") 41 count-- 42 } 43 fmt.Println() 44 45 // 4. defer 延迟执行 46 // 常用于资源释放,函数返回前才会执行 47 defer fmt.Println("Exiting main function...") 48 fmt.Println("Doing some work...") 49} 关键点解释 If 条件 条件表达式 不需要 小括号 ()。 大括号 {} 是 必须 的,且左大括号不能换行。 支持 if statement; condition 写法,常用于处理错误:if err := doSomething(); err != nil { ... }。 Switch 分支 默认不需要 break,匹配到一个 case 后自动停止。 如果想继续执行下一个 case,需要显式使用 fallthrough(极少用)。 switch 后可以没有表达式,直接在 case 中写条件判断,替代复杂的 if-else 链。 For 循环 Go 只有 for。 ...

2024-08-01 · 2 min · 286 words · Hank

GoLang教程——函数与方法

函数是 Go 程序的基本构建单元。Go 语言中的函数非常灵活,支持多返回值,并且可以为特定的类型定义方法(类似于面向对象中的成员函数)。本章将带你快速掌握函数的定义、调用以及方法的绑定。 示例代码 先看一个完整的可运行示例,展示了普通函数、多返回值以及方法的用法。 1package main 2 3import "fmt" 4 5// add 是一个普通函数,接收两个 int 参数,返回一个 int 结果 6func add(a, b int) int { 7 return a + b 8} 9 10// swap 演示多返回值,交换输入的两个字符串 11func swap(x, y string) (string, string) { 12 return y, x 13} 14 15// 定义一个简单的结构体 User 16type User struct { 17 Name string 18 Age int 19} 20 21// SayHello 是绑定到 User 结构体的方法 22// (u User) 称为接收者(Receiver) 23func (u User) SayHello() { 24 fmt.Printf("Hello, my name is %s and I am %d years old.\n", u.Name, u.Age) 25} 26 27// 只有指针接收者才能修改结构体内部的值 28func (u *User) Grow() { 29 u.Age++ 30} 31 32func main() { 33 // 1. 调用普通函数 34 sum := add(10, 20) 35 fmt.Println("10 + 20 =", sum) 36 37 // 2. 调用多返回值函数 38 a, b := swap("hello", "world") 39 fmt.Println(a, b) // world hello 40 41 // 3. 调用方法 42 user := User{Name: "Hank", Age: 18} 43 user.SayHello() 44 45 // 调用指针接收者方法修改状态 46 user.Grow() 47 fmt.Printf("Age after grow: %d\n", user.Age) // 19 48} 关键点解释 函数声明 使用 func 关键字。类型在变量名之后,这是 Go 的特色之一。 ...

2024-07-17 · 2 min · 237 words · Hank

Golang中singleflight的实现原理

我们知道了singleflight的用法,使用 singleflight 我们可以抑制多个请求,极大地节约带宽、增加系统吞吐量、提升性能。那么,singleflight 底层是如何实现的呢?本文我们来分析一番。 整体结构 singleflight 的核心是将同时段的多个请求抑制,只有一个请求能够真正请求资源,其他请求都阻塞。上一篇提到,singleflight的公开api仅包括: Group 对象: 它表示处理"相同数据"的一系列工作,在这里“重复请求”将会被抑制 Result 对象: 表示执行真正业务逻辑的结果对象 Do 方法: 执行请求抑制 DoChan 方法: 与Do相同,只是结果返回 <-chan Result 从这些api我们大致可以知道,调用 Do 或者 DoChan 方法就是我们所述的核心“请求”,可以猜测: 对于同一个 key,首先调用的会执行真正的逻辑,方法返回之前的后续所有相同的 key 调用都会阻塞,当第一个请求返回后,阻塞的这些调用就直接使用其返回值作为自己的返回值了。 顺着上述猜想的逻辑,我们看看singleflight的源码实现。代码不多,算上注释一共就200来行,我们来一一分析。 Group struct 首先看看 Group 的代码: 1type Group struct { 2 mu sync.Mutex // protects m 3 m map[string]*call // lazily initialized 4} Group 表示处理相同数据的一系列工作,这些工作存储到一个 map[string]*call 的结构中,为了保证并发安全,Group 内部持有 sync.Mutex 锁用来保护这个 map 的读写。 Group 有一个非常重要的两个方法 Do 和 DoChan, 在上一篇已经介绍过了。 再来回顾一下 Do 方法的定义: 1func (g *Group) Do(key string, fn func() (interface{}, error)) (v interface{}, err error, shared bool) 已经介绍过,这里再详细看看这些参数: key: 标记为同一请求的key,相同的key认为是相同的请求。这个key其实就是底层map的key,下边会详细介绍 fn: 真正执行业务逻辑的方法,该方法没有任何参数且返回一个任意对象 interface{} 和一个 error,这就是真正执行业务逻辑的标准方法,我们把 fn 方法称为业务方法 返回值: ...

2024-06-10 · 4 min · 775 words · Hank

Golang中singleflight的用法

在开发过程中,尤其是web server,有时候我们需要并发的请求数据,对于某些数据,同时发起的多个请求其实拿到的数据都是相同的,如果不处理这类请求,那么每个请求都会获取一遍数据,这无疑是对资源的浪费。比如要从数据库查询相同 id 的一条数据,并发的多个请求都会执行一次 sql 语句,增加了数据库的压力。 有没有一种方案,当多个请求同时发起后,只有第一个请求去获取数据,在它返回之前,其他请求都各自阻塞等待直到真正执行数据获取的请求返回后,直接拿它的结果?我们把这种将同时发起的多个请求转为一个请求去执行真正业务逻辑的情况称为“请求抑制”。在 Go 中,singleflight 就是用来处理请求抑制的。 简介 singleflight 包全路径为 golang.org/x/sync/singleflight, 目前版本是 v0.7.0。 前边提到,singleflight 用于抑制同一时间获取相同数据的重复请求。当存在多个重复请求时,singleflight 保证只有一个请求能执行,其他请求阻塞,直到前边的请求返回后直接将其返回值共享(shared)给阻塞的这些请求。 在使用之前,首先要理解何为"重复请求",如何区分"相同数据"。 相同数据:指并发下当前时间段多个请求获取的数据是完全相同的,比如获取全局的配置、查询天气数据等。 重复请求:指处理相同数据时,在一个请求从发起到返回之前这段时间,又有其他多个请求发起,那么这些请求就是重复请求。 理解了这两点,现在我们来看看 singleflight 的用法。 用法 singleflight 整体设计比较简单,公开的 api包括: Group 对象: 它表示处理"相同数据"的一系列工作,在这里“重复请求”将会被抑制 Result 对象: 表示执行真正业务逻辑的结果对象 Do 方法: 执行请求抑制,后文详述 DoChan 方法: 与Do相同,只是结果返回 <-chan Result Do 方法 Do 方法表示执行请求抑制,其定义如下: 1func (g *Group) Do(key string, fn func() (interface{}, error)) (v interface{}, err error, shared bool) { 2 // ... 3} 首先,它需要在 singleflight.Group 实例下执行,用于抑制这一组请求。 两个参数: key: 这个参数用来指示"相同数据",也就是说相同的key代表相同数据,区分相同数据是正确使用 singleflight 的关键。 fn: 真正执行业务逻辑的方法,该方法返回一个任意对象 interface{} 和一个 error 返回结果: v: 就是 fn 方法返回的第一个值 err: fn 方法返回的第二值 error shared: 当抑制了其他请求,返回 true, 也就是说将真正执行业务逻辑的请求返回结果共享给其他请求后,该值为 true, 否则为 false DoChan 方法 DoChan 方法与 Do 类似,只是将结果封装为 Result 并返回它的 chan: ...

2024-05-18 · 3 min · 478 words · Hank

依赖注入库wire使用入门

依赖注入(Dependency injection)不是一个新概念了,早在2004年 Martin Fowler 就在其文章Inversion of Control Containers and the Dependency Injection pattern提出了依赖注入模式。在Java的世界中,随处可见依赖注入的使用,如Spring框架的核心之一就是控制反转(IoC),而依赖注入就是 IoC 思想的一种技术实现,比如我们常用的 Spring 的注解 @Autowire,还有 Jsr250规范定义的 @Resource 注解都实现了依赖注入。 简单而言,依赖注入就是以构建松散耦合的程序为目的、以控制反转为基本思想而在软件工程实现的一种编程技术,它使得程序不需要关注对象的创建逻辑,只是通过函数或者属性告诉程序自己需要什么对象,关注所依赖对象的使用逻辑,从而将对象的创建和使用过程分离开。可见,依赖注入技术遵循依赖倒置原则。 虽然 GO 不是面向对象的语言,但是它也有依赖注入实现,常用的依赖注入框架包括 google 自身出品的 wire、Uber的dig、Facebook的inject等等。本文将介绍 wire 的基本使用。 不使用依赖注入时 在开始介绍 wire 之前,我们来编写一个简单的程序,该程序可以创建问候者并向您问好。 我们创建一个 greeter.go 文件,它包含以下几个结构体: 1type Message string 2 3type Greeter struct { 4 Msg Message 5} 6 7type Event struct { 8 Greeter Greeter 9} Message 表示消息,Greeter 表示问候者,它需要一个 Message,Event 表示一个事件,它用来触发问候。 现在,我们需要增加创建这些结构体的方法: 1func NewMessage(name string) Message { 2 return Message(fmt.Sprintf("hello, %s!", name)) 3} 4 5func NewGreeter(msg Message) Greeter { 6 return Greeter{Msg: msg} 7} 8 9func NewEvent(g Greeter) Event { 10 return Event{Greeter: g} 11} 12 13func (g Greeter) Greet() Message { 14 return g.Msg 15} 16 17func (e Event) Start() { 18 msg := e.Greeter.Greet() 19 fmt.Println(msg) 20} 通过 NewMessage 创建消息,通过 NewGreeter 创建一个问候者,通过 NewEvent 创建事件,然后就可以调用 Start 方法来发起问候了,其实底层最终调用的是 Greeter 的 Greet 方法。 ...

2023-08-06 · 4 min · 680 words · Hank

使用cli框架开发CLI程序

Go语言编译后的程序本身就是一个可用于命令行的可执行文件,而且Go天生支持CLI程序(command line interface),这得益于Go精简的语法以及自身支持Flag来解析命令行的选项参数等。但是,基于Go原始能力开发CLI程序仍然非常繁琐,如解析参数就是一个非常麻烦的工作。幸好,有许多非常强大的库可以用来简化我们的工作: cobra: 一个非常强大的用于构建CLI程序的库,官方地址见这儿 urfave/cli: 另一个使用广泛的CLI开发库,同样足够强大且简单易上手,官方地址见这儿 survey: 一个强大的构建交互式命令行程序的库,详情见这里 在开发CLI之前,你可以阅读Go官方的构建CLI程序指南。本文介绍如何使用 urfave/cli 库开发完整的CLI程序。 CLI程序 命令行界面(CLI,command line interface) 是一种通过用户或客户端发出的命令以及设备或程序以文本行形式做出的响应与设备或计算机程序进行交互的方式。 以上是维基百科的解释,简单而言就是控制台程序,我们需要通过控台执行程序并输入程序内置支持的选项、参数等完成与程序的交互以实现功能。 一般而言,CLI都具备这些功能: 命令:一个CLI程序应该至少支持一个命令,才能用来实现功能,大多CLI都支持多个命令,而且命令下还支持多个的子命令,用来将功能细分 选项:选项分为全局选项和命令选项,全局选项表示对所有命令都可以使用的选项,而命令选项这仅对特定命令有效 参数:CLI支持用户通过控制台传入参数告诉其特定信息,一般情况会通过选项指定参数来区分不同的用途,也可以直接传递给命令 帮助:展示给用户如何使用当前程序的帮助信息 输出:程序处理完成后展示给用户的结果信息 别名:命令和选项都应该支持别名,当命令和选项太长时用来简化输入 当然,CLI还包括程序退出码、错误等信息,不再一一列举。 cli框架简介 urfave/cli 是一个简单、快速且有趣的包,用于在 Go 中构建命令行应用程序。目标是使开发人员能够以富有表现力的方式编写快速且可分发的命令行应用程序。 目前最新支持的版本是 v2,这也是目前使用最广泛、功能强大的版本。 官方使用文档: https://cli.urfave.org/v2/getting-started/ 仓库地址: https://github.com/urfave/cli 接下来,我们将创建一个CLI应用并逐步完善它。 创建应用 创建一个cli目录,然后初始化go模块: 1$ mkdir cli 2$ cd cli 3$ go mod init 编辑 go.mod 文件,将模块名称改为 cli_demo,然后安装 urfave/cli: 1$ go get github.com/urfave/cli/v2 新建 main.go 文件作为程序的入口,编写代码如下: 1package main 2 3import ( 4 "fmt" 5 "github.com/urfave/cli/v2" 6 "os" 7) 8 9func main() { 10 cliApp := cli.NewApp() 11 cliApp.Name = "demo-cli" 12 cliApp.Usage = "cli usage demo" 13 cliApp.Version = "0.0.1" 14 15 err := cliApp.Run(os.Args) // app退出不会调用 os.Exit,所以默认退出代码都是0,可以通过 cli.Exit方法指定退出信息和退出码 16 if err != nil { 17 fmt.Printf("demo-cli execute error: %v\n", err) 18 os.Exit(-1) 19 } 20} 首先,我们使用 cli.NewApp() 创建 *cli.App 实例,然后分别设置了程序的名称、使用说明、版本,最后使用 cliApp.Run(os.Args) 方法运行程序,传入系统参数并处理错误信息。 ...

2023-07-16 · 5 min · 973 words · Hank

(译)Go1.13中处理错误

Damien Neil 和 Jonathan Amsterdam, 2019 年 10 月 17 日, 原文地址: https://go.dev/blog/go1.13-errors 介绍 在过去的十年中,Go 将 “错误作为值” 来处理 ,这对我们很有帮助。尽管标准库对错误的支持很少 —— 只有 errors.New 和 fmt.Errorf 函数,它们产生的错误只包含一条消息 —— 内置 error 接口允许 Go 程序员添加他们想要的任何信息。它所需要的只是一个实现 Error 方法的类型: 1type QueryError struct { 2 Query string 3 Err error 4} 5 6func (e *QueryError) Error() string { return e.Query + ": " + e.Err.Error() } 像这样的错误类型无处不在,它们存储的信息千差万别,从时间戳到文件名再到服务器地址。通常,该信息包括另一个较低级别的错误以提供额外的上下文。 一个错误包含另一个错误的模式在 Go 代码中非常普遍,经过 https://go.dev/issue/29934[广泛讨论] 后,Go 1.13 添加了对它的明确支持。这篇文章描述了提供该支持的标准库的新增内容:errors 包中的三个新函数,以及 fmt.Errorf. 在详细描述更改之前,让我们回顾一下在该语言的先前版本中如何检查和构造错误。 Go 1.13 之前的错误 检查错误 Go 错误是值。程序以几种方式根据这些值做出决策。最常见的是比较错误以 nil 查 看操作是否失败。 1if err != nil { 2 // something went wrong 3} 有时我们将错误与已知的 标记 值进行比较,以查看是否发生了特定错误。 1var ErrNotFound = errors.New("not found") 2 3if err == ErrNotFound { 4 // something wasn't found 5} 错误值可以是满足语言定义 error 接口的任何类型。程序可以使用类型断言或类型开关将错误值视为更具体的类型。 ...

2023-05-05 · 5 min · 874 words · Hank

(译)Go错误是值

Rob Pike,2015 年 1 月 12 日 ,原文地址: https://go.dev/blog/errors-are-values Go 程序员(尤其是刚接触该语言的程序员)讨论的一个共同点是如何处理错误,这些讨论最终往往都将回归于无数次出现的代码片段: 1if err != nil { 2 return err 3} 如上所示,但我们最近扫描了所有我们能找到的开源项目,发现这个片段每页或每两页只出现一次,比某些人认为的要少。尽管如此,许多程序员仍然认为必须键入如下代码来处理错误: 1if err != nil 一直以来,人们觉得一定有什么地方出了问题,而明显的目标就是 Go 本身。 这是不幸的、误导性的,而且很容易纠正。也许刚接触 Go 的程序员会问,“我该如何处理错误?”,然后学习这种模式,最后就此打住。在其他语言中,可能会使用 try-catch 块或其他类似机制来处理错误。因此,程序员认为,我在我的旧语言中可以使用 try-catch 时,但在 go 中我只能输入 if err != nil 来处理错误。随着时间的推移,Go 代码收集了许多这样的片段,结果感觉很笨拙。 不管这种解释是否成立,很明显这些 Go 程序员忽略了关于错误的一个基本观点: 错误是值。 可以对值进行编程,并且由于错误是值,因此可以对错误进行编程。 当然,涉及错误值的常见语句是测试它是否为 nil,但是错误值可以做无数其他事情,应用其中一些其他事情可以使您的程序更好,从而消除大部分样板代码,避免多次使用 if 语句检查每个错误。 一个简单示例是 bufio 包中的 https://go.dev/pkg/bufio/#Scanner[`Scanner`]。它的 https://go.dev/pkg/bufio/#Scanner.Scan[`Scan`] 方法执行底层 I/O,这当然可能导致错误。然而,该 Scan 方法根本不会暴露错误。相反,它返回一个布尔值和一个单独的方法,在扫描结束时运行,报告是否发生错误。客户端代码如下所示: 1scanner := bufio.NewScanner(input) 2for scanner.Scan() { 3 token := scanner.Text() 4 // process token 5} 6if err := scanner.Err(); err != nil { 7 // process the error 8} 当然,有一个 nil 检查错误,但它只出现并执行一次。假设将该 Scan 方法改为: ...

2023-04-30 · 2 min · 389 words · Hank

(译)Go模块:管理依赖项

原文地址: https://go.dev/doc/modules/managing-dependencies 当您的代码使用外部包时,这些包(作为模块分发)成为依赖项。随着时间的推移,您可能需要升级或更换它们。Go 提供了依赖管理工具,可帮助您在合并外部依赖项时确保 Go 应用程序的安全。 本文介绍如何执行一些任务来管理您代码中的依赖项,您可以使用 Go tools 执行其中的大部分操作。本主题还介绍了如何执行其他一些您可能会觉得有用的依赖相关任务。 // TODO 翻译后修改连接 相关阅读:: 如果您不熟悉模块和依赖,请查看 https://go.dev/doc/tutorial/getting-started[入门教程] 以获得简要介绍。 使用该 go 命令管理依赖项有助于确保您的需求保持一致,并且您的 go.mod 文件的内容是有效的。有关命令的参考,请参阅 https://go.dev/cmd/go/[go命令文档]。您还可以通过键入 go help 命令名称从命令行获取帮助,如go help mod tidy`. 使用 Go 命令更改依赖项时会编辑 go.mod 文件。有关文件内容的更多信息,请参阅 https://go.dev/doc/modules/gomod-ref[go.mod 文件参考文档]。 让您的编辑器或 IDE 能够感知 Go 模块可以让您更轻松地管理它们。有关支持 Go 的编辑器的更多信息,请参阅 https://go.dev/doc/editors.html[编辑器插件和 IDE]。 本主题不描述如何开发、发布和版本模块以供其他人使用。有关更多信息,请参阅 https://go.dev/doc/modules/[开发和发布模块]。 使用和管理依赖项的工作流程 您可以通过 Go tools 获取和使用有用的包。在 https://pkg.go.dev/[pkg.go.dev] 上,您可以搜索您觉得有用的包,然后使用 go 命令将这些包导入您自己的代码中以调用它们的功能。 下面列出了最常见的依赖管理步骤: 在 https://pkg.go.dev/[pkg.go.dev]上 «查找和导入包, 查找有用的包»。 «查找和导入包, 在代码中导入所需的包»。 将您的代码添加到模块以进行依赖跟踪(如果它不在模块中)。请参阅 «代码中启用依赖项跟踪, 启用依赖项跟踪» [«添加依赖项, 添加外部包作为依赖项»,以便您可以管理它们。 随着时间的推移,根据需要 «升级或降级依赖项, 升级或降级依赖版本»。 依赖项作为模块管理 在 Go 中,依赖项作为包含导入包的模块来管理。此过程由以下功能支持: ...

2023-04-19 · 4 min · 665 words · Hank

(译)Go 中的字符串、字节、符文和字符

原文地址: https://go.dev/blog/slices 作者: Rob Pike 日期: 2013 年 9 月 26 日 [[简介]] 简介 这篇文章讨论了 Go 中的字符串。起初,字符串对于一篇博文来说似乎太简单了,但要很好地使用它们,不仅需要了解它们的工作原理,还需要了解字节、字符和符文之间的区别,Unicode 和 UTF- 8、字符串和字符串字面量的区别,以及其他更细微的区别。 处理该话题的一种方法首先是回答这个问题:“当我在位置 n 检索 Go 字符串时,为什么我没有得到第 n 个字符?” 正如您将看到的,这个问题引导我们了解有关文本在现代世界中如何工作的许多细节。 什么是字符串? 让我们从一些基础知识开始。 在 Go 中,字符串实际上是只读的字节切片。如果您完全不确定字节切片是什么或它是如何工作的,请阅读 数组、切片和字符串 一文。 重要的是首先要明确一个字符串包含_任意_多个字节,不论字符串是否包含 Unicode 文本、UTF-8 文本或任何其他预定义格式。就字符串的内容而言,它完全等价于一个字节切片([]byte)。 下边是一个字符串(稍后详述),它使用 \xNN 符号来定义一个字符串常量,其中包含一些特殊的字节值(字节的取值范围从十六进制值 00 到 FF)。 const sample = "\xbd\xb2\x3d\xbc\x20\xe2\x8c\x98" [[打印字符串]] 打印字符串 由于上边我们的示例字符串 sample 中的某些字节不是有效的 ASCII,甚至不是有效的 UTF-8,所以直接打印字符串会产生奇怪的输出。简单的打印语句如下: 1fmt.Println(sample) 产生这种乱码(其确切外观因环境而异)输出: ��=� ⌘ 为了找出 sample 字符串底层到底是什么,我们需要把它拆开检查一下。有几种方法可以做到这一点。最明显的是循环其内容并单独提取字节,如以下for循环所示: 1for i := 0; i < len(sample); i++ { 2 fmt.Printf("%x ", sample[i]) // 输出为十六进制格式 3} 正如前文所述,索引字符串访问的是单个字节,而不是单个字符,我们将在下面详细阐述该主题。现在,让我们只使用字节,这是逐字节循环的十六进制输出: bd b2 3d bc 20 e2 8c 98 请注意各个字节如何与定义字符串的十六进制转义匹配。 ...

2023-03-25 · 4 min · 728 words · Hank

(译)数组、切片和字符串 - “append” 原理

原文地址: https://go.dev/blog/slices 作者: Rob Pike 日期: 2013 年 9 月 26 日 介绍 过程编程语言最常见的特征之一是数组的概念。数组看起来很简单,但在将它们添加到语言时必须回答许多问题,例如: 固定尺寸还是可变尺寸? 大小是类型的一部分吗? 多维数组是什么样的? 空数组有意义吗? 这些问题的答案会影响数组是否只是语言的一个特性还是其设计的核心部分。 在 Go 的早期开发中,在设计感觉正确之前,花了大约一年的时间来确定这些问题的答案。关键步骤是引入 slices,它建立在固定大小的 数组 之上,以提供灵活、可扩展的数据结构。然而,直到今天,刚接触 Go 的程序员经常对切片的工作方式感到困惑,也许是因为其他语言的经验影响了他们的思维。 在这篇文章中,我们将尝试消除混淆。我们将通过构建片段来解释 append 内置函数是如何工作的,以及为什么它会以这种方式工作。 数组 数组是 Go 中的一个重要构建块,但就像建筑物的基础一样,它们通常隐藏在更可见的组件之下。在我们继续讨论更有趣、更强大、更突出的切片概念之前,我们必须简单地讨论一下它们。 数组在 Go 程序中并不常见,因为数组的大小是其类型的一部分,这限制了它的表达能力。 以下代码: 1var buffer [256]byte 声明了一个数组变量 buffer ,[256]byte 表示它持有的数据类型为 byte,长度为 256。如果想声明 512 个字节的数组可以这样: [512]byte。 与数组关联的数据就是 数组中的元素。上边声明的数组缓冲区在内存中看起来像这样: 1buffer: byte byte byte ... 256 times ... byte byte byte 也就是说,该变量只保存 256 个字节的数据,仅此而已。我们可以使用熟悉的索引语法 buffer[0]、buffer[1] 到 buffer[255] 来访问它的元素。(索引范围 0 到 255 涵盖 256 个元素)尝试使用超出此范围的索引值访问 buffer 会使程序崩溃。 内置函数 len 会返回数组、切片或其他一些数据类型中的元素数量。在我们的示例中,len(buffer) 返回固定值 256。 ...

2023-02-09 · 8 min · 1620 words · Hank

(译)进入Go模糊测试的世界

原文地址: https://go.dev/doc/tutorial/fuzz[] 本教程介绍了 Go 中模糊测试的基础知识。模糊测试会针对您的测试准备一些随机数据然后运行测试时使用它们,以尝试找出漏洞或导致崩溃的输入。可以通过模糊测试发现的一些漏洞示例包括 SQL 注入、缓冲区溢出、拒绝服务和跨站点脚本攻击(XSS)。 在本教程中,您将为一个简单的函数编写一个模糊测试,运行 go 命令,并调试和修复代码中的问题。 有关本教程中术语的帮助,请参阅 “词汇表”。 您将逐步完成以下部分: «为您的代码创建一个文件夹» «添加代码进行测试» «添加单元测试» «添加模糊测试» «修复无效字符串错误» «修复双反错误» «结论» 注意 更多 Go 教程,请参阅 https://go.dev/doc/tutorial/index.html[教程]。 Go fuzzing 当前支持 https://go.dev/security/fuzz/#requirements[Go Fuzzing 文档] 中列出的内置类型的子集,并支持将来添加的更多内置类型。 先决条件 Go 1.18 或更高版本的安装。 有关安装说明,请参阅 https://go.dev/doc/install[安装 Go]。 用于编辑代码的工具。 您拥有的任何文本编辑器都可以正常工作。 一个命令终端。 Go 在 Linux 和 Mac 上的任何终端以及 Windows 中的 PowerShell 或 cmd 上都能很好地工作。 支持模糊测试的环境。 目前仅在 AMD64 和 ARM64 架构上使用覆盖检测进行模糊测试。 [[为您的代码创建一个文件夹]] 为您的代码创建一个文件夹 首先,为您要编写的代码创建一个文件夹。 1、 打开命令提示符并切换到您的主目录。 在 Linux 或 Mac 上: ...

2022-12-19 · 9 min · 1893 words · Hank

(译)初始Go模糊测试

原文地址: https://go.dev/security/fuzz/ 从 Go 1.18 开始,Go 在其标准工具链中支持模糊测试。Native Go 模糊测试受 https://google.github.io/oss-fuzz/getting-started/new-project-guide/go-lang/#native-go-fuzzing-support[OSS-Fuzz 支持]。 Go模糊测试详细教程见:进入Go模糊测试的世界 一文。 概述 Fuzzing 是一种自动化测试,它不断地操纵程序的输入以查找错误。Go fuzzing 使用覆盖率指导来智能地不断重复执行模糊测试的代码,以发现并向用户报告问题。由于它可以覆盖人类经常错过的边缘情况,因此模糊测试对于发现安全漏洞特别有价值。 下面是一个 «fuzztarget, 模糊测试» 的例子,突出了它的主要组成部分。 上图显示整体模糊测试的示例代码,其中包含一个模糊目标( «corpus, fuzz target» )。 在模糊目标之前调用 f.Add 添加种子语料库,模糊目标的参数高亮显示为fuzzing参数。 编写模糊测试 要求 以下是模糊测试必须遵循的规则。 模糊测试必须是一个形如 FuzzXxx 的函数,以 Fuzz 作为前缀,它只接受一个 *testing.F 参数并且没有返回值。 模糊测试必须在 *_test.go 文件中才能运行。 调用 https://pkg.go.dev/testing#F.Fuzz[(*testing.F).Fuzz] 方法时的匿名函数参数称之为 «fuzztarget, 模糊目标»,形如 func(t *testing.T, xxx)*, 它必须是一个函数,接受一个 *testing.T 作为第一个参数,其他后续参数称为模糊参数,且该函数没有返回值。 每个模糊测试必须只有一个模糊目标。 所有 «seedcoprus, 种子语料库» 条目必须具有与 «fuzzing, 模糊参数» 相同的类型,并且顺序相同。这适用于调用 https://pkg.go.dev/testing#F.Add[(*testing.F).Add] 添加的种子语料库和 testdata/fuzz 目录中已有的语料库文件。 模糊测试参数只能是以下类型: string, []byte int, int8, int16, int32/rune, int64 uint, uint8/byte, uint16, uint32, uint64 float32, float64 bool 建议 以下建议将帮助您充分利用模糊测试。 ...

2022-10-26 · 3 min · 435 words · Hank

GoLang教程——常量

golang 中常量设计摒弃了 C 语言中常量被诟病的问题,比如类型安全问题、定义和使用复杂、编译器替换字面值等,而是进行了简化。在 go 中,常量一旦声明则在编译期其值就已知且不可更改。 常量的定义 常量的定义很简单,使用 const 关键字声明即可: 1// 单行声明常量 2const c1 = 1 3const c2 = -100 4const c3 = "hello" 与变量一样,同样可以组合以简化声明: 1const ( 2 c4 = 3.14 3 c5 = 1.2 + 12i // 复数 4) 但是需要注意,组合声明时后续的常量如果没有赋值则会重复最近一个非空表达式的值,例如: 1const ( 2 m = 1 // 1 3 n // 1 4 k // 1 5 l = m + 1 // 2 6 i // 2 7) 8fmt.Println(m, n, k, l, i) 有类型常量和无类型常量 上边的示例中声明常量时并没有指明具体类型,这是 go 可以的设计。go 中常量可以分为有类型常量和无类型常量,大多数情况下我们只需要使用无类型常量即可。 有类型常量的问题 go 是一门强类型语言,即使变量的基础类型相同,也是不能直接运算的,看下边的例子: 1func add() { 2 var x int = 1 3 var y int32 = 2 4 // fmt.Println(x + y) // 编译失败:invalid operation: x + y (mismatched types int and int32) 5 fmt.Println(x + int(y)) // <1> 6} 7 8type MyInt int 9 10func add2() { 11 var x int = 1 12 var y MyInt = 2 13 // fmt.Println(x + y) // 编译失败 14 fmt.Println(MyInt(x) + y) // <2> 15} <1> int 和 int32 是不同的类型,运算时需要类型转换 <2> 即使 MyInt 的基础类型是 int,但是它们仍然是不同的类型,需要强制类型转换 ...

2022-09-10 · 4 min · 641 words · Hank

GoLang教程——变量

本质上,变量是一个持有一类值的数据存储空间的命名,我们声明变量其实就是在申请存储空间,之后引用变量其实就是在使用这块存储空间中的值。GoLang是一门强类型语言,每一个声明的变量都对应着一种类型,但是声明变量的方式却有多种。 变量的定义 https://go.dev/ref/spec#Variables[Go语言规范]中对变量的定义如下: [NOTE] 变量是保存着值的一块存储空间,存储什么样的值由变量的类型决定。 变量的概念在各个语言中都有,本质上变量就是为一块存储空间取一个名字,以便开发者后续引用这个名字来使用存储空间中存储的值。至于存储什么样的值,这必须有变量的类型来决定,所以Go、Java这种强类型语言在变量声明时必须指定变量类型。 变量的声明 上一篇中提到:变量声明过后,必须被使用,否则编译错误。Go中变量的声明有两种方式: 使用 var 声明一个变量,适用于大多数情况,可以不用赋初始值时 使用 := 声明变量并初始化它,适用于需要为变量直接赋初始值时,比如直接声明变量接收方法的返回值 第一种,使用 var 关键字声明变量: 1var v0 rune // 声明字符类型的变量 v0 2var v1 int // 声明整型变量 v1 3var v2 string // 声明字符串类型的变量 v2 4v0 = 'A' // 给变量赋初始值 这种方式可以先声明变量然后赋值,也可以直接在声明时赋值,此时可以省略类型,编译器会自动推导出类型: 1var i1 = 1 2fmt.Printf("i1 = %d\n", i1) 在声明多个变量时,可以使用如下简写的语法: 1var ( 2 v3 [2]int // 数组 3 v4 []int // 数组切片 4 v5 struct{ i int } // 结构体 5 v6 *int // 指针 6 v7 map[string]int // map 7) 第二种方式,使用 := 声明变量,此时必须初始化变量: 1v9 := 10 // 声明变量 v9 并初始化,自动推导类型 2fmt.Printf("v9 = %d, type: %T \n", v9, v9) 3 4a, b := func1() // 直接声明变量并接收方法的返回值 5fmt.Println(a, b) 这种方式可以简化变量的声明,适用于需要声明变量同时赋值的情况,比如上边示例代码中的直接声明变量接收方法的返回值。 [IMPORTANT] 使用 var 声明的变量,不能再使用 := 来赋值,因为两者都是在 声明一个新的变量,会导致重复声明。 ...

2022-08-02 · 2 min · 339 words · Hank

GoLang教程——代码风格

Go作为编程语言中的后起之秀,在语言规范上吸收了诸多语言的优点,并形成了自身独特的语言风格。本文探讨一下Go语言的代码风格。 main方法 main 方法同样是go程序的入门函数,但是其定义非常简单,比如上一篇中的: 1func main() { 2 fmt.Println("Hello, world!") 3} 直接用 func 关键字定义一个名为 main 的方法即可,没有任何参数。 那么,如何获取传递给 main 的参数呢,可以使用 os.Args: 1func main() { 2 fmt.Println("hello go!") 3 for i, arg := range os.Args { 4 fmt.Println(i, "=", arg) 5 } 6} os.Args 获取命令行传递的参数,第一个始终为执行程序的名称,一个示例执行结果如下: 1$ go run main_func.go a b 2hello go! 30 = /var/folders/8b/y3pklwbs1wj_cq7hm_yjwgpr0000gn/T/go-build3458283994/b001/exe/main_func 41 = a 52 = b 当然,通常情况不会使用 os.Args 来解析命令行参数,而是使用 go 标准库的 flag 包。 go的关键字 go的内置关键字有25个: 1break default func interface select 2case defer go map struct 3chan else goto package switch 4const fallthrough if range type 5continue for import return var 这25个关键字不能作为标识符使用。 ...

2022-07-30 · 3 min · 586 words · Hank

GoLang教程——初识Go语言

https://golang.google.cn/[Go] 语言是最近几年发展最快、最火热的编程语言之一,由 google 公司出品,其学习成本低、天生支持高并发、语法简洁高效、强大的标准库以及日益丰富的生态特细等特性使得其非常适合开发后端服务程序,并逐渐挑战着 Java 在服务端编程语言中的领导地位。 Go语言的诞生 Go 语言的前身被认为是一种名为 Limbo 的编程语言,它是由 Unix 之父、C语言之父 肯·汤普逊(Ken Thompson) 和 丹尼斯·里奇(https://en.wikipedia.org/wiki/Dennis_Ritchie[Dennis Ritchie]) 这两位计算机灵魂人物领衔并在一个名为 Plan 9 的的操作系统研究项目中发明的。 Go语言起源于2007年,当时还是身在 Google 的 Plan 9 项目原班人马在业余时间编写完成的,后来 Google 意识到了 Go 的巨大潜力并全力支持这个项目。Go 在2009年11月正式对外开放,并在2012年3月28日发布了第一个正式版本。 Go 语言的主要开发者有 肯·汤普逊(http://en.wikipedia.org/wiki/Ken_Thompson[Ken Thompson])、罗布·派克(http://en.wikipedia.org/wiki/Rob_Pike[Rob Pike])、罗伯特·格里泽默(https://en.wikipedia.org/wiki/Robert_Griesemer[Robert Griesemer])等,每一位都是赫赫有名的大师级人物: 肯·汤普逊:与丹尼斯·里奇在贝尔实验室发明了Unix操作系统,以及用于该系统的 C编程语言 .Ken Thompson (left) and Dennis Ritchie (right) 罗布·派克:Unix小组成员,参与了Unix后续的Plan 9和Inferno操作系统,同时也是Limbo语言和Go语言共同的发明者 罗伯特·格里泽默:协助制作Java的HotSpot编译器和Chrome浏览器的JavaScript引擎V8,Go语言的共同创造者 Go的特点 [quota,Rob Pike] Go 始于 2007 年 9 月,当时 Robert Griesemer、Ken Thompson 和我开始讨论一种新语言,以解决我们和 Google 的同事在日常工作中面临的工程挑战。 ...

2022-07-21 · 3 min · 452 words · Hank

(译)Go并发模式——Context

原文地址: https://go.dev/blog/context[] 作者:Sameer Ajmani 时间:29 July 2014 简介 footnote:ddns[这篇文章是go官网博客中的一篇,尽管文章比较早,但是较详细的描述了 context 出现的原因、使用方式,仍然值得一读。] 在 Go 服务器中,每个传入的请求都在其自己的 goroutine 中处理。请求处理程序通常会启动额外的 goroutine 来访问数据库和 RPC 服务等后端。处理请求的一组 goroutine 通常需要访问特定于请求的值,例如最终用户的身份、授权令牌和请求的截止日期。当请求被取消或超时时,所有处理该请求的 goroutines 都应该快速退出,以便系统可以回收它们正在使用的任何资源。 在 Google,我们开发了一个 context 包,可以轻松地将请求范围的值、取消信号和截止日期等跨 API 边界传递给正在处理请求的所有 goroutine。 https://go.dev/pkg/context[该软件包] 作为context公开可用 。本文介绍了如何使用该包并提供了一个完整的工作示例。 Context context 包的核心是 Context 类型: 1type Context interface { // <1> 2 Done() <-chan struct{} // <2> 3 4 Err() error // <3> 5 6 Deadline() (deadline time.Time, ok bool) // <4> 7 8 Value(key interface{}) interface{} // <5> 9} <1> Context 携带截止日期、取消信号和请求范围的跨越 API 边界的值,多个 goroutine 同时使用它的方法是安全的。 <2> Done 返回一个在此 Context 取消或超时时的通道(chan) <3> Err 错误信息说明 context 为什么被取消, 在 Done 返回的 chan 被关闭之后获取 <4> Deadline 返回 Context 被取消的时间 <5> Value 返回参数 key 关联的值,没有则返回 nil ...

2022-07-19 · 5 min · 1012 words · Hank