流程控制: if, for, switch

Go 的流程控制关键字精简而强大——没有 while、没有 do、switch 默认不穿透 一、C++ 对照速查表 概念 C++ Go 关键区别 条件判断 if (cond) {} if cond {} Go 不需要括号 初始化语句 if (int x = f(); x > 0) (C++17) if x := f(); x > 0 语法几乎等价 基本循环 for (init; cond; post) {} for init; cond; post {} Go 统一用 for while 循环 while (cond) {} for cond {} Go 无 while 关键字 do-while do {} while (cond); for { ...; if !cond { break } } Go 无 do-while 无限循环 for (;;) {} for {} Go 写法更简洁 范围遍历 for (auto& x : vec) {} for i, v := range slice {} range 可解构 key/value switch 穿透 默认穿透(需 break) 默认不穿透(需 fallthrough) 完全相反 switch 无条件 switch { case ...: } switch { case ...: } 两者相同 类型判断 dynamic_cast/typeid switch v := x.(type) Go 的 type switch 更优雅 延迟执行 RAII / 析构函数 defer defer 是函数级,非作用域级 goto goto label; goto label; 两者几乎相同 标号跳转 break label(跳出循环) break label(跳出标签所在的 for) Go 标签只对 for 有效 二、if 语句 2.1 基本用法——不需要括号 // Go: 条件表达式不需要括号 if x > 0 { fmt.Println("正数") } else if x == 0 { fmt.Println("零") } else { fmt.Println("负数") } C++ 对照:if (x > 0) { ... } — Go 省略了括号,且 { 不能另起一行(编译器自动插入分号机制)。 ...

2026 年 10 月 13 日

三色 GC 与写屏障

C++ 的 RAII/智能指针是"代码到哪回收到哪";Go 的 GC 让事情自动发生——代价是你必须理解它何时跑、怎么跑 一、简述 Go 从 1.5 开始使用并发三色标记-清除垃圾回收器:标记阶段与应用并发跑,通过写屏障保证并发下的正确性,把 STW(Stop-The-World,全停)压缩到毫秒级。本章剖析三色算法、写屏障原理、GC 触发时机(GOGC / GOMEMLIMIT)、gctrace 日志解读,以及从 C++ 视角看"自动回收"带来的确定性问题。 核心要点:垃圾回收的本质是"追踪所有存活对象的引用图"。三色让追踪与程序运行并发,写屏障保证并发期间引用关系不变而漏标对象;GOGC==100 的意思是"堆翻倍时才触发下一次 GC"。理解这些,你才能在调优时不被玄学推着走。 二、C++ 对照速查表 概念 C++ Go 关键差异 生命周期管理 new/delete、RAII、shared_ptr 自动 GC C++ 确定、Go 不确定(回收时点不定) 引用计数 手写计数 / shared_ptr 无引用计数 Go 用可达性分析,无循环引用问题 循环引用 weak_ptr 打破 天然解决 三色标记只关心可达性 悬垂指针 shared_ptr 防悬垂、weak_ptr 观测 不可能悬垂(GC 保证活着就安全) 但 unsafe 除外 析构时机 作用域结束/shared_ptr 归零 不确定(对象 GC 后才回收内存) 需要清理副作用时必须显式 Close 手动释放 delete *不存在(runtime.GC() 只建议回收) 别依赖手工 GC 触发条件 无 heap 增长到阈值 / 周期 / 显式 GOGC、GOMEMLIMIT 控制 停顿 无(确定性) 少量 STW(毫秒级) 现在基本无感,但被 K8s CPU 限流仍可能抖动 代价感知 手工优化分配 分配快、回收不确定 追求低延迟要控制分配率 三、逐主题详解 3.1 为什么要 GC:先看可达性 GC 不关心"这块内存是不是有人 stack 上引用"的细节,只关心一件事:从根(root)出发,你能走到哪些对象? ...

2026 年 10 月 13 日

数据竞争与 race 检测

C++ 的 TSan / helgrind 你在用,Go 内置了一个更好使的:go run -race 一键找数据竞争 一、简述 数据竞争(data race):两个或多个 goroutine 并发访问同一内存位置,且至少一个是写操作,且没有任何同步(锁 / channel / 原子操作)约束它们之间的顺序。数据竞争的结果是未定义行为(UB):读到撕裂值、编译器优化后行为诡异、crash、或者——最恼人的——偶尔才出错。 C++ 里你熟知的工具:ThreadSanitizer(TSan)、-fsanitize=thread、helgrind。Go 的对应物是内置的 race detector: go run -race main.go # 直接跑并检测 go test -race ./... # 测试时检测(推荐) go build -race main.go # 构建带检测的二进制(性能有损,仅调试) 核心要点:Go race detector 不是「预防工具」,而是「逮现行犯」的探针——它监测每个内存访问,报告「两个 goroutine 同时访问同一地址」的现场。加了 -race,不等于程序没问题;没加 -race,问题照样存在。 修复手段三件套:Mutex、atomic、channel 传值。 二、C++ 对照速查表 概念 C++ Go 关键差异 数据竞争定义 两个线程无同步访问同一地址 同,goroutine 版 语义一致:至少一写 + 无同步 检测工具 TSan / -fsanitize=thread / helgrind go run -race / go test -race Go 内置,零配置,一行参数 检测原理 影子内存 + happens-before 分析 同样基于 happens-before 的 shadow 内存 实现同源(Go 4 个字节内存配 8 字节影子) 共享计数器 std::atomic<int> / mutex atomic.AddInt64(首选) Go 原子 / 锁 / channel 三种解法都有 map 并发写 std::unordered_map + mutex map 并发写 = 直接 panic Go 有额外保护:写中读/写检测 修复手段 锁 / 原子 / 无共享 Mutex / atomic / channel 传值 第三种(传值)是 Go 特色 内存模型文档 cppreference memory_order The Go Memory Model Go 文档短得多,先读「happen-before」 检测开销 慢 5~15 倍 运行慢 510 倍,内存多 510 倍 都只该在测试/CI 用 三、逐主题详解 3.1 什么是数据竞争 判定为数据竞争需要三条同时成立: ...

2026 年 10 月 13 日

数据库接入 database/sql · gorm · sqlx

从 C++ 的 sqlite3 / libpq + 手写 ORM 到 Go:database/sql 抽象层 + 驱动隔离,数据访问可以有一份标准答案 一、简述 Go 数据库世界的中心是 database/sql——一个驱动无关的 SQL 访问抽象层。就像 C++ 里 sqlite3_open、PQconnectdb 各写各的,Go 则约定「先 sql.Open 拿到抽象连接,然后 Query/Exec/事务全走同一套接口」,不同数据库只是换一个驱动 import 和连接串。sqlx 在其上补了「结构体 ↔ 行」的便捷映射;gorm 则是全功能 ORM(自动建表、关联、迁移),适合快速开发。三者是「底层 → 便捷 → 全自动」的一条谱系。 核心要点:先记住 database/sql 的三个纪律——连接池要调、行/stmt/tx 用后必关、占位符随驱动变化(MySQL 用 ?,PostgreSQL 用 $1)。在这之上,sqlx 与 gorm 都是锦上添花。 二、C++ 对照速查表 概念 C++ Go 关键差异 数据库接口 sqlite3.h / libpq 各一套 database/sql 标准库 统一抽象,换库只换驱动/连接串 连接池 自己写 thread pool sql.DB 自带 + SetMaxOpenConns 连接池是 DB 的默认行为 查询单行 sqlite3_step + 手动取列 db.QueryRow(...).Scan(&v) Scan 一次到位 查询多行 每列手动转换 rows.Next() + rows.Scan 循环+Scan,模板化 预处理 sqlite3_prepare_v2 db.Prepare + stmt.Exec 语义一致,但事务内惯用 tx 级 prepare 事务 BEGIN/COMMIT/ROLLBACK db.Begin() + tx.Commit/Rollback 要 defer tx.Rollback() 做兜底 结构体映射 手写行→struct 拷贝 sqlx Get/Select 自动映射 tag db:"col" 声明式 ORM 手写/第三方(如 sqlpp11) GORM(AutoMigrate/关联) 自动建表、crud、迁移 错误处理 返回码 err != nil 全链路 每步都要查 三、逐主题详解 3.1 database/sql 起步 —— Open 与连接池 sql.Open 并不会真的连数据库(Ping 才触发真实连接),它返回的 *sql.DB 是一个连接池句柄: ...

2026 年 10 月 13 日

数组与切片

Go 的序列容器:固定长度的数组与动态的切片——切片是 Go 中最常用的数据结构 一、C++ 对照速查表 C++ Go 关键差异 std::array<T, N> [N]T Go 数组长度是类型的一部分,[3]int 和 [4]int 是不同类型 std::vector<T> []T(切片) 切片是底层数组的引用/视图,不是独立容器 vector::push_back append append 返回新切片,必须重新赋值 vector::size() / capacity() len(s) / cap(s) 完全对应 vector<T> 赋值 切片赋值 C++ vector 赋值是深拷贝;Go 切片赋值只是拷贝引用 std::vector<T> (值类型) 切片是引用语义 切片共享底层数组,修改会相互影响 数组 == 比较 [N]T 支持 == 切片不能用 == 比较(只能和 nil 比) 核心区别:C++ 的 std::vector 是一个拥有数据的容器;Go 的切片只是一个"窗口"——包含指针、长度、容量三个字段,指向某个底层数组。 二、数组 [N]T 2.1 基本语法 // 声明:长度在类型中,不可省略 var a [5]int // 零值:[0 0 0 0 0] b := [3]string{"hello", "go", "world"} // 字面量初始化 c := [...]int{1, 2, 3} // 编译器自动推断长度 = 3 // 长度是类型的一部分:[3]int 和 [4]int 是不同类型! // var x [3]int = [4]int{...} // 编译错误 2.2 数组是值类型(与 C++ std::array 一致) func main() { a := [3]int{1, 2, 3} b := a // 值拷贝,b 是 a 的副本 b[0] = 999 // 修改 b 不影响 a fmt.Println(a) // [1 2 3] fmt.Println(b) // [999 2 3] } 2.3 数组支持 == 比较 a := [3]int{1, 2, 3} b := [3]int{1, 2, 3} fmt.Println(a == b) // true——长度相同、元素逐个相等即可比较 实际使用:Go 中几乎不用数组,绝大多数场景用切片。数组主要用于固定长度的场景(如 RGB 颜色 [3]byte、矩阵维度等)。 ...

2026 年 10 月 13 日

逃逸分析与内存分配

C++ 里 new/delete、智能指针、内存池全靠手写;Go 用逃逸分析 + 分级分配器把"栈还是堆"自动定好了 一、简述 在 Go 里写 x := 42,变量到底放栈上还是堆上,不由你说了算,由编译器说了算。这个"说了算"的过程叫逃逸分析(escape analysis)。它决定了一个变量能否安全地呆在栈上(函数返回就免费销毁),还是必须逃逸到堆(交给 GC 管理)。这直接关系到顺滑度、GC 压力与性能——理解了它,你就从"手动管内存"(C++)切换到"理解自动管内存"(Go)的思维。 核心要点:栈上分配 = 零 GC 压力、函数返回即释放;堆上分配 = 每次都要触发 GC 或分配器。逃逸分析的目标很简单:能放栈就不放堆。用 go build -gcflags="-m" 就能窥见编译器的决策过程。 二、C++ 对照速查表 概念 C++ Go 关键差异 栈上分配 int x;(RAII,作用域结束自动析构) x := 42,若未逃逸 Go 编译器决定,不是你决定 堆上分配 new / malloc,手动 delete 变量逃逸时自动堆分配 Go 无需/不能手动释放 智能指针 shared_ptr / unique_ptr 管理引用 GC 统一管理 逃逸对象由 GC 回收,无引用计数 返回局部对象 悬垂指针风险(返回局部地址) 返回局部指针 → 自动逃逸到堆,安全 见下方示例,关键差异 内存池/对象池 手写 objpool / 复用 vector 分配器内建(mcache → mcentral → mheap) Go 内置分级分配器 大小类(size class) 手写 编译期确定,分配器按 size class 取块 C++ 分配器可替代,Go 内建 允许拷贝后仍安全 深拷贝麻烦 值语义默认,复制即深拷贝 逃逸分析顺带优化拷贝去重 大对象堆分配 依赖 malloc 行为 >32KB 直接 mheap,不经过 mcache 阈值不同,直觉一致 接口装箱 void* 转换,手动管理生命周期 具体值→interface{} 可能逃逸 装箱常伴随逃逸 三、逐主题详解 3.1 栈 vs 堆:先建立直觉 ┌─────────────────── 栈(Stack)───────────────────┐ │ main() 的帧 │ │ ├─ 局部变量 x: 42 ← 编译器决定放这 │ │ └─ 调用 f() 的帧 │ │ └─ f 返回时,这一帧整体弹出,"免费释放" │ └─────────────────────────────────────────────────┘ ┌─────────────────── 堆(Heap)───────────────────┐ │ 对象:(指向它的指针被 main 拿走了) │ │ 生命周期:直到 GC 认为没人引用它才回收 │ │ 成本:分配(分配器查找空闲块)+ 回收(GC 扫描) │ └─────────────────────────────────────────────────┘ 栈上的变量:随函数帧创建/销毁。函数返回时,栈指针回落,整块内存"原地废弃"——无分配成本、无释放成本、无 GC 参与。这正是 C++ 栈对象的免费午餐。 堆上的变量:生命周期逃出当前函数,必须由 GC 跟踪回收。分配要走分配器、回收要看 GC 何时跑,每一笔都有成本。 Go 与 C++ 的最大不同:C++ 里 int x; 一定在栈上、new int 一定在堆上、返回局部变量地址=悬垂指针(未定义行为)。Go 里同一段代码,编译器可能让变量留在栈上,也可能挪去堆——全看会不会"逃逸"出当前函数。返回局部变量的指针在 Go 里完全安全: ...

2026 年 10 月 13 日

项目结构 internal/pkg/cmd

从 C++ 的目录与命名空间组织到 Go 的「包即边界」:cmd/internal/pkg 是社区事实标准 一、简述 Go 没有 C++ 头文件/源文件对,也没有 C++ 那种「目录 + 命名空间」两级组织。Go 的组织单位是包(package),一个目录一个包,import 路径即模块路径 + 目录路径。一个可发布的 Go 项目结构,社区多年沉淀出了事实标准: <repo> ├── cmd/ # 可执行程序入口(main 包) ├── internal/ # 私有代码:仅本项目可导入 ├── pkg/ # 可复用库代码:对外公开 ├── configs/ # 配置文件 ├── scripts/ # 构建/运维脚本 └── go.mod 更向上看,Go 的哲学是 「包即层级」:包之间靠 import 建立依赖,import 循环会被编译器直接拒绝——比 C++ 的循环 include 报错更早、更严格。这天然强制了「按层依赖」的架构。 核心要点:cmd / internal / pkg 不是语言强制,而是 golang-standards/project-layout 提出的事实约定。真正由 Go 强制的是:internal 的可见性规则、每个 main 包是入口、包不能循环导入。 二、C++ 对照速查表 概念 C++ Go 关键差异 组织单位 命名空间(namespace)+ 头文件 包(package) 目录=包=导入路径,三合一 入口 int main() package main + func main() main 包即程序入口 公共接口 namespace Foo 内公开符号 大写导出 / 小写私有 可见性跟标识符首字母走 私有代码 pimpl / 内部命名空间 / 隐藏符号 internal/ 目录 语言级可见性守卫 循环依赖 前置声明 + 复杂 include 图 编译器拒绝 包循环导入即编译错误 头文件 .h 声明 + .cpp 实现 一个 .go 文件即包的一段 无头文件,声名即定义 分层 module/backend/… 手工约定 handler→service→repository 由 import 依赖方向强制 单一大包 一个巨型头文件库 拆小而聚焦的包 Go 鼓励多包、小接口 测试辅助 测试私有符号靠 friend/内部类 internal 包可见 测试可直接放同包访问私有函数 三、逐主题详解 3.1 标准布局总览:cmd / internal / pkg 以 golang-standards/project-layout 为参考的常见布局: ...

2026 年 10 月 13 日

原生 HTTP 服务 net/http

从 C++ 的 Boost.Beast / Crow 到 Go:标准库就是一套完整的 HTTP 服务器,Handler 即函数签名 一、简述 在 C++ 里写 HTTP 服务要选库(Boost.Beast 偏底层、Crow 要自己搭路由、Drogon 大而全),而在 Go 里标准库 net/http 就是一个能上生产的基础 HTTP 服务框架——路由、请求解析、响应写出、超时控制、优雅关闭全都有。它的核心哲学是「一个 http.Handler 就是一个 http.ResponseWriter 参数 + 一个 *http.Request 参数的函数」,一切中间件、路由、框架都是这个接口的装饰与组合。 核心要点:Handler 是唯一的核心接口。Go 1.22 起 http.ServeMux 原生支持「方法 + 路径」路由("GET /users/{id}"),标准库写 REST 服务不再需要任何第三方框架。 二、C++ 对照速查表 概念 C++ Go 关键差异 HTTP 库 Boost.Beast / Crow / Drogon / cpp-httplib net/http 标准库 开箱即用,零依赖起服务 处理器 继承 handler / 回调 http.Handler 接口 + http.HandlerFunc 函数签名即处理器 路由 Crow 手动匹配 / router 库 http.NewServeMux Go 1.22+ 支持方法路由 路径参数 :id 需框架支持 r.PathValue("id") ServeMux 通配符 {id} 服务对象 Crow::run() http.Server 结构体 超时/监听全是 Server 字段 会话/keep-alive 手管 socket 自动,IdleTimeout 管空闲连接 默认开启 JSON 响应 序列化库 + 手动 header json.NewEncoder(w).Encode(v) 一行流式写出 中间件 手写装饰器 / Crow 外挂 func(http.Handler) http.Handler 纯函数包裹,标准模式 优雅关闭 自行实现 srv.Shutdown(ctx) 标准库内置,等待在飞请求 三、逐主题详解 3.1 最小 HTTP 服务 —— Handler 与 HandlerFunc 任何 HTTP 服务都绕不开这两个类型: ...

2026 年 10 月 13 日

指针 (Pointers)

Go 保留了指针的核心能力(间接寻址、传地址),但砍掉了指针算术和引用类型 &T,换来内存安全和 GC 兼容。 C++ 对照速查表 概念 C++ Go 差异说明 声明指针 int* p / int *p var p *int Go 把 * 放在类型前 取地址 &x &x 完全相同 解引用 *p *p 完全相同 指针算术 p++, p + n 不支持 Go 禁止,用切片代替 空指针 NULL / nullptr nil Go 没有 NULL 关键字 引用类型 int& ref = x 不存在 Go 没有引用,统一用指针 new new int (调用构造) new(int) → *int Go 的 new 只分配零值内存,不调用构造函数 struct 访问 ptr->field ptr.field Go 编译器自动解引用,不需要 -> make 无对应物 make([]int, 5) 仅用于 slice/map/channel,返回初始化后的值(不是指针) 1. 指针基础:&、*、*T package main import "fmt" func main() { x := 42 var p *int = &x // p 是指向 x 的指针,*int 表示"指向 int 的指针" fmt.Println("x 的值:", *p) // 解引用:通过指针读取值 → 42 fmt.Println("x 的地址:", p) // 打印地址 → 0xc0000b4008(类似) *p = 100 // 通过指针修改原变量 fmt.Println("修改后 x:", x) // → 100 // 零值:指针的零值是 nil var q *int fmt.Println("q == nil:", q == nil) // → true } 要点:*int 的 * 是类型修饰符(“int 的指针”),*p 的 * 是操作符(“解引用”)。 ...

2026 年 10 月 13 日

字符串与 Rune

Go 字符串的本质、Unicode 处理与常用操作速查 一、简述 Go 的 string 是不可变的字节序列,底层以 UTF-8 编码存储。它是一个值类型(赋值和传参都会拷贝),长度固定后不能修改。 与 C++ std::string 最大的区别: 不可变——没有 operator[] 返回引用,不能就地修改字符 UTF-8 原生支持——但 len() 返回的是字节数,不是字符数 rune 类型——用于表示单个 Unicode 码点(int32 别名) 二、C++ 对照速查表 Go C++ 说明 string std::string Go 是不可变值类型;C++ 可变 rune char32_t rune = int32,表示一个 Unicode 码点 byte uint8 / unsigned char 一个字节 len(s) s.size() Go 返回字节数,不是字符数 s[i] s[i] Go 返回 byte;C++ 返回 char&(可写) for _, r := range s for (char32_t c : s) Go 遍历 rune;C++ 依赖编码 strings.Builder std::ostringstream 高效字符串拼接 strconv.Itoa(n) std::to_string(n) 整数转字符串 strconv.Atoi(s) std::stoi(s) 字符串转整数 strings.Contains std::string::find 判断子串是否存在 []byte(s) reinterpret_cast 显式转换(会拷贝数据) 没有隐式转换 std::string 构造 string 和 []byte 必须显式转换 三、代码示例 3.1 string 底层:不可变字节序列 package main import "fmt" func main() { s := "Hello, 世界" fmt.Println("len:", len(s)) // 13(字节数!不是字符数) // s[0] = 'h' // 编译错误!字符串不可变 // 底层就是字节数组的只读视图 b := []byte(s) // 转为 []byte 后可修改 b[0] = 'h' s2 := string(b) // 再转回 string fmt.Println(s2) // hello, 世界 } 3.2 rune 类型与 byte vs rune package main import "fmt" func main() { s := "Go语言" // byte —— 字节视角 fmt.Println("字节数:", len(s)) // 8(G=1, o=1, 语=3, 言=3) // rune —— 字符视角 runes := []rune(s) fmt.Println("字符数:", len(runes)) // 4 // 每个 rune 是一个 int32,代表一个 Unicode 码点 fmt.Printf("语 的 Unicode: U+%04X\n", runes[2]) // U+8BED } 3.3 遍历字符串:range vs 索引 package main import "fmt" func main() { s := "Hello世界" // 方式一:for range —— 按 rune 遍历(推荐用于字符级操作) fmt.Println("=== for range ===") for i, r := range s { fmt.Printf("字节索引 %d, rune: %c (U+%04X)\n", i, r, r) } // 输出:H(0), e(1), l(2), l(3), o(4), 世(5), 界(8) // 注意:i 是字节索引!"世"从第5字节开始,占3字节,"界"从第8字节开始 // 方式二:for i —— 按字节遍历 fmt.Println("=== for i (字节) ===") for i := 0; i < len(s); i++ { fmt.Printf("s[%d] = 0x%02X\n", i, s[i]) } // 中文字符的字节是 0xE4 0xB8 0xAD 这样的三字节序列 // 方式三:转为 []rune 后按索引遍历 r := []rune(s) for i := 0; i < len(r); i++ { fmt.Printf("r[%d] = %c\n", i, r[i]) } } 3.4 utf8 包 package main import ( "fmt" "unicode/utf8" ) func main() { s := "Go语言" // 获取 rune 数量(等价于 len([]rune(s)) 但更高效) fmt.Println("字符数:", utf8.RuneCountInString(s)) // 4 // 验证是否为合法 UTF-8 fmt.Println("合法 UTF-8:", utf8.ValidString(s)) // true // 非法 UTF-8 字节 bad := []byte{0xCE, 0xBA, 0xE1, 0xBD, 0xB9, 0xCF, 0x83, 0xCE, 0xBC, 0xCE, 0xB5} fmt.Println("合法:", utf8.Valid(bad)) // false // RuneLen 返回某个 rune 编码后占多少字节 fmt.Println("语 占字节:", utf8.RuneLen('语')) // 3 } 3.5 strings 包常用函数 package main import ( "fmt" "strings" ) func main() { s := " Hello, Go World! " // 查找 fmt.Println(strings.HasPrefix(s, " Hello")) // true fmt.Println(strings.HasSuffix(s, "! ")) // true fmt.Println(strings.Contains(s, "Go")) // true fmt.Println(strings.Index(s, "Go")) // 10(字节索引) // 裁剪 fmt.Println(strings.TrimSpace(s)) // "Hello, Go World!" fmt.Println(strings.Trim(s, " !")) // "Hello, Go World" fmt.Println(strings.TrimLeft(s, " H")) // "ello, Go World! " fmt.Println(strings.TrimSuffix("test.go", ".go")) // "test" fmt.Println(strings.TrimPrefix("cmd/test.go", "cmd/")) // "test.go" // 替换 fmt.Println(strings.Replace("aabbcc", "bb", "xx", 1)) // "aaxxcc" (只替换1次) fmt.Println(strings.ReplaceAll("aabbcc", "a", "x")) // "xxbbcc" (替换所有) fmt.Println(strings.ReplaceAll("aabbcc", "bb", "")) // "aacc" (删除子串) // 分割与合并 parts := strings.Split("a,b,c", ",") // ["a", "b", "c"] joined := strings.Join(parts, "-") // "a-b-c" fmt.Println(joined) // 大小写 fmt.Println(strings.ToUpper("hello")) // "HELLO" fmt.Println(strings.ToLower("HELLO")) // "hello" fmt.Println(strings.Title("hello go")) // "Hello Go" (1.18 后建议用 cases) // 重复与填充 fmt.Println(strings.Repeat("ab", 3)) // "ababab" fmt.Println(strings.Repeat("=", 20)) // "====================" // 字段分割(按空白字符) fields := strings.Fields(" foo bar baz ") fmt.Println(fields) // [foo bar baz] } 3.6 strconv 包:类型转换 package main import ( "fmt" "strconv" ) func main() { // ---- 整数 ---- // int -> string fmt.Println(strconv.Itoa(42)) // "42" fmt.Println(strconv.FormatInt(255, 2)) // "11111111" (二进制) // string -> int n, err := strconv.Atoi("42") if err != nil { fmt.Println("转换失败:", err) } else { fmt.Println(n) // 42 } // 更灵活的解析 n2, _ := strconv.ParseInt("FF", 16, 64) // 十六进制 -> int64 fmt.Println(n2) // 255 // ---- 布尔 ---- b, _ := strconv.ParseBool("true") fmt.Println(b) // true fmt.Println(strconv.FormatBool(true)) // "true" // ---- 浮点数 ---- f, _ := strconv.ParseFloat("3.14159", 64) fmt.Println(f) // 3.14159 // 浮点数格式化 fmt.Println(strconv.FormatFloat(3.14159, 'f', 2, 64)) // "3.14" fmt.Println(strconv.FormatFloat(0.15, 'f', 10, 64)) // "0.1500000000" fmt.Println(strconv.FormatFloat(3.14159, 'e', 4, 64)) // "3.1416e+00" } 3.7 string 与 []byte 的转换 package main import "fmt" func main() { s := "你好世界" // string -> []byte(拷贝!底层数组不共享) b := []byte(s) b[0] = 'x' // 修改不影响原字符串 fmt.Println(string(b)) // "x好世界" fmt.Println(s) // "你好世界"(原字符串不变) // string -> []rune(按字符拆分) r := []rune(s) r[0] = '我' fmt.Println(string(r)) // "我好世界" // 频繁转换的性能提示: // 如果需要反复读取 []byte,先转换一次再复用,不要反复转 } 3.8 字符串拼接:三种方式 package main import ( "fmt" "strings" ) func main() { // ---- 方式一:+ 运算符(简单场景,少量拼接) ---- hello := "Hello" + ", " + "World!" fmt.Println(hello) // 注意:每次 + 都会分配新内存并拷贝 // 循环中使用 + 拼接性能差(O(n^2)) // ---- 方式二:strings.Builder(推荐,高效) ---- var builder strings.Builder builder.Grow(100) // 预分配容量(可选但推荐) for i := 0; i < 10; i++ { builder.WriteString(fmt.Sprintf("item%d ", i)) } result := builder.String() fmt.Println(result) // "item0 item1 item2 ... item9 " // 也可以用 WriteByte, WriteRune builder.Reset() builder.WriteString("Go") builder.WriteByte('!') fmt.Println(builder.String()) // "Go!" // ---- 方式三:strings.Join(拼接已有切片) ---- items := []string{"apple", "banana", "cherry"} joined := strings.Join(items, " | ") fmt.Println(joined) // "apple | banana | cherry" } 3.9 fmt.Sprintf 格式化 package main import "fmt" func main() { // 基本格式化 fmt.Printf("整数: %d, 浮点: %f, 字符串: %s\n", 42, 3.14, "hello") // Sprintf 返回字符串而非打印 s := fmt.Sprintf("年龄: %d, 成绩: %.1f%%", 18, 95.5) fmt.Println(s) // "年龄: 18, 成绩: 95.5%" // 常用格式动词 fmt.Printf("十进制: %d\n", 255) fmt.Printf("八进制: %o\n", 255) // 377 fmt.Printf("十六进制: %x\n", 255) // ff fmt.Printf("十六进制(大写): %X\n", 255) // FF fmt.Printf("二进制: %b\n", 255) // 11111111 fmt.Printf("Unicode: %U\n", '语') // U+8BED fmt.Printf("字符: %c\n", 8364) // € (欧元符号) // 宽度与对齐 fmt.Printf("[%10s]\n", "right") // [ right] fmt.Printf("[%-10s]\n", "left") // [left ] fmt.Printf("[%05d]\n", 42) // [00042] // 结构体格式化 type Person struct { Name string Age int } p := Person{"Alice", 30} fmt.Printf("%+v\n", p) // {Name:Alice Age:30} fmt.Printf("%#v\n", p) // main.Person{Name:"Alice", Age:30} fmt.Printf("%T\n", p) // main.Person // 错误处理常用 err := fmt.Errorf("文件 %s 不存在 (行号: %d)", "config.txt", 42) fmt.Println(err) } 3.10 多行字符串 package main import "fmt" func main() { // ---- 反引号:原生字符串(推荐) ---- // 不能包含反引号本身,不处理转义符 raw := ` 第一行 第二行(有缩进) \n 这不是换行,是字面量 ` fmt.Println(raw) // ---- 双引号 + 转义 ---- escaped := "第一行\n第二行\n\t\t缩进\n路径: C:\\Users\\test" fmt.Println(escaped) // 反引号常用于:正则、SQL、模板、路径 regex := `^\d{3}-\d{4}$` fmt.Println(regex) // 不需要转义反斜杠 } 四、常见坑 坑 1:len() 返回字节数,不是字符数 s := "你好" fmt.Println(len(s)) // 6,不是 2! fmt.Println(utf8.RuneCountInString(s)) // 2(正确获取字符数) 教训:涉及中文等多字节字符时,用 utf8.RuneCountInString() 或 len([]rune(s))。 ...

2026 年 10 月 13 日