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 · 265 words · Hank

Go 项目工程结构最佳实践

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

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

环境变量与配置管理

环境变量与配置管理 同一份代码需要在开发、测试、生产等不同环境运行。配置管理让我们能够灵活切换环境,而不需要修改代码。 1. 环境变量基础 1.1 读取环境变量 import ( "fmt" "os" ) func main() { // 读取环境变量 dbHost := os.Getenv("DB_HOST") if dbHost == "" { dbHost = "localhost" // 默认值 } fmt.Println("DB Host:", dbHost) // 检查环境变量是否存在 port, exists := os.LookupEnv("PORT") if !exists { port = "8080" } } 1.2 设置环境变量 // 在程序中设置(仅影响当前进程) os.Setenv("API_KEY", "secret123") // 在 shell 中设置 // export DB_HOST=localhost // export DB_PORT=3306 2. godotenv:.env 文件 2.1 安装 go get -u github.com/joho/godotenv 2.2 使用 创建 .env 文件: DB_HOST=localhost DB_PORT=3306 DB_USER=root DB_PASSWORD=secret API_KEY=your-api-key DEBUG=true 加载配置: import ( "github.com/joho/godotenv" "log" "os" ) func main() { // 加载 .env 文件 err := godotenv.Load() if err != nil { log.Println("No .env file found") } dbHost := os.Getenv("DB_HOST") dbPort := os.Getenv("DB_PORT") fmt.Printf("Connecting to %s:%s\n", dbHost, dbPort) } 2.3 多环境配置 func main() { env := os.Getenv("GO_ENV") if env == "" { env = "development" } // 根据环境加载不同的配置文件 godotenv.Load(".env." + env) // .env.development // .env.production // .env.test } 3. Viper:强大的配置库 3.1 安装 go get -u github.com/spf13/viper 3.2 基础使用 创建 config.yaml: ...

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

模糊测试入门 (Fuzzing)

单元测试只能测试你想到的情况,而模糊测试能帮你发现你没想到的边界情况。 1. 什么是模糊测试? 模糊测试 (Fuzzing) 是一种自动化测试技术,通过生成大量随机或半随机的输入数据来测试程序,寻找崩溃、panic、死循环等异常。 传统测试 vs 模糊测试: // 传统单元测试:测试已知的输入 func TestAdd(t *testing.T) { if Add(2, 3) != 5 { t.Error("2 + 3 should be 5") } } // 模糊测试:测试大量随机输入 func FuzzAdd(f *testing.F) { f.Fuzz(func(t *testing.T, a, b int) { result := Add(a, b) // 检查属性而不是具体值 if result < a && result < b { t.Errorf("Add(%d, %d) = %d, should be >= both", a, b, result) } }) } 2. 编写 Fuzz 测试 2.1 基础示例 假设我们有一个解析 URL 的函数: // url.go package myurl import ( "fmt" "strings" ) func ParseURL(rawURL string) (string, error) { if !strings.HasPrefix(rawURL, "http://") && !strings.HasPrefix(rawURL, "https://") { return "", fmt.Errorf("invalid protocol") } return rawURL, nil } 编写 Fuzz 测试: // url_test.go package myurl import ( "testing" ) func FuzzParseURL(f *testing.F) { // 添加种子语料库(可选) f.Add("http://example.com") f.Add("https://google.com") f.Add("ftp://invalid.com") // Fuzz 函数 f.Fuzz(func(t *testing.T, rawURL string) { result, err := ParseURL(rawURL) // 检查:不应该 panic // 检查:如果没有错误,结果应该等于输入 if err == nil && result != rawURL { t.Errorf("ParseURL(%q) = %q, want %q", rawURL, result, rawURL) } }) } 2.2 运行 Fuzz 测试 # 运行模糊测试(会一直运行直到发现问题或手动停止) go test -fuzz=FuzzParseURL # 限制运行时间 go test -fuzz=FuzzParseURL -fuzztime=30s # 限制迭代次数 go test -fuzz=FuzzParseURL -fuzztime=10000x 2.3 查看结果 如果发现问题,Go 会自动保存导致崩溃的输入: ...

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

日志管理:从 log 到 zap

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

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

文件与 IO 操作实战

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

2025-06-19 · 3 min · 606 words · Hank

Go 模块管理入门

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

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

GoLang教程——项目实战示例

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

2025-05-25 · 1 min · 202 words · Hank

GoLang教程——泛型编程入门

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

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

GoLang教程——Context上下文实战

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

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

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

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

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

GoLang教程——标准库精选

Go 被誉为“自带电池”(Batteries Included)的语言,因为其标准库极其丰富,涵盖了开发中 80% 的需求。本章精选了几个最常用、最基础的标准库,展示它们的常规用法。 示例代码 package main import ( "encoding/json" "fmt" "strings" "time" ) type User struct { ID int `json:"id"` Name string `json:"name"` // json:"-" 表示在 JSON 中忽略该字段 Email string `json:"-"` } func main() { // 1. fmt - 格式化 age := 18 // Sprintf 返回字符串,Printf 直接打印 msg := fmt.Sprintf("I am %d years old", age) fmt.Println(msg) // 2. strings - 字符串处理 str := " hello world " fmt.Println(strings.TrimSpace(str)) // 去除首尾空格 fmt.Println(strings.Contains(str, "world")) // true fmt.Println(strings.Join([]string{"a", "b"}, "-")) // a-b // 3. time - 时间处理 now := time.Now() // Go 的独特日期格式化模板:2006-01-02 15:04:05 fmt.Println(now.Format("2006-01-02 15:04:05")) // 时间计算 later := now.Add(time.Hour) fmt.Println(later.Sub(now)) // 1h0m0s // 4. encoding/json - JSON 编解码 user := User{ID: 1, Name: "Hank", Email: "admin@hankmo.com"} // 序列化 Struct -> JSON jsonData, _ := json.Marshal(user) fmt.Println(string(jsonData)) // {"id":1,"name":"Hank"} // 反序列化 JSON -> Struct var u2 User json.Unmarshal(jsonData, &u2) fmt.Println(u2.Name) } ...

2025-01-12 · 1 min · 196 words · Hank

GoLang教程——并发进阶

在掌握了 Goroutine 和 Channel 的基础知识后,我们需要了解 Go 标准库 sync 包提供的各种同步工具。这些工具能帮助我们更好地控制并发程序的执行流程,避免数据竞争,提高程序的可靠性。 sync.WaitGroup:等待一组 Goroutine 完成 基本用法 WaitGroup 用于等待一组 Goroutine 完成。它有三个方法: Add(delta int):增加计数器 Done():减少计数器(相当于 Add(-1)) Wait():阻塞直到计数器为 0 package main import ( "fmt" "sync" "time" ) func worker(id int, wg *sync.WaitGroup) { defer wg.Done() // 函数结束时调用 Done() fmt.Printf("Worker %d starting\n", id) time.Sleep(time.Second) fmt.Printf("Worker %d done\n", id) } func main() { var wg sync.WaitGroup // 启动 5 个 worker for i := 1; i <= 5; i++ { wg.Add(1) // 每启动一个 goroutine,计数器 +1 go worker(i, &wg) } wg.Wait() // 等待所有 worker 完成 fmt.Println("All workers completed") } ...

2024-12-05 · 4 min · 697 words · Hank

GoLang教程——并发基础

Go 语言之所以在云原生时代大放异彩,很大程度上归功于其原生支持的高效并发模型。Go 采用了 Goroutine(协程)和 Channel(通道)来实现并发,这种模式被称为 CSP(通信顺序进程)。本章将带你入门 Go 的并发世界。 示例代码 package main import ( "fmt" "time" ) // 一个模拟耗时任务的函数 func worker(id int, jobs <-chan int, results chan<- int) { for j := range jobs { fmt.Printf("Worker %d started job %d\n", id, j) time.Sleep(time.Second) // 模拟耗时 1 秒 fmt.Printf("Worker %d finished job %d\n", id, j) results <- j * 2 // 将结果发送回 results 通道 } } func main() { // 创建两个通道:任务通道和结果通道 // 设置缓冲区大小为 10,防止阻塞 jobs := make(chan int, 10) results := make(chan int, 10) // 启动 3 个 Goroutine (Worker) // 它们会并发地从 jobs 通道抢任务做 for w := 1; w <= 3; w++ { go worker(w, jobs, results) } // 发送 5 个任务 for j := 1; j <= 5; j++ { jobs <- j } close(jobs) // 关闭任务通道,告知 Worker 没有新任务了 // 接收并打印结果 for a := 1; a <= 5; a++ { <-results } fmt.Println("All jobs finished!") } ...

2024-11-19 · 2 min · 366 words · Hank

GoLang教程——错误处理

Go 对错误处理的态度非常直白:错误就是一种值(Values),不是异常(Exceptions)。我们通过函数返回值来传递错误,并显式地检查它们。只有在真正的不可恢复情况(如数组越界)下,才会使用 panic。 示例代码 package main import ( "errors" "fmt" ) // 定义一个除法函数,返回 result 和 error func divide(a, b int) (int, error) { if b == 0 { // 使用 errors.New 创建一个简单的错误对象 return 0, errors.New("cannot divide by zero") } return a / b, nil } // 演示 panic 和 recover func safeCall() { // defer 必须在 panic 发生前定义 defer func() { // recover() 捕获 panic,如果返回值不为 nil,说明发生了 panic if r := recover(); r != nil { fmt.Println("Recovered from panic:", r) } }() panic("Something went wrong terribly!") fmt.Println("This line will not execute") } func main() { // 1. 标准错误处理 res, err := divide(10, 0) if err != nil { fmt.Println("Error:", err) } else { fmt.Println("Result:", res) } // 2. 演示从 panic 中恢复 fmt.Println("Starting safeCall...") safeCall() fmt.Println("Program continues...") } ...

2024-10-29 · 1 min · 184 words · Hank

GoLang教程——结构体与接口

Go 语言没有传统的“类”和“继承”概念,而是通过 结构体 (Struct) 来封装数据,通过 接口 (Interface) 来定义行为。这种组合式设计(Composition over Inheritance)让代码更加灵活和解耦。 示例代码 package main import "fmt" // 定义一个接口:只要实现了 Speak 方法的类型,都满足这个接口 type Speaker interface { Speak() string } // 定义结构体 Dog type Dog struct { Name string } // Dog 实现 Speaker 接口 // 注意:Go 中没有 implements 关键字,这是隐式实现的 func (d Dog) Speak() string { return "Woof!" } // 定义结构体 Cat type Cat struct { Name string } // Cat 实现 Speaker 接口 func (c Cat) Speak() string { return "Meow!" } // 多态演示:接收任何 Speaker func introduce(s Speaker) { fmt.Println(s.Speak()) } func main() { d := Dog{Name: "Buddy"} c := Cat{Name: "Kitty"} introduce(d) // Woof! introduce(c) // Meow! // 接口类型断言 var s Speaker = d if dog, ok := s.(Dog); ok { fmt.Printf("It's a dog named %s\n", dog.Name) } } ...

2024-09-15 · 1 min · 200 words · Hank

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

在处理数据集合时,Go 提供了三种主要方式:数组(Array)、切片(Slice)和映射(Map)。其中,切片和映射是日常开发中使用频率最高的数据结构。本章将介绍它们的区别与用法。 示例代码 package main import "fmt" func main() { // 1. 数组 (Array) // 长度是类型的一部分,固定不可变 var arr [3]int = [3]int{10, 20, 30} fmt.Println("Array:", arr) // 2. 切片 (Slice) // 动态数组,引用类型 slice := []int{1, 2, 3, 4, 5} // 操作:切取子集 [start:end] (左闭右开) subSlice := slice[1:3] // 包含索引 1, 2 的元素 -> {2, 3} fmt.Println("SubSlice:", subSlice) // 操作:追加元素 // 当容量不足时,append 会自动扩容 slice = append(slice, 6) fmt.Println("Appended Slice:", slice) // 3. 映射 (Map) // 键值对集合,类似 Python 的 dict 或 Java 的 HashMap scores := make(map[string]int) scores["Alice"] = 95 scores["Bob"] = 88 // 检查键是否存在 // val 是值,ok 是布尔值(存在为 true) if score, ok := scores["Alice"]; ok { fmt.Printf("Alice's score is %d\n", score) } // 删除键值对 delete(scores, "Bob") // 遍历 Map (注意:遍历顺序是随机的) for name, score := range scores { fmt.Printf("%s: %d\n", name, score) } } ...

2024-08-22 · 1 min · 191 words · Hank

GoLang教程——控制结构

Go 语言的控制结构非常精简。它没有 while 或 do-while,只有一个强大的 for 循环。同时,switch 更加智能,if 支持初始化语句。本章还将介绍 Go 独有的资源管理神器 —— defer。 示例代码 package main import "fmt" func main() { // 1. if 语句 x := 10 // 条件判断不需要小括号 () if x > 5 { fmt.Println("x is large") } // if 支持初始化语句:先执行初始化,再判断 if y := x * 2; y > 15 { fmt.Println("y is", y) // y 的作用域仅限于 if 块 } // 2. switch 语句 day := "Mon" switch day { case "Mon": fmt.Println("Start of week") // Go 默认不需要 break,自动终止 case "Fri": fmt.Println("Weekend is coming") default: fmt.Println("Other day") } // 3. for 循环:Go 唯一的循环结构 // 形式一:类似于 C/Java 的 for for i := 0; i < 3; i++ { fmt.Print(i, " ") } fmt.Println() // 形式二:类似于 while count := 3 for count > 0 { fmt.Print(count, " ") count-- } fmt.Println() // 4. defer 延迟执行 // 常用于资源释放,函数返回前才会执行 defer fmt.Println("Exiting main function...") fmt.Println("Doing some work...") } ...

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

GoLang教程——函数与方法

函数是 Go 程序的基本构建单元。Go 语言中的函数非常灵活,支持多返回值,并且可以为特定的类型定义方法(类似于面向对象中的成员函数)。本章将带你快速掌握函数的定义、调用以及方法的绑定。 示例代码 先看一个完整的可运行示例,展示了普通函数、多返回值以及方法的用法。 package main import "fmt" // add 是一个普通函数,接收两个 int 参数,返回一个 int 结果 func add(a, b int) int { return a + b } // swap 演示多返回值,交换输入的两个字符串 func swap(x, y string) (string, string) { return y, x } // 定义一个简单的结构体 User type User struct { Name string Age int } // SayHello 是绑定到 User 结构体的方法 // (u User) 称为接收者(Receiver) func (u User) SayHello() { fmt.Printf("Hello, my name is %s and I am %d years old.\n", u.Name, u.Age) } // 只有指针接收者才能修改结构体内部的值 func (u *User) Grow() { u.Age++ } func main() { // 1. 调用普通函数 sum := add(10, 20) fmt.Println("10 + 20 =", sum) // 2. 调用多返回值函数 a, b := swap("hello", "world") fmt.Println(a, b) // world hello // 3. 调用方法 user := User{Name: "Hank", Age: 18} user.SayHello() // 调用指针接收者方法修改状态 user.Grow() fmt.Printf("Age after grow: %d\n", user.Age) // 19 } ...

2024-07-17 · 1 min · 208 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 的代码: type Group struct { mu sync.Mutex // protects m m map[string]*call // lazily initialized } Group 表示处理相同数据的一系列工作,这些工作存储到一个 map[string]*call 的结构中,为了保证并发安全,Group 内部持有 sync.Mutex 锁用来保护这个 map 的读写。 Group 有一个非常重要的两个方法 Do 和 DoChan, 在上一篇已经介绍过了。 再来回顾一下 Do 方法的定义: func (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 · 657 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 方法表示执行请求抑制,其定义如下: func (g *Group) Do(key string, fn func() (interface{}, error)) (v interface{}, err error, shared bool) { // ... } 首先,它需要在 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 · 2 min · 408 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 文件,它包含以下几个结构体: type Message string type Greeter struct { Msg Message } type Event struct { Greeter Greeter } Message 表示消息,Greeter 表示问候者,它需要一个 Message,Event 表示一个事件,它用来触发问候。 现在,我们需要增加创建这些结构体的方法: func NewMessage(name string) Message { return Message(fmt.Sprintf("hello, %s!", name)) } func NewGreeter(msg Message) Greeter { return Greeter{Msg: msg} } func NewEvent(g Greeter) Event { return Event{Greeter: g} } func (g Greeter) Greet() Message { return g.Msg } func (e Event) Start() { msg := e.Greeter.Greet() fmt.Println(msg) } 通过 NewMessage 创建消息,通过 NewGreeter 创建一个问候者,通过 NewEvent 创建事件,然后就可以调用 Start 方法来发起问候了,其实底层最终调用的是 Greeter 的 Greet 方法。 ...

2023-08-06 · 3 min · 608 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模块: $ mkdir cli $ cd cli $ go mod init 编辑 go.mod 文件,将模块名称改为 cli_demo,然后安装 urfave/cli: $ go get github.com/urfave/cli/v2 新建 main.go 文件作为程序的入口,编写代码如下: package main import ( "fmt" "github.com/urfave/cli/v2" "os" ) func main() { cliApp := cli.NewApp() cliApp.Name = "demo-cli" cliApp.Usage = "cli usage demo" cliApp.Version = "0.0.1" err := cliApp.Run(os.Args) // app退出不会调用 os.Exit,所以默认退出代码都是0,可以通过 cli.Exit方法指定退出信息和退出码 if err != nil { fmt.Printf("demo-cli execute error: %v\n", err) os.Exit(-1) } } 首先,我们使用 cli.NewApp() 创建 *cli.App 实例,然后分别设置了程序的名称、使用说明、版本,最后使用 cliApp.Run(os.Args) 方法运行程序,传入系统参数并处理错误信息。 ...

2023-07-16 · 4 min · 831 words · Hank

(译)Go1.13中处理错误

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

2023-05-05 · 4 min · 828 words · Hank

(译)Go错误是值

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

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

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

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

2023-04-19 · 3 min · 636 words · Hank

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

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

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

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

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

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

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

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

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

(译)初始Go模糊测试

原文地址: https://go.dev/security/fuzz/ 从 Go 1.18 开始,Go 在其标准工具链中支持模糊测试。Native Go 模糊测试受 OSS-Fuzz 支持。 Go模糊测试详细教程见:进入Go模糊测试的世界 一文。 1. 概述 Fuzzing 是一种自动化测试,它不断地操纵程序的输入以查找错误。Go fuzzing 使用覆盖率指导来智能地不断重复执行模糊测试的代码,以发现并向用户报告问题。由于它可以覆盖人类经常错过的边缘情况,因此模糊测试对于发现安全漏洞特别有价值。 下面是一个 模糊测试 的例子,突出了它的主要组成部分。 上图显示整体模糊测试的示例代码,其中包含一个模糊目标( fuzz target )。 在模糊目标之前调用 f.Add 添加种子语料库,模糊目标的参数高亮显示为fuzzing参数。 2. 编写模糊测试 2.1. 要求 以下是模糊测试必须遵循的规则。 模糊测试必须是一个形如 FuzzXxx 的函数,以 Fuzz 作为前缀,它只接受一个 *testing.F 参数并且没有返回值。 模糊测试必须在 *_test.go 文件中才能运行。 调用 (testing.F).Fuzz 方法时的匿名函数参数称之为 模糊目标,形如 func(t *testing.T, xxx), 它必须是一个函数,接受一个 *testing.T 作为第一个参数,其他后续参数称为模糊参数,且该函数没有返回值。 每个模糊测试必须只有一个模糊目标。 所有 种子语料库 条目必须具有与 模糊参数 相同的类型,并且顺序相同。这适用于调用 (*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 · 2 min · 426 words · Hank

GoLang教程——常量

golang 中常量设计摒弃了 C 语言中常量被诟病的问题,比如类型安全问题、定义和使用复杂、编译器替换字面值等,而是进行了简化。在 go 中,常量一旦声明则在编译期其值就已知且不可更改。 1. 常量的定义 常量的定义很简单,使用 const 关键字声明即可: // 单行声明常量 const c1 = 1 const c2 = -100 const c3 = "hello" 与变量一样,同样可以组合以简化声明: const ( c4 = 3.14 c5 = 1.2 + 12i // 复数 ) 但是需要注意,组合声明时后续的常量如果没有赋值则会重复最近一个非空表达式的值,例如: const ( m = 1 // 1 n // 1 k // 1 l = m + 1 // 2 i // 2 ) fmt.Println(m, n, k, l, i) ...

2022-09-10 · 3 min · 585 words · Hank

GoLang教程——变量

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

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

GoLang教程——代码风格

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

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

GoLang教程——初识Go语言

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

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

(译)Go并发模式——Context

原文地址: https://go.dev/blog/context 作者:Sameer Ajmani 时间:29 July 2014 1. 简介 [1] 在 Go 服务器中,每个传入的请求都在其自己的 goroutine 中处理。请求处理程序通常会启动额外的 goroutine 来访问数据库和 RPC 服务等后端。处理请求的一组 goroutine 通常需要访问特定于请求的值,例如最终用户的身份、授权令牌和请求的截止日期。当请求被取消或超时时,所有处理该请求的 goroutines 都应该快速退出,以便系统可以回收它们正在使用的任何资源。 在 Google,我们开发了一个 context 包,可以轻松地将请求范围的值、取消信号和截止日期等跨 API 边界传递给正在处理请求的所有 goroutine。 该软件包 作为context公开可用 。本文介绍了如何使用该包并提供了一个完整的工作示例。 2. Context context 包的核心是 Context 类型: type Context interface { (1) Done() <-chan struct{} (2) Err() error (3) Deadline() (deadline time.Time, ok bool) (4) Value(key interface{}) interface{} (5) } 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 · 887 words · Hank