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中检测 Data Race

Data Race概念 Data Race (数据竞争),是并发系统中最常见和最难调试的错误类型之一,当两个线程(协程)同时访问同一个变量并且至少其中一次访问是写入时,就会发生 Data Race 。Java 内存模型(JMM)定义了明确的 Happens-Before 规则和提供 Volatile 机制来初步保证程序的执行顺序,Go内存模型(GMM)同样也有相同的规则定义,此外 Golang 还提供了一种检测 Data Race 的机制,帮助开发者更好的分析并发代码的正确性。 go在1.1版本时支持data race 检测。 Data Race 和 Race Condition 的区别: 这两个概念非常容易混淆,stackoverflow上也有讨论这个问题,其实我认为这篇文章很好的区别了它们,所以我进行了翻译。 检测 Data Race 使用 -race 参数可以用来检测这种竞态: $ go test -race mypkg // to test the package $ go run -race mysrc.go // to run the source file $ go build -race mycmd // to build the command $ go install -race mypkg // to install the package func main() { c := make(chan int) m := make(map[string]int) go func() { m["a"] = 1 // 访问map冲突 c <- 1 }() m["a"] = 2 // 访问map冲突 <-c for k, v := range m { fmt.Printf("key = %v, val = %v\n", k, v) } } 示例程序中,两个 goroutine 同时读写 map 存在竞态,可能造成数据不正确的情况,但是错误难以发现,通过执行时添加 -race 选择可以检测: ...

2024-06-20 · 5 min · 873 words · Hank

Data Race vs Race Condition

原文地址: https://blog.regehr.org/archives/490, 翻译并略作改动。 竞态条件(race Condition)是当事件的时间或顺序影响程序的正确性时发生的缺陷。一般来说,需要某种外部计时或排序非确定性来产生竞态条件;典型的例子有上下文切换、操作系统信号、多处理器上的内存操作和硬件 中断。 当程序中有两次内存访问时,就会发生数据竞争(Data Race): 目标为同一内存位置 由两个线程同时执行 不是读取操作 不是同步操作 上边这个定义来自微软研究院的 Sebastian Burckhardt。该定义的两个方面需要注意: “同时”意味着没有像锁这样的东西强制一个操作在另一个操作之前或之后发生。 “不是同步操作”是指程序可能包含特殊的内存操作,例如用于实现锁的操作,这些操作本身并不同步。 在实践中,它们两者存在相当大的重合:许多 Race Condition 是由 Data Race 引起的,并且许多 Data Race 导致 Race Condition。另一方面,两者也可以相互独立,可能产生没有 Data Race 的 Race Condition,也可能产生没有 Race Condition 的 Data Race。 让我们从一个在两个银行账户之间转移资金的简单函数开始: transfer1 (amount, account_from, account_to) { if (account_from.balance < amount) return NOPE; account_to.balance += amount; account_from.balance -= amount; return YEP; } 当然,这并不是银行真正转移资金的方式,但这个例子非常有用。我们知道,账户余额应该是非负的,并且转移之后不能凭空创造(多出)或损失(丢失)金钱。当在没有外部同步的情况下从多个线程调用时,该函数会产生 Data Race(多个线程可以同时尝试更新帐户余额)和 Race Condition(在并行上下文中它将创造或损失金钱)。 我们可以尝试这样修复它: transfer2 (amount, account_from, account_to) { atomic { bal = account_from.balance; } if (bal < amount) return NOPE; atomic { account_to.balance += amount; } atomic { account_from.balance -= amount; } return YEP; } 这里的“atomic”(原子性)是由语言运行时实现的,也许简单地通过在原子块开始时获取线程互斥体(Mutex)并在结束时释放它,也许使用某种事务(Transaction),或者也许通过禁用中断 —— 出于示例的目的,只要 atomic 块内的代码以原子方式执行就能解决竞争问题。 ...

2024-06-18 · 2 min · 235 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

使用 Let‘S Encrypt 为您的网站申请免费证书

网站的https证书过期了,一直使用阿里云的免费ssl证书,但是现在阿里云调整了策略,证书有效期从1年缩短到3个月了,所以我决定放弃阿里云转而使用 Let’s Encrypt 申请免费证书。 简介 Let’s Encrypt 是一家免费、开放、自动化的公益性证书颁发机构(CA), 由互联网安全研究组(ISRG)运作,详细介绍可以看这里。 申请证书 按照官方文档,对于没有命令行访问权限的网站,比如wordpress、cPanel等,可以通过控制台设置或者申请后手动上传证书;而我的网站服务器托管在阿里云,拥有命令行访问权限,所以直接使用官方推荐的 Certbot ACME 客户端来管理证书,这里详细记录下步骤。 CertBot 文档有详细的安装步骤,我的 ubuntu14 上的步骤如下: 安装 snapd snap 是linux的应用程序包,可以从其自身的snap store中安装、管理软件,有点类似包管理器如 apt、yum,但是不依赖linux发行版,安全、跨平台且无依赖;而 snapd 则是一个自动管理和维护 snap 的后台服务,它们之间的区别请参阅官方文档。 Ubuntu 16.04LTS版及之上的版本已经集成了snapd,无需再安装了,而我的是 14,所以需要手动安装。 sudo apt update sudo apt install snapd 测试是否安装成功,可以安装官方的 hello-world 程序: $ snap install hello-world 2024-06-09T16:15:39+08:00 INFO Waiting for automatic snapd restart... hello-world 6.4 from Canonical✓ installed 执行并成功输出信息表示安装成功: $ hello-world Hello World! 删除 certbot-auto 和任何 Certbot OS 软件包 如果您使用操作系统包管理器(如 apt 、 dnf 或 yum 安装了任何 Certbot 包,则应在安装 Certbot snap 之前删除它们,以确保运行命令 certbot 时,将使用 snap,而不是从操作系统包管理器进行安装。执行此操作的确切命令取决于您的操作系统,但常见的示例是 sudo apt-get remove certbot 、 sudo dnf remove certbot 或 sudo yum remove certbot 。 ...

2024-06-09 · 2 min · 374 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

Rust 学习笔记 03:函数

Rust 学习笔记 03:函数 “Talk is cheap. Show me the code.” – Linus Torvalds 在这一课,我们来聊聊编程语言中最基础的构建块——函数。 如果你熟悉 Go,你可能会觉得:“函数嘛,不就是 func 换成了 fn,大括号里写逻辑吗?” 确实,表面上看区别不大。但 Rust 引入了一个对 Go 开发者来说比较新颖的概念:语句 (Statements) 与 表达式 (Expressions) 的严格区分。这直接改变了代码的书写习惯(和逼格)。 1. 定义函数 Rust 使用 snake_case (蛇形命名法) 来命名函数,所有字母小写,单词间用下划线分开。 fn main() { println!("Hello, world!"); another_function(); } fn another_function() { println!("Another function."); } Go 也是这么写的(除了大括号位置 Go 强制不换行,Rust 推荐不换行但没强制)。 2. 参数与类型 和 Go 一样,Rust 是强类型语言,函数参数必须声明类型。这点没得商量。 fn print_labeled_measurement(value: i32, unit_label: &str) { println!("The measurement is: {}{}", value, unit_label); } 3. 语句 vs 表达式 (The Twist) 这是本节的重点! ...

2024-05-18 · 2 min · 262 words · Hank

自由与梦想

今天终于读完了《我在北京送快递》一书,这本15万字的书前前后后读完居然花了两个多月的时间——习惯使然,我读什么书都喜欢字斟句酌,这一点让我有些苦恼:一方面是因为进度非常缓慢,想象一下,读一本人物传记我还在拿着笔一边阅读一边给不认识的字词标注笔记……,另一方面,由于阅读的周期很长,导致容易半途而废,可能前半部分阅读的很仔细,后半部分就走马观花甚至放弃了。不过这本书我还算比较认真的读完了,我想谈一谈我对“自由”的理解。 作者在尾声的“后记”一章中阐述了自己对“自由”的观点:这些年来自己经常处于一边工作、一边写作的生活状态中,工作时无法全心投入写作,而写作时也无法积极投身工作,作者自己希望的自由是“在高度发展上的个人意识的个人追求和自我实现”。他认为,追求自己热衷的事情,形成与别人不同的自我意识上的区别,那么这种追求是自由的;相反,表面上看似自由,却并没有实现自我价值,只是迫于某种原因而不得不去做,又或者是安于现状,非个人追求或无法实现自我,那么这些都不能算是真正的自由。 作者写到,在迫于生计而不得不付诸大量时间和精力在工作上时,自己便无法专注于自己喜欢的事情——写作,自身也被疲倦、困意、复杂的人际关系所折磨,,身心俱疲,就算挣到了钱但自己并不快乐,这种生活不能算是自由;而之后回到老家,全身心投入写作,不必为生计而担忧,每天作者自己喜欢的事情——阅读、写作、与其他作者交流写作创意等等,自己的阅读量、写作水平都有很大的提升,这在知识的高度上与其他人拉开了差距、形成了区别,自身的追求有了一定的成果,自己也觉得开心、快乐,这样的生活是自由的。 我很喜欢作者的这个观点。简而言之,作者所述的自由,在我看来是“做自己想做的事,走自己想走的路”。 每个人心中都会有梦想,那就是我们最想做的事情,最想走的路,只是多数时候我们为生活所迫东奔西走,仿佛随波逐流的沙粒,被生活磨平了棱角,早就忘记了自己的梦想是什么了。**“人们只有两条路,一条需要用心走,叫做梦想;另一条需要用脚走,叫做现实。”**诚然,现实与梦想仿佛总是相对立的两面。对于大多数人而言,梦想往往是高不可攀的,只能深埋于心,为了生计不得不放弃梦想而从事自己并不喜欢的工作的人不计其数,为追逐梦想而奋斗此生最终获得成功的人寥寥无几。 在人生的道路上,现实与梦想常常交织着,而在这交织之中,自由似乎成了一种奢侈。对许多人而言,梦想是一种遥不可及的幻影,而现实却是残酷的枷锁,将他们紧紧束缚。在这样的环境下,自由似乎只存在于幻想之中。 普通的农民每天的工作就是完成农务,整日与农田、二十四节气打交道,农忙时努力播种、收获,农闲时与朋友们一起打打牌、聊聊天。我相信,他们大多觉得生活本就如此,自给自足也挺好,除非生活实在拮据迫不得已才会跑出去打工赚钱。此时,或许他们早已习惯了这样的生活,早已忘记年轻时候自己想要做的事情了。对于他们而言,无论忙碌或是清闲,可能都不重要,年复一年重复着同样的事情,循规蹈矩地过着自己的生活,这就是最简单的自由和幸福吧。 在车水马龙的城里,白领们的知识水平比普通农民要高出许多,他们终日奔波忙碌,所选择的工作就是自己所追求的梦想吗?我相信大多连“事业”都称不上。他们所做的只是“为老板打工”,赚取自己的劳动报酬罢了,他们的工作心态总是“尽力完成我能力范围内的工作”的得过且过,很少有人会将自己的工作真正的当成一份事业来打拼,因为就算你再努力,那也是为别人赚钱,对自己并没有多大意义。所以,大多数白领会默默地选择平庸,只是在偶尔看到少数人成功之后才能激起心中的激情,也许那只是害怕自己被周围人超越的恐慌,然后疯狂地努力一段时间,随后逐渐发现无论自己多努力似乎都无法提升自己到一个新的高度,慢慢难以坚持最后果断放弃并又再次平庸下去,这就样反反复复,最终一无所成。 当我们回首自己的生活时,是否还记得小时候那个满怀梦想的自己?那个对未来充满信心、对世界充满好奇的自己是否还在内心深处?然而,随着年岁的增长,生活的压力和社会的现实往往会让我们逐渐放弃那些美好的梦想。我们被迫在现实的洪流中奔波,为了生计为了生活,不得不选择一条看似安稳但缺少激情的道路。 梦想的实现谈何容易?马云当初创业时,初创团队“十八罗汉”吃住都其家里,十几人就挤在几十平方的房子里,他们没日没夜的工作,饿了就吃泡面,困了就打地铺小睡一会儿……但是,团队人人都充满着激情,没有一个人轻言放弃,因为他们知道他们现在是在为了梦想而奋斗,奋斗着并奋不顾身。就在阿里巴巴上市的那一天,这十八罗汉人人都身价过亿、功成名就、梦想成真。回望这一路坎坷,我想他们应该是无怨无悔了吧?至少,为了梦想,再苦再累也都是甘之如饴吧。这样的生活,看似处处是桎梏,却总能令人无限神往,这又何尝不是一种自由呢?马云当然属于成功者,这些年义无反顾的追梦,等到如今大获成功之后,可能他才发现原来自己的人生还有其他更想做的事情,所以他开办学校、拍电影、搞演唱会……有钱真任性,但是他有任性的资本,试问此时,为所欲为的马云是否获得了真正的自由呢?答案只有他自己知道,看似风光无限,可能高处不胜寒的寂寞只有他自己懂得了。 当我们审视自己的生活,回顾梦想与现实的交织,或许我们会发现,自由并非遥不可及的幻影,而是一种内心的坚定和勇气。在生活的征途中,或许我们会因为生计而忙碌,会因为社会的压力而迷失,但只要我们保持对梦想的信念,不忘初心,奋力前行,那梦想的光芒就会一直照亮前行的路。或许成功并非是终点,而是一种过程,而真正的自由,其实就是坚持不懈地追寻内心的信仰,敢于实现自己的梦想,敢于选择一条不一样的道路。因为只有在追逐梦想的路上,我们才能找到真正属于自己的自由和幸福。愿我们都能保持对梦想的热爱,勇敢追寻内心的声音,活出真正的自己,活出自由的精彩人生。

2024-04-26 · 1 min · 11 words · Hank

Rust 学习笔记 02:变量与可变性

Rust 学习笔记 02:变量与可变性 “Stability is not immobility.” – Prince Metternich 习惯了 Go 语言的 var 和 :=,第一次写 Rust 时,最让人抓狂的报错一定是 cannot assign twice to immutable variable。 在 Go 里,变量生来就是可变的(除了 const),但在 Rust 里,变量生来就是不可变的。 这就像是:Go 是一个自由奔放的游乐场,你想去哪就去哪;而 Rust 是一个戒备森严的博物馆,除非你申请了"触摸许可"(mut),否则只能看,不能摸。 1. 默认不可变 (Immutability) 在 Rust 中,当你写下: let x = 5; 注意: Rust 必须在语句末尾加上 “;",与 Java 一样,但是 Go 却不需要,这需要 Go 开发者习惯很久。 这不仅是定义了一个变量,更是立下了一个誓言:“x 的值就是 5,永远不会变。” 如果你试图打破誓言: x = 6; // 编译报错! 编译器会无情地告诉你:cannot assign twice to immutable variable x。 ...

2024-04-12 · 2 min · 349 words · Hank

这本区块链爱好者必读作品终于迎来第三版

学习区块链的书籍我推荐这两本,我在这里已经推荐过一次: Mastering Bitcoin Mastering Ethereum 比特币开创了一门新的技术——区块链,而以太坊则在比特币的理念基础上进行再创新和升华,二者是近十多年来区块链技术的代表。因此,学习区块链技术,绝对离不开它们。恰好,这两本书就完全阐述了比特币和以太坊的技术。 这两本书都由同一名作者编写,他是一名有十多年区块链研究经验的爱好者,两本书的传播度非常广,是学习、研究区块链技术的必读作品。 《Mastering Ethereum》翻译为《精通以太坊》,目前该书可以在 github 上免费阅读,地址在这里,翻译版在这里。 《Mastering Bitcoin》中文译作《精通比特币》,本书详细介绍了比特币底层的实现逻辑,目前版本是第二版。但是随着比特币2017、2021两次的重大升级,添加了隔离见证、Taproot、Schnorr签名等重大改进,不论是逻辑上还是技术上都有着很大的学习难度,第二版是在2017升级之前刊印的,现在未免显得有点过时了。如果你想学习比特币的底层知识,可以在这里阅读到本书的中文版。 让人兴奋的是,2023年《Mastering Bitcoin》迎来了它的第三个版本,它由原作者 Andreas Antonopoulos 和另一位区块链爱好者 David Harding 联合编著,在第二版的基础上补全了这几年比特币的多个升级技术。可惜的是,本书目前还没有中文版本,但你可以直接在 github 上阅读英文版,地址在这里。 另外,为了便于阅读,我也fork了本书,编译并搭建了在线阅读网站,打开即可直接阅读,可以选择单页阅读整书也可以分章节阅读。 区块链技术的学习是一个枯燥和费力的过程,但历史的车轮总是向前推进的,技术在进步,坚持下来者方能迎接蜕变。 相关阅读: Bitcoin’s Taproot Upgrade: What You Should Know https://github.com/ajtowns/taproot-review

2024-03-20 · 1 min · 34 words · Hank

Python教程10:第一个实用程序

Python 教程 10:第一个实用程序 “纸上得来终觉浅,绝知此事要躬行。” 经过前面 9 课的学习,我们已经掌握了 Python 的基础知识。今天,让我们把这些知识串起来,开发一个真正实用的程序:批量文件重命名工具。 1. 项目需求 开发一个命令行工具,能够: 批量重命名文件:支持添加前缀、后缀、替换文本 过滤文件:支持按扩展名、文件名模式过滤 预览模式:先预览修改,确认后再执行 撤销功能:记录操作,支持撤销 这个工具很实用,能解决日常工作中的真实问题。 2. 项目结构 file_renamer/ ├── file_renamer.py # 主程序 ├── renamer.py # 核心重命名逻辑 ├── utils.py # 工具函数 └── history.json # 操作历史记录 3. 核心功能实现 3.1 列出目录中的文件 # utils.py import os def list_files(directory, extension=None, pattern=None): """ 列出目录中的文件 Args: directory: 目标目录 extension: 文件扩展名过滤(如'.txt') pattern: 文件名模式(简单的包含匹配) Returns: 文件路径列表 """ files = [] for filename in os.listdir(directory): filepath = os.path.join(directory, filename) # 只处理文件,忽略目录 if not os.path.isfile(filepath): continue # 扩展名过滤 if extension and not filename.endswith(extension): continue # 文件名模式过滤 if pattern and pattern not in filename: continue files.append(filepath) return files 3.2 重命名逻辑 # renamer.py import os import re class FileRenamer: """文件重命名器""" def __init__(self, directory): self.directory = directory self.changes = [] # 记录修改 def add_prefix(self, files, prefix): """添加前缀""" for filepath in files: dirname = os.path.dirname(filepath) filename = os.path.basename(filepath) new_name = prefix + filename new_path = os.path.join(dirname, new_name) self.changes.append((filepath, new_path)) def add_suffix(self, files, suffix): """添加后缀(在扩展名前)""" for filepath in files: dirname = os.path.dirname(filepath) filename = os.path.basename(filepath) name, ext = os.path.splitext(filename) new_name = name + suffix + ext new_path = os.path.join(dirname, new_name) self.changes.append((filepath, new_path)) def replace_text(self, files, old_text, new_text): """替换文件名中的文本""" for filepath in files: dirname = os.path.dirname(filepath) filename = os.path.basename(filepath) new_name = filename.replace(old_text, new_text) new_path = os.path.join(dirname, new_name) if filepath != new_path: # 只记录有变化的 self.changes.append((filepath, new_path)) def preview(self): """预览修改""" if not self.changes: print("没有要修改的文件") return print(f"\n将要进行 {len(self.changes)} 项修改:") print("-" * 60) for i, (old, new) in enumerate(self.changes, 1): old_name = os.path.basename(old) new_name = os.path.basename(new) print(f"{i}. {old_name} -> {new_name}") print("-" * 60) def execute(self): """执行重命名""" if not self.changes: print("没有要执行的操作") return success_count = 0 for old_path, new_path in self.changes: try: os.rename(old_path, new_path) success_count += 1 except Exception as e: print(f"错误:{old_path} -> {e}") print(f"\n成功重命名 {success_count}/{len(self.changes)} 个文件") # 保存操作历史 self.save_history() def save_history(self): """保存操作历史(简化版)""" import json from datetime import datetime history_file = "history.json" # 读取现有历史 history = [] if os.path.exists(history_file): with open(history_file, 'r', encoding='utf-8') as f: history = json.load(f) # 添加新记录 history.append({ 'time': datetime.now().isoformat(), 'changes': [(old, new) for old, new in self.changes] }) # 保存 with open(history_file, 'w', encoding='utf-8') as f: json.dump(history, f, indent=2, ensure_ascii=False) 3.3 主程序 # file_renamer.py #!/usr/bin/env python3 # -*- coding: utf-8 -*- """ 文件批量重命名工具 用法: python file_renamer.py """ import os from renamer import FileRenamer from utils import list_files def main(): print("=" * 60) print("文件批量重命名工具") print("=" * 60) # 获取目录 directory = input("\n请输入目录路径(留空使用当前目录):").strip() if not directory: directory = "." if not os.path.exists(directory): print(f"错误:目录 '{directory}' 不存在") return # 获取文件过滤条件 extension = input("文件扩展名过滤(如.txt,留空跳过):").strip() if not extension: extension = None # 列出文件 files = list_files(directory, extension) if not files: print("没有找到符合条件的文件") return print(f"\n找到 {len(files)} 个文件") # 创建重命名器 renamer = FileRenamer(directory) # 操作菜单 while True: print("\n请选择操作:") print("1. 添加前缀") print("2. 添加后缀") print("3. 替换文本") print("4. 预览修改") print("5. 执行重命名") print("0. 退出") choice = input("\n请输入选择:").strip() if choice == "1": prefix = input("请输入前缀:") renamer.add_prefix(files, prefix) print("✓ 已添加前缀规则") elif choice == "2": suffix = input("请输入后缀:") renamer.add_suffix(files, suffix) print("✓ 已添加后缀规则") elif choice == "3": old_text = input("请输入要替换的文本:") new_text = input("请输入新文本:") renamer.replace_text(files, old_text, new_text) print("✓ 已添加替换规则") elif choice == "4": renamer.preview() elif choice == "5": renamer.preview() confirm = input("\n确认执行?(y/N):").strip().lower() if confirm == 'y': renamer.execute() break else: print("已取消") elif choice == "0": print("再见!") break else: print("无效的选择") if __name__ == "__main__": main() 4. 使用示例 场景 1:照片重命名 假设有一批照片: ...

2024-03-18 · 3 min · 581 words · Hank

Rust 学习笔记 01:简介与环境搭建

Rust 学习笔记 01:简介与环境搭建 “A language that doesn’t affect the way you think about programming, is not worth knowing.” – Alan Perlis 作为一名写了几年 Go 的程序员,习惯了 GC 的安逸,也忍受了 if err != nil 的繁琐。一直听说 Rust 有多强,但每次看到那陡峭的学习曲线和满屏的生命周期引用,都默默劝退。 2024 年了,是时候走出舒适区,挑战一下这个"编译器教你做人"的语言了。 这系列笔记不是官方教程的复读机,而是从一个 Go/Java 开发者的视角,记录学习过程中的困惑、对比和感悟。 1. 为什么要折腾自己? 如果 Go 是一把瑞士军刀,简单实用;那 Rust 就像是一把手术刀,精准锋利,但由于太锋利,很容易割到手。 对于 Go 开发者来说,Rust 的吸引力在于: 零成本抽象:不用担心封装会带来性能损耗。 没有 GC:从此告别 STW (Stop The World),虽然 Go 的 GC 已经很快了,但有些场景下,完全控制内存是刚需。 安全性:编译器会在编译阶段就拦下绝大多数内存错误和并发数据竞争问题。 2. 环境搭建 Rust 的安装体验和 Go 差不多,甚至更现代。 macOS/Linux 一行命令搞定: curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh 这里有几个核心概念: ...

2024-03-05 · 1 min · 202 words · Hank

Python教程09:Python编码规范(PEP 8)

Python 教程 09:Python 编码规范(PEP 8) “代码的阅读次数远远多于编写次数。” PEP 8 是 Python 官方的编码规范,定义了如何写出"Pythonic"的代码。遵循这些规范,你的代码会更专业、更易读、更容易被其他 Python 程序员理解。 1. 什么是 PEP 8 PEP (Python Enhancement Proposal) 是 Python 增强提案。PEP 8 专门定义了 Python 代码的风格指南。 核心思想: 代码更多是被阅读,而不是被编写 一致性很重要 可读性至上 完整文档:https://peps.python.org/pep-0008/ 2. 缩进和空格 使用 4 个空格缩进 # 正确 def hello(): print("Hello") if True: print("World") # 错误:使用Tab或2个空格 def hello(): print("Hello") # 2个空格,不推荐 续行对齐 # 方法1:对齐左括号 result = some_function(argument1, argument2, argument3, argument4) # 方法2:悬挂缩进 result = some_function( argument1, argument2, argument3, argument4 ) # 列表、字典的续行 my_list = [ 1, 2, 3, 4, 5, 6, ] # 末尾逗号是好习惯 3. 空行 类和函数之间 # 顶层函数和类之间空2行 def function1(): pass def function2(): pass class MyClass: pass class AnotherClass: pass 方法之间 class MyClass: # 类中的方法之间空1行 def method1(self): pass def method2(self): pass 函数内部逻辑分组 def complex_function(): # 初始化部分 x = 10 y = 20 # 计算部分 result = x + y # 返回结果 return result 4. 最大行长度 每行不超过 79 个字符(文档字符串/注释不超过 72 个字符)。 ...

2024-03-02 · 3 min · 629 words · Hank

Python教程08:列表推导式入门

Python 教程 08:列表推导式入门 “简洁是智慧的灵魂。” —— 莎士比亚 列表推导式是 Python 最具特色的语法之一,它让你用一行代码完成原本需要多行循环才能实现的功能。这不仅是代码的简化,更是思维方式的提升。 1. 什么是列表推导式 列表推导式(List Comprehension)是一种简洁的创建列表的方式。 传统方法: # 生成1-10的平方 squares = [] for i in range(1, 11): squares.append(i ** 2) print(squares) # [1, 4, 9, 16, ..., 100] 列表推导式: # 一行搞定 squares = [i ** 2 for i in range(1, 11)] print(squares) # [1, 4, 9, 16, ..., 100] 代码从 3 行变成 1 行,清晰简洁,这就是 Python 的魅力。 2. 基本语法 [表达式 for 变量 in 序列] 执行过程: 遍历序列中的每个元素 将元素赋值给变量 计算表达式 将结果添加到新列表 # 示例 numbers = [1, 2, 3, 4, 5] # 每个数乘以2 doubled = [n * 2 for n in numbers] print(doubled) # [2, 4, 6, 8, 10] # 转换为字符串 str_list = [str(n) for n in numbers] print(str_list) # ['1', '2', '3', '4', '5'] # 调用方法 names = ['alice', 'bob', 'charlie'] capitalized = [name.capitalize() for name in names] print(capitalized) # ['Alice', 'Bob', 'Charlie'] 3. 带条件的列表推导式 可以添加 if 条件进行过滤: ...

2024-02-28 · 4 min · 790 words · Hank

Python教程07:字符串深入

Python 教程 07:字符串深入 “语言是思维的外壳。” 字符串是编程中最常用的数据类型之一,几乎每个程序都要处理文本。今天我们深入学习 Python 字符串的各种操作,从格式化到正则表达式,让你处理文本得心应手。 1. 字符串的创建 Python 中创建字符串有多种方式: # 单引号 s1 = 'Hello' # 双引号 s2 = "World" # 三引号(多行字符串) s3 = """这是一个 多行 字符串""" s4 = '''也可以用 单引号''' # 原始字符串(忽略转义字符) path = r"C:\Users\name\documents" # \n不会被解释为换行 # 字符串拼接 full = s1 + " " + s2 # "Hello World" 2. 字符串格式化:三种武器 方法 1:%格式化(老式,不推荐) name = "张三" age = 25 print("我叫%s,今年%d岁" % (name, age)) # 格式控制 pi = 3.14159 print("π = %.2f" % pi) # 保留2位小数 方法 2:format()方法 # 位置参数 print("{}+{}={}".format(1, 2, 3)) # 索引 print("{0}+{1}={2}".format(1, 2, 3)) print("{2}+{1}={0}".format(3, 2, 1)) # 调换顺序 # 关键字参数 print("{name}今年{age}岁".format(name="李四", age=30)) # 格式控制 print("{:.2f}".format(3.14159)) # 3.14 print("{:0>5}".format(42)) # 00042(左侧填充0,总宽度5) print("{:*^10}".format("Hi")) # ****Hi****(居中,宽度10,填充*) 方法 3:f-string(Python 3.6+,最推荐) name = "王五" age = 28 city = "北京" # 简洁直观 print(f"{name}今年{age}岁,来自{city}") # 表达式 print(f"明年我{age + 1}岁") print(f"2的10次方是{2 ** 10}") # 格式控制 pi = 3.14159 print(f"π ≈ {pi:.2f}") # 对齐和填充 num = 42 print(f"{num:0>5}") # 00042 print(f"{num:*^10}") # ****42**** # 调试输出(Python 3.8+) x = 10 print(f"{x=}") # x=10 推荐:新代码统一使用 f-string,简洁且高效。 ...

2024-02-27 · 3 min · 581 words · Hank

Python教程06:控制流程-循环语句

Python 教程 06:控制流程-循环语句 “重复是力量之母。” 如果说条件语句让程序会"选择",那循环语句就让程序会"重复"。想象一下,如果要打印 1 到 100 的数字,难道要写 100 行print()吗?循环语句就是为了解决这类重复性工作而生的。 1. for 循环:遍历序列 for 循环用于遍历序列(列表、字符串、范围等)中的每个元素。 基本语法 # 遍历列表 fruits = ["苹果", "香蕉", "橙子"] for fruit in fruits: print(f"我喜欢吃{fruit}") # 遍历字符串 for char in "Python": print(char) # 遍历字典 user = {"name": "张三", "age": 25, "city": "北京"} for key in user: print(f"{key}: {user[key]}") 语法要点: for 变量 in 序列: 循环体必须缩进 每次循环,变量会依次取序列中的每个值 range()函数 range()生成数字序列,是 for 循环的好搭档。 # range(stop):从0到stop-1 for i in range(5): print(i) # 0, 1, 2, 3, 4 # range(start, stop):从start到stop-1 for i in range(1, 6): print(i) # 1, 2, 3, 4, 5 # range(start, stop, step):指定步长 for i in range(0, 10, 2): print(i) # 0, 2, 4, 6, 8 # 倒序 for i in range(10, 0, -1): print(i) # 10, 9, 8, ..., 1 enumerate():带索引的遍历 有时候需要同时获取元素和索引: ...

2024-01-21 · 3 min · 615 words · Hank

ios如何同步obsidian笔记仓库?

对于技术人而言,经常需要记录、整理大量的笔记内容,形成自己的知识库。一款好的笔记软件我认为需要具备以下几个条件: 必须:跨平台,同时支持桌面电脑(Windows,Mac,Linux)和手机(Android,iOS) 必须:支持同步,在多台设备中打开任何一台都能接接着编写笔记 必须:实时存储,就算突然断电、司机也不会丢失已写笔记内容 必须:支持代码高亮,更便于友好地阅读代码 必须:支持 Markdown 格式,快速编写文档必备格式,谁用谁知道 必须:支持多重备份,最好是本地一份、远端一份,首选支持git同步的 可选:支持双向链接,这样笔记与笔记之间就可以形成关联关系,慢慢积累后就形成了自己的知识库 可选:支持笔记导出,比如导出 pdf 等格式,便于分享,如果能一键发布到常见博客如 hexo、wordpress、jekyll 更好 我用过诸多笔记软件,但都存在或多或少的问题,无法满足上述要求,后来一直使用网易的有道笔记,它支持一键保存笔记,不过编辑器实在难用,markdown 的图片要自己搭建图床。 阮一峰推荐的笔记软件是 github.dev,可以看看他的这篇文章。不过我更喜欢原生的 app 软件,我选择 obsidian,因为它免费而且支持上述的大部分需求,尤其是第 6 点,对于不想将笔记存储保存给笔记软件厂商而言非常好,直接使用git同步。 现在,我基本上都是用 obsidian 来记笔记和写文章,它支持双向链接,很容易形成知识体系,而且具备丰富的插件支持,具体特性可以自己咨询网络。如果对于格式非常多的文章,我也会使用 AsciiDoc 格式来编写,相比于 Markdown 它更加强大,但是语法也更复杂。Obsidian 目前并不支持,非常遗憾。 在使用 Obsidian 时,最大的问题就是手机端的同步。我是用 Github 存储笔记,手机端没有很好的同步方案,官方的同步方式无法满足需求,而且需要付费。我的需求是,macOS、windows、iPhone、iPad 四种设备上需要从 github 同步我的笔记,没有安卓端同步的需要,所以我使用 iSH 这个 app,它开源免费,支持 ios,完美的解决了我的问题。 什么是 iSH 它是一个在 ios 上模拟 Linux 环境的 app,使用的是 alpine linux,在你的 iPhone、iPad 上都可以运行并创建 Linux Shell 环境。正好我可以使用它来同步 git 仓库到我的 ios 设备上。 安装 ish app 从 ios 应用商店下载 iSH Shell,注意看名称和 logo: 下载好后打开,你就得到了一个 Linux Shell 环境。整体界面与 Shell 一样,这里重点说一下工具栏: ...

2024-01-06 · 2 min · 366 words · Hank

2023年终回顾和2024新年展望

每至年终,总慨时光飞逝,悔不该浪费光阴而不自知。欲提笔总结一年之种种,却感人生多琐事、岁月少芳华,生活不过柴米油盐酱醋茶。常人者,虽一年之大事鲜有,却亦欲略举一二。 2023年,整个国际形势依旧不太妙,俄乌冲突、巴以冲突、缅甸内战,各国都在想法设法为自己牟利,战乱频发,新冠疫情之后本来就衰弱的国际经济形势更是雪上加霜。逐年变暖的气候,给我们的家园带来了严峻的挑战:火灾、洪水、泥石流、地震、台风…… 2023国际十大新闻 2023中国十大新闻 作为一介庶民,本该独善其身,”各人自扫门前雪,莫管他人网上霜“,然人是社会人,践行之何其难也! 博客 这一年,撰写的文章倒是很少。一方面,感觉没什么可写的,可能自己正慢慢变得懒散了;另一方面,每每要提笔写些什么,发现对要写的东西还了解的不够透彻,只好搁浅。新的一年,得试着多见多听多写,多思考。这是本年的文章列表: 又一个大佬辞世,技术人该何去何从? Redis集群中的 CROSSSLOT Keys Error Nginx Bad Gateway和no live upstreams错误分析 依赖注入库wire使用入门 使用cli框架开发CLI程序 (译)Go1.13中处理错误 (译)Go错误是值 (译)Go模块:管理依赖项 (译)Go 中的字符串、字节、符文和字符 (译)数组、切片和字符串 - “append” 原理 技能 2023除了继续 golang 之外,也复习了一下 python3,包括: 阅读了《Python编程 从入门到实践 第2版》,里边基础讲解不是很透彻,实例倒是不错 廖雪峰的Python教程 语言都是互通的,但建议技术人至少掌握一门静态语言(Java、Go、Rust等)和一门动态脚本语言(Python,Ruby等),以便应对不同的场景,比如使用脚本语言快速开发一些小工具。2024年的工作中应该会大量使用 Go 和 Python。 2024打算加强区块链的课程,包括: 北大肖臻区块链课程 精读《精通以太坊》,之前只是泛读了一遍 精读《精通比特币》 读书是充实自我、提升自我最好的方式。 2023年读了几本书: 《Go语言精进之路1》:掌握go语言编程思想的必要书籍,尤其是从 java 转 go,很多套路与 java 不同,参考本书可以快速掌握 go 的一些编程技巧,本书还有下册,准备2024研究 《朱元璋传》:描写朱元璋的一生,讲述其如何从一个被逼进入寺庙混口饭吃的和尚,到建国当皇帝开辟传奇道路。本书内容较多,适合泛读。 《任正非传》:讲述了任正非与华为的故事,想要了解华为、了解任正非的人必读。 《苏东坡传》:林语堂的作品,讲述大才苏东坡传奇的人生故事。读此书除了了解苏轼一生大才却屡遭贬谪、坎坷的一生,更应该学习其豁达、乐观的人生境界。 《乡土中国》:目前正在读,看费孝通用浅显的文字讲述什么是中国乡土社会。 2024年打算读这几本书: 《MySQL技术内幕:InnoDB存储引擎》 《Go语言精进之路2》 《我在北京送快递》,豆瓣评分很高,了解北漂一簇的辛酸历程。 《三体》:中国科幻巅峰作品,电视剧看了没感觉,必须来读书了。 《人性的弱点》 读一本经济学的图书,拒绝做经济小白 我的豆瓣读书:https://book.douban.com/people/hankmo/ 生活 2023年,生活上平平淡淡才是真,没什么起起落落的故事。 作为技术人,总有或这或那的职业病,现在也不想去想这些问题了,快乐生活每一天,因为明天和意外永远不知道哪一个先到,管他作甚?我们要做的,就是努力锻炼身体,保持充足的睡眠,勤于自律,戒骄戒躁! ...

2024-01-02 · 1 min · 77 words · Hank

又一个大佬辞世,技术人该何去何从?

陈皓,网名左耳朵耗子,技术圈亲切地称他为“耗子叔”、“皓哥”,是一位资深技术大咖、骨灰级程序员,前阿里云资深架构师、天猫开发总监、亚马逊高级研发经理、汤森路透基础架构师和高级研发经理,他的博客酷壳每篇文章的阅读量高达数十万,其发布的专栏课程购买量也近20万,可见其影响力惊人! 然而,这位技术大佬在2023年5月13日因心梗而与世长辞,享年47岁。英年早逝,无不令听者痛心、见者流泪! 我很早都在拜读他的文章了,之前他一直在csdn撰写了大量的技术分享类博文,后来又自建博客。其博文范围广泛,涉及架构、技术、管理、生活方方面面,其文风犀利、见解独到、个性鲜明,每每读之均令人深受启发。这位技术传道者除了爱写作、爱分享,而且还一直处于不断学习、提升自己的状态,其生前博客还发表着GoLang、Rust、区块链等等技术的文章,实乃吾辈之楷模! 有段时间没有翻阅老师的博客了,昨天一打开发现老师并没有更新文章,最近的一篇仍然停留在《是微服务架构不香还是云不香?》 翻阅博客之时,偶然看到评论中说老师此时的消息,难以置信,无比痛心,不得不感叹命运多变、世事无常。 作为技术人的我们,用技术改变着这个世界,却无法避免被这个世界所抛弃。一方面,技术人要被公司体制所折磨,公司的利益高于一切,技术只为公司的利益服务,只要有利可图,随时可以要求技术人加班加点干活,而不管需求是否合理、技术是否可达;另一方面,技术人大多是一个隐藏于后台的微小角色,他们不受重视、不被尊重,肩负着强于身体承受能力数倍的工作量,却因为一些技术问题被公司批评,甚至辞退。加班、熬夜、饮食不规律、运动少、职业病、亚健康等等词汇都是贴在每一个技术人身上的标签,年轻时不断地过度消耗着身体,殊不知随着年龄的增长,这些“负债”总是要一点点偿还,甚至是付出生命的代价。 正如陈皓在其痛恨手册中所述:“ 痛恨各种不从研发团队出发,不从团队和项目出发的流程、方法论、咨询师、SQA、流程部门。 痛恨那些为所欲为的,为了自己商业目标牺牲用户利益的中国IT企业。 痛恨中国的C2C式的那种简单的抄袭和复制。 ……” 在这喧杂的社会,我们不得不沉下心来思考:我们会不会是下一个陈皓?我们又该何去何从呢?推荐大家阅读《软技能:代码之外的生存指南》一书,该书从职业规划、自我营销、自我学习提升个人专业技能,到理财、健身、精神来修炼自身体质等多个层面向我们展示了技术人应该如何进行技能、身心双修。 这里大致罗列作者的观点,值得我们深思: 职业:每一位技术人都需要学会管理自己的职业生涯。树立明确的目标,指定可靠、周密的计划,什么时候做什么都经过精心的安排,然后朝着目标努力。 自我营销:你的营销手段决定了你的营销对象是受益还是受损。营销需要人们的关 注,以便让人们关注你,关注你的产品。优秀的营销会将人们的需要或者期待与能够满足此愿望的产品或服务关联起来。“实现价值在先,要求回报在后”。做自己的产品、写博客、发视频,让跟多的人认识你。 学习:“活到老,学到老”。尤其是技术人,在这个飞速变化的世界里,学习的能力是至关重要的。技术人绝不能固步自封,忽视自己的技能发展,否则就会被社会淘汰。 生产力:“外行静坐等待灵感,其他人则唤起激情努力工作”。如何克服拖延症,“做该做的事”,我们需要运用方法、使用工具、培养习惯,提升自己的生产力。 理财:技术人耗费青春拿到较高的薪酬,但是如果不会理财,纵使有万贯家财,最终也可能毁于一旦。 健身:“身体是革命的本钱”,长期的久坐令技术人大多处于亚健康的状态,我们需要健身,只有强健的身体才能更好的发展自己。我认为这是大部分技术人忽略的一项,但它确实重中之重! 精神:我们并不是简单的机器——我们是人类。我们不只是一个与思想相连的躯壳。我们不能只下达指示然后就期望身体能完成这些指令。这个世界存在着另一股很强大的力量,它能带领我们走 上成功之路,把我们推向成功。这种力量为——精神。身体强健了,还要求我们保持良好的精神,乐观、豁达的心态,积极上下的生活态度,都是技术人所必须的。我们不只是只在电脑上敲敲打打的码农,我们更是父母、儿女、丈夫和妻子,积极地融入生活、享受生活吧。 陈皓老师走了,仍然有无数个陈皓前仆后继地为技术而奋斗着。我们执着与技术的同时,别忘了我们仍然是这个社会的一份子,更需要积极、乐观的去享受生活的美好! 谨以此文悼念陈皓老师,一路走好!

2023-12-27 · 1 min · 24 words · Hank

Python教程05:控制流程-条件语句

Python 教程 05:控制流程-条件语句 “人生处处是选择。” 程序和人生一样,也需要做出选择。条件语句就是让程序具备"决策"能力的工具,就像十字路口的红绿灯,告诉你该往哪走。 1. if 语句:单向选择 最简单的条件语句,满足条件就执行,不满足就跳过。 age = 20 if age >= 18: print("你已经成年了") print("可以独立做决定") 语法要点: if后面跟条件表达式,以冒号结尾 条件代码块必须缩进(通常 4 个空格) 缩进的代码属于 if 块,一起执行或跳过 2. if-else:双向选择 两条路,必须选一条。 age = 15 if age >= 18: print("成年人,可以投票") else: print("未成年,不能投票") 就像走到岔路口,往左或往右,总要选一个方向。 3. if-elif-else:多向选择 当选择超过两个时,使用elif(else if 的缩写)。 score = 85 if score >= 90: grade = "A" elif score >= 80: grade = "B" elif score >= 70: grade = "C" elif score >= 60: grade = "D" else: grade = "F" print(f"分数:{score},等级:{grade}") 执行顺序: 从上到下依次判断 遇到第一个为 True 的条件就执行,然后跳出整个 if-elif-else 结构 如果所有条件都是 False,执行 else 块(如果有的话) 这就像走迷宫,找到第一个出口就出去了,不会继续找其他出口。 4. 嵌套条件 条件语句里还可以包含条件语句。 age = 20 has_id = True if age >= 18: if has_id: print("验证通过,可以进入") else: print("请出示身份证") else: print("未成年,不能进入") 注意缩进:每一层嵌套增加一层缩进。 ...

2023-12-06 · 3 min · 501 words · Hank

Python教程04:运算符

Python 教程 04:运算符 “巧妇难为无米之炊。” 有了数据类型,接下来就要学会如何操作这些数据。运算符就是操作数据的工具,就像厨房里的刀、铲、勺,每种工具各有用途。 1. 算术运算符 最基础的运算符,用于数学计算。 基本运算 运算符 说明 示例 结果 + 加法 5 + 3 8 - 减法 5 - 3 2 * 乘法 5 * 3 15 / 除法 5 / 2 2.5 // 整除 5 // 2 2 % 取模(余数) 5 % 2 1 ** 乘方 2 ** 3 8 # 算术运算示例 a = 10 b = 3 print(f"{a} + {b} = {a + b}") # 13 print(f"{a} - {b} = {a - b}") # 7 print(f"{a} * {b} = {a * b}") # 30 print(f"{a} / {b} = {a / b}") # 3.333... print(f"{a} // {b} = {a // b}") # 3 print(f"{a} % {b} = {a % b}") # 1 print(f"{a} ** {b} = {a ** b}") # 1000 有趣的细节 除法的"历史遗留问题" ...

2023-12-04 · 5 min · 861 words · Hank

Python教程03:数据类型基础

Python 教程 03:数据类型基础 “万物皆有类。” 在 Python 的世界里,所有数据都有自己的类型。了解数据类型,就像认识食材,知道哪些能一起烹饪,哪些会"水火不容"。 1. Python 的基础数据类型 Python 有几种基础数据类型,今天我们先学习最常用的四种: 类型 英文名 示例 说明 整数 int 42, -100, 0 没有小数点的数字 浮点数 float 3.14, -0.5, 2.0 带小数点的数字 字符串 str "Hello", 'Python' 文本数据 布尔值 bool True, False 真或假 还有一个特殊的值:None,表示"空"或"无值"。 2. 整数(int) 整数就是没有小数部分的数字,可正可负可为零。 # 整数示例 age = 25 temperature = -10 zero = 0 print(age, type(age)) # 25 <class 'int'> print(temperature) # -10 # Python 3的整数可以无限大(只要内存够) big_number = 1234567890123456789012345678901234567890 print(big_number) # 正常输出,不会溢出 # 不同进制的整数 binary = 0b1010 # 二进制,等于十进制的10 octal = 0o12 # 八进制,等于十进制的10 hexadecimal = 0x1F # 十六进制,等于十进制的31 print(binary, octal, hexadecimal) # 10 10 31 整数运算 a = 10 b = 3 # 基本运算 print(a + b) # 13 加法 print(a - b) # 7 减法 print(a * b) # 30 乘法 # 除法:注意Python 3的除法很特别 print(a / b) # 3.3333... 除法,结果是浮点数 print(a // b) # 3 整除,结果是整数 print(a % b) # 1 取模(求余数) # 乘方 print(a ** 2) # 100 (10的2次方) print(2 ** 10) # 1024 这里有个有趣的现象:在 Python 3 中,10 / 3的结果是3.333...(浮点数),而不是像 Go/Java 那样得到3。如果你想要整除,必须用//。 ...

2023-11-20 · 3 min · 607 words · Hank

Python教程02:基础语法

Python 教程 02:基础语法 “纸上得来终觉浅,绝知此事要躬行。” 学编程和学游泳一样,看再多教程也不如下水扑腾几下。今天我们来学习 Python 的基础语法,这些是写代码的"规矩"。 1. 注释:给代码写"旁白" 注释就像电影里的旁白,是写给人看的,不会被 Python 执行。写注释有两个好处: 提醒自己:三个月后回头看代码,如果没有注释,你可能会问"这是谁写的垃圾代码?"(然后发现是自己写的) 方便别人:团队协作时,注释能让其他人快速理解你的思路 单行注释 用#开头,从#开始到行尾的所有内容都是注释: # 这是一个单行注释 print("Hello") # 这也是注释,可以放在代码后面 # 计算圆的面积 radius = 5 area = 3.14 * radius ** 2 多行注释 Python 没有专门的多行注释语法,但可以用三引号('''或""")来实现: """ 这是一个多行注释 可以写很多行 通常用于写文档字符串(docstring) """ ''' 单引号也可以 但更推荐用双引号 ''' def calculate_area(radius): """ 计算圆的面积 参数: radius: 圆的半径 返回: 圆的面积 """ return 3.14 * radius ** 2 最佳实践:注释要写"为什么",而不是"是什么"。比如# 计算圆的面积这种注释意义不大,因为代码本身已经很清楚了。更好的注释是:# 使用简化的π值,精确度够用且计算更快 2. 缩进:Python 的"规矩" 如果说 C/Java 用大括号{}来组织代码结构,那 Python 用的就是缩进。这是 Python 最独特的地方,也是新手最容易犯错的地方。 ...

2023-11-10 · 3 min · 484 words · Hank

Python教程01:Python简介与环境搭建

Python 教程 01:Python 简介与环境搭建 “工欲善其事,必先利其器。” 在开始学习 Python 之前,我们先要把环境搭建好。别担心,这比组装宜家家具简单多了。 1. Python 是什么? Python 是一种高级编程语言,由荷兰程序员 Guido van Rossum 于 1991 年创建。有趣的是,这个名字并非来自那条盘在树上的蟒蛇,而是源自英国喜剧团体"Monty Python"(蒙提·派森)。Guido 在开发 Python 时正在看这个喜剧团的剧集,觉得这名字"简短、独特、略带神秘",于是就用了。 不过大家还是约定俗成地用蛇做 Logo,毕竟这样看起来更酷一些。 Python 的三大特点 简单易学:语法接近自然语言,读代码就像读英文句子 功能强大:从网站开发到人工智能,从自动化脚本到科学计算,几乎无所不能 生态丰富:拥有数十万个第三方库,就像一个超级大工具箱 用一句话概括:Python 是程序员界的瑞士军刀——简单好用,功能齐全。 2. 为什么学 Python? 应用领域广泛 Web 开发:Django、Flask、FastAPI 框架让你快速搭建网站 数据分析:Pandas、NumPy、Matplotlib 是数据科学家的标配 人工智能:TensorFlow、PyTorch 支撑着深度学习的发展 自动化脚本:批量处理文件、爬虫、运维工具,让重复劳动自动化 游戏开发:Pygame 虽然小众,但也很有趣 科学计算:SciPy 在科研领域广泛应用 市场需求大 根据 TIOBE 编程语言排行榜,Python 常年稳居前三。这意味着无论你是找工作、做副业,还是只是想提升技能,Python 都是一个非常好的选择。 就像学外语,你可以学世界语(优雅但没人用),也可以学英语(实用且吃香)。Python 就是编程界的"英语"。 代码简洁优雅 Python 崇尚"用最少的代码做最多的事"。同样的功能,Python 代码往往比其他语言短 50%以上。比如打印 1 到 10: # Python:简洁明了 for i in range(1, 11): print(i) 非常的简洁。 3. Python 2 vs Python 3:历史的遗留问题 目前 Python 有两个主要版本: ...

2023-10-18 · 2 min · 368 words · Hank

Redis集群中的 CROSSSLOT Keys Error

场景 Redis单节点没有问题,切换到Redis Cluster后业务上某一功能出错: CROSSSLOT Keys in request don't hash to the same slot 出错的代码: var ctx = context.TODO() _, err := uq.queueImpl.rc.TxPipelined(ctx, func(pip redis.Pipeliner) error { cmd := pip.LPush(ctx, uq.key, dest...) if cmd.Err() != nil { return cmd.Err() } return pip.SAdd(context.Background(), uq.setkey, dest...).Err() }) 这段代码的逻辑是向 list 中push一条数据,再向一个 set 加入数据,两步操作通过 pipline 在同一个事务中完成。 问题分析 错误的大概意思就是: 请求中跨槽的key没有被hash到相同的hash槽中。通过代码分析,事务中的两次操作的key并不相同,他们没有被hash到同一个hash槽从而出现上述错误。 什么是hash槽 Redis Cluster 规范中的 Key distribution model(key分布模型)说明如下: Redis集群中将key的存储空间划分为16384个slot,整个集群可以支持最大16384个主节点,实际上建议不超过1000个节点。每一个主节点又可以处理16384个 hash slot。整个集群将hash slot分布于不同的节点组成高可用集群,单个节点有多个副本以便在节点故障时将重新平衡节点,达到高可用的目的。关于可用性保证,可以看这里。 如下的公式用于计算key的hash slot: HASH_SLOT = CRC16(key) mod 16384 Redis将相同hash值的slot分布到同一个node下,如下图所示: 图片出自这里 可以看出,hash槽(slot)就是一个整数,通过key计算得来,它的作用就是决定key存储于哪一个节点中。 为什么会出现CROSSSLOT错误 主要原因是应用程序尝试在多个键上运行命令,但操作中涉及的键不在同一个哈希槽中,这会导致集群中不允许的“CROSSSLOT”操作。 比如,使用Set的SUION命令时,如果多个key的 hash slot 不在集群中的同一个node上,则会出现CROSSSLOT错误。 前边场景中,在同一个事务中操作多个key,集群环境下必须要保证这些被操作的key必须被hash到同一个slot,否则同样会抛出CROSSSLOT错误。 Redis这么做的主要原因还是在于避免分布式数据被破坏的风险,而且在同一个事务下或者同一个命令中操作多个跨节点的key,会因网络等因素带来性能损耗,所以Redis禁止这么做。如果有这种场景,Redis也提供了解决方案:使用Hash Tags。 问题解决 那么,什么是 Hash Tag? Hash Tag(哈希标签)是一种计算HASH_SLOT的特殊处理方式,是一种确保多个键分配在同一个哈希槽中的方法。这样就可以实现在Redis集群中执行多key操作。 哈希标签的使用很简单,形式为{...},也就是将 key 中的部分字符使用大括号包裹起来,比如 {mykey}:abc。Redis在计算HASHSLOT时只会计算{}中的字符串,而不是整个key。 ...

2023-10-17 · 1 min · 123 words · Hank

Nginx Bad Gateway和no live upstreams错误分析

最近项目的生产环境中客户端出现大量的Nginx 502 Bad Gateway错误,逐步排查最终定位到是由于被ddos攻击造成服务器资源耗尽无法响应造成的问题。遂整理过程著文以记之。 场景 线上4个节点,每个节点都有两个相同服务通过Nginx作负载均衡,均采用Nginx默认值,未做过多配置,配置类似: upstream test-server { server 127.0.0.1:8002; server 127.0.0.1:8001; } 客户端出现大量的 502 Bad Gateway 信息,查看Nginx错误日志,信息如下: no live upstreams while connecting to upstream 初步定位问题,发现后台出现了很多莫名其妙的错误,查看Nginx错误日志发现也打印了很多上述错误。怀疑是后台某个接口请求出错导致返回了 500,导致Nginx将这个服务下线,后经过排查,后台确实存在一些错误信息,但是出错上述错误的时间不匹配。 后整理思路并认真思考,发现可能思路存在偏差:后台http状态码怎么会影响Nginx将服务下线呢? 因为Http状态码表示http响应的状态,也就是表示响应结果的正确与否,比如 2xx 表示服务端正确处理并响应了数据,4xx 表示客户端存在错误妨碍了服务端的处理,5xx 表示服务端错误导致处理失败了。但是,能够返回这些状态码说明服务端连接正常,只是由于特定原因导致服务端响应错误了。返回了这些状态码Nginx当真会将服务下线吗?试想一下,某一个客户端请求查询一条不存在的数据导致服务端处理时抛出异常并响应 500 状态码,然后Nginx认为服务不可用并将其下线,导致一定时间内所有的请求都返回上述错误,那么罪魁祸首到底是服务端还是客户端?Nginx这么处理是不是太过了呢?Nginx肯定不会这么来设计。 所以,我猜测Nginx不可能如此是非不分,应该是别的原因导致的。 查阅upstream官方文档,看到这么两个参数配置: fail_timeout=time: 设置多少时间内不能与下游服务成功通信 max_fails 次后可以认为下游服务不可用,这是一个周期时间,即每隔 fail_timeout 时间都会进行统计,默认为 10 秒。 max_fails=number: 设置在 fail_timeout 时间内与下游服务通信失败的次数,达到该次数后认为服务不可用,默认为1。 按照Nginx的默认设置,也就是说每个10秒统计下有服务的通信失败次数,达到1次就认为服务不可用,此时Ngingx会将其踢下线,后续所有转发到该服务的请求都会返回 no live upstreams while connecting to upstream,直到下一个10秒再重新处理(max_fails 为0又重新将服务上线)。 关键在于这个 通信失败 的理解。通信失败,表示Nginx转发请求给服务,但是服务没有任何响应,而不是一开始怀疑的 HTTP 状态不是200,能成功响应,不论是什么状态码,都应该认为与服务通信成功。 实践才能出真知,为了验证我的猜想,必须进行实验。 实验 使用golang编写一个http服务,代码如下: var inErr bool func main() { port := os.Args[1] node := os.Args[2] r := gin.Default() go func() { for { if node == "node1" { break } inErr = !inErr time.Sleep(3 * time.Second) } }() r.GET("/", func(ctx *gin.Context) { if inErr { ctx.String(http.StatusInternalServerError, "error: "+node) } else { ctx.JSON(http.StatusOK, gin.H{"msg": "ok, " + node}) } }) r.GET("/err", func(ctx *gin.Context) { ctx.String(http.StatusInternalServerError, "error: "+node) ctx.Abort() }) r.GET("/timeout", func(ctx *gin.Context) { time.Sleep(time.Second * 10) ctx.String(http.StatusOK, "after 10s responsed") }) _ = r.Run(":" + port) // 参数0为执行文件本身信息,真正的参数下标为1 } 上述代码使用了 gin 框架,大致的逻辑: ...

2023-10-07 · 4 min · 837 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