复合类型

数组、切片、Map、结构体——Go 的复合数据结构 一、数组(Array) Go 的数组是固定长度、同类型的值类型。 1.1 声明与初始化 // 声明:长度是类型的一部分 var arr [5]int // [0 0 0 0 0] arr := [5]int{1, 2, 3, 4, 5} arr := [...]int{1, 2, 3} // 编译器自动推断长度 = 3 // 指定索引初始化 arr := [5]int{1: 10, 3: 30} // [0 10 0 30 0] 1.2 数组特点 // 长度是类型的一部分 var a [3]int = [3]int{1, 2, 3} var b [3]int = [3]int{1, 2, 3} // a == b ✅ 同类型、同长度、同元素 → 相等 var c [4]int = [4]int{1, 2, 3, 4} // a == c ❌ 编译错误:类型不同([3]int != [4]int) // 值类型:赋值和传参会复制整个数组 arr1 := [3]int{1, 2, 3} arr2 := arr1 // arr2 是 arr1 的完整副本 arr2[0] = 100 // 不影响 arr1 fmt.Println(arr1) // [1 2 3] // ⚠️ 实际开发中很少直接用数组,切片(slice)更常用 1.3 多维数组 matrix := [2][3]int{ {1, 2, 3}, {4, 5, 6}, } fmt.Println(matrix[1][2]) // 6 二、切片(Slice)— 最核心的数据结构 切片是 Go 中最常用的复合类型。它是一个对底层数组的引用。 ...

2026 年 10 月 13 日

构建部署与 Docker 化

从 C++ 的动态库/libtool 部署噩梦到 Go 的静态二进制:一个文件搞定容器化 一、简述 Go 编译产出静态二进制(前提下 CGO_ENABLED=0),没有动态库依赖、没有运行时安装(对比 C++ 的 .so/.dll + 运行时动态链接)。这让部署模型极简: go build → 一个可执行文件 多阶段 Dockerfile(builder 编译 → 肥镜像剥离 → 瘦镜像运行) scratch/alpine 镜像,体积通常 10~30MB docker run / Compose 编排 C++ 部署的经典烦恼——libstdc++.so.6 not found、GLIBC_2.34 not found、ldconfig、需要把一堆 .so 一起拷走——在纯 Go 世界里直接消失。 核心要点:静态编译 + 多阶段构建演示了 Go 的部署哲学:「产物单一、环境无关」。只要你控制好 CGO 开关,一个二进制能跑在任意 Linux 上。 二、C++ 对照速查表 概念 C++ Go 关键差异 构建产物 动态库/静态库 + 可执行文件 单一可执行文件 无 .so/.a 需分发 运行时依赖 目标机需匹配 libc/动态库 CGO_ENABLED=0 时零依赖 拷贝即运行 版本冲突 GLIBC_2.30 not found、.so 名冲突 无(Go 自带运行时) 彻底告别 ABI 地狱 镜像方案 手动装依赖/多级 COPY .so 多阶段:build → scratch/alpine 一条 FROM ... AS builder 镜像体积 常见 500MB~2GB 常见 10~30MB 静态二进制省掉一层层库 基础镜像 ubuntu/debian 全量 scratch(空镜像)/ alpine 可以空到极致 健康检查 手写探针/脚本 HEALTHCHECK + HTTP /healthz Dockerfile 指令 服务发现/编排 systemd / 手动进程管理 同样 systemd,但容器内单进程 容器化程度完全可控 部署步骤 装库 → 拷二进制 → 起服务 跑镜像 镜像即交付物 三、逐主题详解 3.1 go build 产物:静态优先 # 常见部署编译命令(Linux amd64、静态、精简符号) CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \ go build -ldflags "-s -w" -o app ./cmd/server # 产物检查(Linux 上) file app # app: ELF 64-bit LSB executable, x86-64, statically linked # 确认没有动态链接 ldd app # 不是 a dynamic executable ⚠️ C++ 对照:C++ 默认生成动态链接可执行文件,目标机必须带好 .so 与匹配的 libc 版本;Go 默认(cgo 关)就是静态链接。代价与取舍见 3.3。 ...

2026 年 10 月 13 日

函数、多返回值与闭包

Go 函数的设计哲学:多返回值、闭包、defer——简洁而强大 一、函数定义 1.1 基本语法 // 格式:func 函数名(参数名 类型, ...) 返回类型 func add(a int, b int) int { return a + b } C++ 对照: 概念 C++ Go 函数定义 int add(int a, int b) func add(a int, b int) int 类型位置 类型在前 int a 类型在后 a int 多返回值 std::pair<T1,T2> / std::tuple 直接返回 (int, string) 函数声明 头文件 + 源文件分离 同文件(包内导出靠大小写) 1.2 参数类型简写 相邻同类型参数可以合并: // 两种写法等价 func calc(a int, b int, c int) int { ... } func calc(a, b, c int) int { ... } // 更简洁 1.3 没有默认参数,没有函数重载 Go 不支持默认参数和函数重载。需要类似效果时用函数选项模式或可变参数: // 用 Option 模式替代默认参数(后面会详细讲) type ServerOption struct { Addr string Port int } // 或者更简单的做法:拆成多个函数 func NewServer(addr string) *Server { ... } func NewServerWithPort(addr string, port int) *Server { ... } 二、多返回值 这是 Go 最核心的设计之一,C++ 没有直接对应。 ...

2026 年 10 月 13 日

函数与方法

Go 的函数设计、方法接收者、闭包与高阶函数 一、函数基础 1.1 函数定义 // 基本语法 func 函数名(参数列表) 返回类型 { // 函数体 } // 多参数 func add(a int, b int) int { return a + b } // 相同类型可以合并参数名 func add(a, b int) int { return a + b } // 多返回值(Go 的核心特性之一) func divide(a, b float64) (float64, error) { if b == 0 { return 0, fmt.Errorf("division by zero") } return a / b, nil } // 使用多返回值 result, err := divide(10, 3) if err != nil { log.Fatal(err) } fmt.Println(result) // 3.3333333333333335 1.2 命名返回值 // 返回值可以有名字,相当于在函数内部声明了局部变量 func split(sum int) (x, y int) { x = sum * 4 / 9 y = sum - x return // 裸 return,自动返回 x, y } // 命名返回值 + 裸 return 在短函数中可读性好 // 但长函数中容易出错,建议谨慎使用 1.3 可变参数函数(Variadic) // 使用 ... 实现可变参数 func sum(nums ...int) int { total := 0 for _, n := range nums { total += n } return total } fmt.Println(sum(1, 2, 3)) // 6 fmt.Println(sum(1, 2, 3, 4, 5)) // 15 // 传递切片给可变参数:使用 ... nums := []int{1, 2, 3} fmt.Println(sum(nums...)) // 6 二、函数作为值 2.1 函数是一等公民 // 函数可以赋值给变量 add := func(a, b int) int { return a + b } fmt.Println(add(1, 2)) // 3 // 函数可以作为参数传递 func apply(a, b int, op func(int, int) int) int { return op(a, b) } result := apply(10, 5, func(a, b int) int { return a - b }) fmt.Println(result) // 5 // 函数可以作为返回值 func makeMultiplier(factor int) func(int) int { return func(n int) int { return n * factor } } double := makeMultiplier(2) fmt.Println(double(5)) // 10 三、闭包(Closure) 闭包是引用了外部变量的匿名函数。Go 中每次创建闭包都会捕获变量的引用。 ...

2026 年 10 月 13 日

环境与工具链

Go 的安装、配置与核心工具链速览 一、Go 的安装与配置 1.1 下载安装 平台 方式 Windows 下载 .msi 安装包:go.dev/dl macOS brew install go 或下载 .pkg Linux 下载 .tar.gz 解压至 /usr/local/go 安装完成后验证: go version # go version go1.24.x windows/amd64 1.2 环境变量 Go 通过几个关键环境变量控制行为: 变量 作用 常用值 GOROOT Go 安装目录(一般自动检测) /usr/local/go GOPATH 工作区目录(存放模块缓存和编译缓存) ~/go(默认) GOBIN go install 安装的可执行文件目录 $GOPATH/bin GOPROXY 模块代理地址(国内必改) https://goproxy.cn,direct GO111MODULE 模块模式开关(1.16+ 默认开启) on 国内加速配置(重要): # 永久生效(PowerShell) go env -w GOPROXY=https://goproxy.cn,direct # 或设置环境变量 set GOPROXY=https://goproxy.cn,direct 1.3 工作区结构(GOPATH) GOPATH/ ├── bin/ # go install 编译出的可执行文件 ├── pkg/ # 编译缓存 └── src/ # [已废弃] 早期放源码,现在用 Go Modules Go 1.16+ 不再需要把代码放在 GOPATH/src 下。使用 Go Modules 后,项目可以放在任意目录。 ...

2026 年 10 月 13 日

接口 interface

从 C++ 的虚函数/抽象类到 Go 的接口:鸭子类型、隐式实现与组合式设计 一、简述 Go 的 interface 是一组方法签名的集合,它定义了类型"能做什么",而不是"是什么"。与 C++ 的抽象类最大的不同:Go 接口是隐式实现——一个类型只要实现了接口里的所有方法,就自动满足该接口,不需要显式声明 implements。 核心要点:Go 接口让"面向接口编程"从设计原则变成了语言级强制。它催生了组合优于继承、依赖注入、mock 测试等一整套惯用法。 二、C++ 对照速查表 概念 C++ Go 关键差异 抽象接口 class Shape { virtual double area() = 0; } type Shape interface { Area() float64 } Go 只有方法签名,没有数据成员 实现 class Circle : public Shape { ... } func (c Circle) Area() float64 { ... } 隐式实现,无需继承声明 多态 虚函数表 vtable + 指针/引用 接口值(类型 + 数据指针) 运行时动态分派,机制不同 空接口 void* interface{} / any 表示任意类型,但不建议滥用 接口嵌套 多重继承(菱形问题) 接口嵌入接口 无菱形问题 类型断言 dynamic_cast<T*> v, ok := x.(T) Go 更安全:带 ok 判断 判断实现 typeid / 编译期检查 编译期自动检查 只要方法对上即可 三、接口的定义与实现 3.1 定义接口 // 定义接口:只声明方法,不实现 type Shape interface { Area() float64 Perimeter() float64 } 3.2 隐式实现 // Circle 不需要写 "implements Shape",只要方法对上就自动满足 type Circle struct { Radius float64 } func (c Circle) Area() float64 { return 3.14159 * c.Radius * c.Radius } func (c Circle) Perimeter() float64 { return 2 * 3.14159 * c.Radius } // Rectangle 同样自动实现 Shape type Rectangle struct { Width, Height float64 } func (r Rectangle) Area() float64 { return r.Width * r.Height } func (r Rectangle) Perimeter() float64 { return 2 * (r.Width + r.Height) } 3.3 使用接口 func printShape(s Shape) { fmt.Printf("面积=%.2f, 周长=%.2f\n", s.Area(), s.Perimeter()) } func main() { c := Circle{Radius: 1.0} r := Rectangle{Width: 2, Height: 3} printShape(c) // 传入接口参数,自动多态 printShape(r) // 接口变量可以存任意实现类型 var s Shape = c fmt.Println(s.Area()) } 对照 C++:C++ 必须 class Circle : public Shape 显式继承;Go 只要方法签名对上就行,实现与接口完全解耦——甚至可以给别人的类型补上方法让它满足你的接口。 ...

2026 年 10 月 13 日

接口与多态

Go 的隐式接口、空接口与面向接口编程 一、接口基础 1.1 接口定义 // 接口是一组方法签名的集合 type Reader interface { Read(p []byte) (n int, err error) } type Writer interface { Write(p []byte) (n int, err error) } // 组合多个接口 type ReadWriter interface { Reader Writer } 1.2 隐式实现(鸭子类型) Go 的接口是隐式实现的——不需要 implements 关键字: // 只要一个类型实现了接口的所有方法,它就自动满足该接口 type MyReader struct { data []byte pos int } func (r *MyReader) Read(p []byte) (n int, err error) { // 实现 Read 方法 if r.pos >= len(r.data) { return 0, io.EOF } n = copy(p, r.data[r.pos:]) r.pos += n return n, nil } // MyReader 自动实现了 io.Reader 接口,无需声明 var r io.Reader = &MyReader{} // ✅ 编译通过 Go 的哲学:行为决定类型,而不是声明决定类型。 如果它像鸭子一样走路、像鸭子一样叫,那它就是鸭子。 ...

2026 年 10 月 13 日

结构体与方法

Go 没有 class,结构体(struct) + 方法(method) 是面向对象的核心替代方案 一、C++ 对照速查表 C++ 概念 Go 对应 说明 struct struct 默认 public,字段首字母大写才导出 class struct + 方法 Go 没有 class 关键字 this 指针 Receiver(值/指针) 显式声明,名字自取 构造函数 NewXxx() 工厂函数 没有构造函数语法 析构函数 defer + Close() 没有析构函数语法 继承 (is-a) 嵌入 (has-a) 组合优于继承,没有继承 运算符重载 不支持 Go 刻意不提供 访问控制 private/protected/public 首字母大小写 大写=导出,小写=包内可见 模板/泛型 泛型(Go 1.18+) func F[T any](x T) T 虚函数 / 多态 接口隐式实现 鸭子类型,无需声明 implements 二、结构体定义与零值 package main import "fmt" // 定义结构体:type + struct 关键字 // C++ 对比:类似 struct MyPoint { double x; double y; }; 但没有方法体 type Point struct { X float64 // 首字母大写 = 导出字段(其他包可访问) Y float64 name string // 首字母小写 = 未导出(仅本包内可访问) } func main() { // 零值:所有字段自动初始化为各自类型的零值 // float64 -> 0.0, int -> 0, string -> "", bool -> false, 指针/切片/map -> nil var p Point fmt.Printf("零值: %+v\n", p) // {X:0 Y:0 name:} } 三、结构体初始化 package main import "fmt" type User struct { Name string Age int Email string } func main() { // 方式1: 字面量(最常用,推荐) u1 := User{Name: "张三", Age: 25, Email: "zhangsan@example.com"} fmt.Println(u1) // 方式2: 按字段名初始化(部分字段,其余为零值) u2 := User{Name: "李四"} // Age=0, Email="" fmt.Println(u2) // 方式3: 按顺序初始化(不推荐!字段顺序变化会导致 bug) u3 := User{"王五", 30, "wangwu@example.com"} // 需要 import fmt 不会用到,编译器不检查字段名 fmt.Println(u3) // 方式4: new 返回指针,所有字段为零值 u4 := new(User) // *User 类型 u4.Name = "赵六" // Go 允许对指针直接访问字段,自动解引用 fmt.Println(u4) // 方式5: 声明后再赋值 var u5 User u5.Name = "钱七" u5.Age = 28 fmt.Println(u5) } NewXxx 工厂函数(替代构造函数) // Go 没有构造函数,惯用 NewXxx 工厂函数 func NewUser(name, email string, age int) *User { return &User{ Name: name, Age: age, Email: email, } } // 使用 // u := NewUser("张三", "zhangsan@example.com", 25) // 优点:返回指针,避免大结构体拷贝;可以做参数校验 四、结构体比较 package main import "fmt" type Point struct { X int Y int } func main() { a := Point{1, 2} b := Point{1, 2} c := Point{3, 4} // 同类型结构体可以用 == 比较(逐字段比较) fmt.Println(a == b) // true fmt.Println(a == c) // false // 包含不可比较字段(slice, map)的结构体不能用 == // type Bad struct { Data []int } // var x, y Bad // x == y // 编译错误!slice 不能比较 } 五、方法定义 package main import "fmt" type Rectangle struct { Width float64 Height float64 } // 值 Receiver:操作的是副本,不会修改原值 // C++ 对比:相当于 void method() const(不能修改成员) func (r Rectangle) Area() float64 { return r.Width * r.Height } // 指针 Receiver:操作的是原值,可以修改字段 // C++ 对比:相当于非 const 的 this 指针方法 func (r *Rectangle) SetWidth(w float64) { r.Width = w // 直接修改原结构体 } // String 方法实现 fmt.Stringer 接口 func (r Rectangle) String() string { return fmt.Sprintf("Rectangle(%.1f x %.1f)", r.Width, r.Height) } func main() { rect := Rectangle{Width: 3, Height: 4} fmt.Println(rect.Area()) // 12 rect.SetWidth(5) fmt.Println(rect) // Rectangle(5.0 x 4.0) // Go 会自动处理取地址/解引用: rectPtr := &Rectangle{Width: 1, Height: 2} rectPtr.Area() // 值方法:自动解引用 (*rectPtr).Area() (*rectPtr).Area() // 手动解引用,效果相同 rect2 := Rectangle{Width: 1, Height: 2} rect2.SetWidth(3) // 指针方法:自动取地址 (&rect2).SetWidth(3) } 六、值 Receiver vs 指针 Receiver 选择原则 场景 推荐 Receiver 原因 只读取,不修改字段 值 Receiver 避免不必要的拷贝开销(小结构体) 需要修改字段 指针 Receiver 必须修改原值 结构体较大 指针 Receiver 避免每次调用都拷贝 实现接口 同一类型保持一致 混用会导致方法集问题(见下文) slice/map 字段需修改 指针 Receiver 虽然 slice 底层是引用,但语义更清晰 关键区别 type Counter struct { Value int } // 值 Receiver:操作的是拷贝 func (c Counter) IncByValue() { c.Value++ // 修改的是副本,原值不变! } // 指针 Receiver:操作的是原值 func (c *Counter) IncByPtr() { c.Value++ // 直接修改原值 } func main() { c := Counter{Value: 0} c.IncByValue() fmt.Println(c.Value) // 0 —— 没变! c.IncByPtr() fmt.Println(c.Value) // 1 —— 变了 } 七、结构体嵌入(Embedding)—— 组合优于继承 package main import "fmt" // 基础结构体 type Person struct { Name string Age int } // 嵌入 Person —— 注意:没有字段名! // 这不是 "Person 字段",而是把 Person 的字段和方法"提升"到 Employee 中 type Employee struct { Person // 嵌入(embedding),不是字段 Company string Salary float64 } // Person 的方法也会被提升 func (p Person) Greet() string { return fmt.Sprintf("你好,我是 %s", p.Name) } // Employee 自己的方法 func (e Employee) Info() string { return fmt.Sprintf("%s 在 %s 工作,薪资 %.0f", e.Name, e.Company, e.Salary) } func main() { // 初始化:嵌入结构体可以逐层初始化 emp := Employee{ Person: Person{Name: "张三", Age: 30}, Company: "字节跳动", Salary: 30000, } // 字段提升:直接访问嵌入结构体的字段 fmt.Println(emp.Name) // "张三" —— Person 的字段被提升 fmt.Println(emp.Age) // 30 fmt.Println(emp.Company) // "字节跳动" // 方法也被提升 fmt.Println(emp.Greet()) // "你好,我是 张三" // 也可以显式指定 fmt.Println(emp.Person.Name) // "张三" } 嵌入接口 // 结构体可以嵌入接口 type Logger interface { Log(msg string) } type Server struct { Logger // 嵌入接口:Server 隐式实现了 Logger Addr string } // 使用时 Server 必须给 Logger 赋值,否则调用 Log 会 panic // srv := Server{Logger: someLoggerImpl, Addr: ":8080"} C++ 对比:嵌入 vs 继承 ...

2026 年 10 月 13 日

类型断言与 type-switch

从 C++ 的 dynamic_cast / typeid 到 Go 的类型断言、类型开关与反射初探 一、简述 接口变量在运行时存储了"动态类型"。当我们需要把接口值还原成具体类型、或按不同类型走不同逻辑时,就用类型断言(type assertion)和类型开关(type switch)。 核心要点:Go 类型断言是 x.(T) 语法——它是"安全的 dynamic_cast":带 ok 判断时永远不会 panic。类型开关 switch v := x.(type) 则是处理"未知类型集合"最优雅的方式。 二、C++ 对照速查表 概念 C++ Go 关键差异 向下转型 dynamic_cast<T*>(ptr) v, ok := x.(T) Go 更简洁,双返回值 不安全转型 static_cast / reinterpret_cast v := x.(T) Go 会 panic(不推荐) 类型判断 typeid(x) == typeid(T) x.(type)(type switch) 语法不同 类型名称 typeid(x).name() fmt.Sprintf("%T", x) 运行时类型名 反射 RTTI / <typeinfo> reflect 包 用法差异大 三、类型断言基础 3.1 语法与两种形式 var x any = 42 // 形式一:带 ok(推荐,安全) v, ok := x.(int) if ok { fmt.Println("是 int:", v) // 42 } else { fmt.Println("不是 int") } // 形式二:不带 ok(不推荐,失败直接 panic) v2 := x.(int) // ✅ 成功 // v3 := x.(string) // ❌ panic: interface conversion 规则:永远用带 ok 的形式。不带 ok 的写法相当于 C++ 的 static_cast——假设你知道类型,错了就崩。 ...

2026 年 10 月 13 日

流程控制

if、for、switch、defer——Go 的控制流语句 一、if 条件语句 1.1 基本语法 // Go 的 if 条件不需要括号,但花括号必须存在 if score >= 60 { fmt.Println("pass") } else if score >= 80 { fmt.Println("good") } else { fmt.Println("fail") } 1.2 if 带初始化语句 Go 的 if 可以包含一条简短的初始化语句,变量作用域仅限于 if/else 块内: // 经典用法:同时获取值和错误 if err := doSomething(); err != nil { fmt.Println("error:", err) return } // 这里 err 不可用 // 读取文件的经典模式 if data, err := os.ReadFile("config.txt"); err != nil { fmt.Println("读取失败:", err) } else { fmt.Println("内容:", string(data)) } Go 的惯例:if err != nil 是 Go 代码中出现频率最高的模式之一。 二、for 循环 Go 只有 for 一种循环语句,但功能覆盖了 while、do-while 的场景。 2.1 三种形式 // 形式1:传统 for 循环 for i := 0; i < 10; i++ { fmt.Println(i) } // 形式2:while 风格(只有条件) for condition { // ... } // 形式3:无限循环 for { // break 退出 if done { break } } 2.2 遍历集合 // 遍历切片/数组 nums := []int{10, 20, 30} for i, v := range nums { fmt.Printf("索引=%d 值=%d\n", i, v) } // 只要值 for _, v := range nums { fmt.Println(v) } // 只要索引 for i := range nums { fmt.Println(i) } // 遍历 map m := map[string]int{"Go": 1, "Java": 2} for k, v := range m { fmt.Printf("%s → %d\n", k, v) } // 遍历字符串(按 rune 遍历) for i, ch := range "Hello, 世界" { fmt.Printf("字节位置=%d 字符=%c Unicode=%U\n", i, ch, ch) } // 输出中 '世' 的索引是 7(不是 10),因为 range 按 rune 遍历 2.3 for-range 陷阱 // 陷阱:range 的值是副本,修改不影响原切片 nums := []int{1, 2, 3} for _, v := range nums { v = v * 2 // ❌ 只修改了副本,nums 不变 } // 正确做法:通过索引修改 for i := range nums { nums[i] = nums[i] * 2 // ✅ 修改原切片 } 三、switch 多分支 Go 的 switch 比其他语言更强大,默认不穿透(fall through)。 ...

2026 年 10 月 13 日