反射(reflection)

C++ 的 RTTI 只能拿到 typeid/dynamic_cast 少许信息;Go 的 reflect 能在运行时查看、遍历、修改任意类型与值 一、简述 反射(reflection):程序在运行时检查自身的类型与结构,并据此做动态操作。Go 里就是 reflect 包:给你 reflect.Type(类型信息)和 reflect.Value(一段具体值),能动态读字段、读 tag、调方法、甚至修改可设置的值。序列化库(encoding/json)、ORM、配置解析、测试断言统统建立在这套 API 之上。但反射有显著的性能代价与脆弱性——本章教你怎么用、什么时候值得用。 核心要点:反射的入口是 reflect.TypeOf / reflect.ValueOf,一切操作都围着这两个对象转;可设置性(CanSet) 是反射修值的前提(变量要可寻址);三大定律是官方给的使用边界。对 C++ 来说,typeid/dynamic_cast 只能做"类型识别",Go 反射还能"改值、建新值、动态调用",范围大得多,代价也大得多。 二、C++ 对照速查表 概念 C++ Go 关键差异 类型识别 typeid(x).name() reflect.TypeOf(x) C++ 只给名字,Go 给结构、字段、方法全图 运行时类型转换 dynamic_cast<T>(须多态) 首选类型断言 x.(T);reflect 侧有 Type.AssignableTo / ConvertibleTo / Implements 做判断、Value.Convert 做转换 Go 反射不要求继承,直接看类型;reflect 包没有 AssertableTo 方法 遍历字段 boost::pfr/手写 reflect.Value.NumField/Field Go 原生支持任意结构体遍历 读字段 tag 无标准机制 reflect.StructTag.Get Go 的 struct tag 是反射的主要消费场景 动态改值 *T + reinterpret_cast 危险操作 reflect.Value.Set* Go 有 CanSet 检查,越界即 panic 动态调用方法 std::invoke(C++17,仍要具体类型) reflect.Value.Method(i).Call(...) Go 无需编译期函数指针,纯运行时 创建新实例 T{} 写死 / 工厂模式 reflect.New(T) 反射可"凭空造值",C++ 做不到通用 性能 零开销(编译期类型) 每次反射调用开销 ~几百 ns 起 Go 反射充满动态分派与检查 应用场景 typeid 用于调试/序列化 序列化/ORM/测试/DI 反射生态在 Go 里是日常 类型断言 C++ 无 x.(T) 是内置语法 注意反射 TypeOf + 断言可以互相换算 三、逐主题详解 3.1 反射两大入口:Type 与 Value package main import ( "fmt" "reflect" ) type Person struct { Name string `json:"name"` Age int `json:"age"` } func main() { p := Person{Name: "小甲", Age: 18} t := reflect.TypeOf(p) // 类型信息 v := reflect.ValueOf(p) // 值信息 fmt.Println("Type:", t) // main.Person fmt.Println("Kind:", t.Kind()) // struct fmt.Println("Value:", v) // {小甲 18} fmt.Println("TypeOf(v):", v.Type()) // main.Person } reflect.Type:描述"类型是什么"——包名、名称、Kind、字段列表、方法集、是否可比较等。URI:t.NumField()、t.Field(i)、t.Method(i)。 reflect.Value:描述"这段值是什么"——包含了底层值,可读可设(受 CanSet 约束)、可调方法、可转 interface。 Kind():底层类别(Int、String、Slice、Struct、Map 等 26 种)。Type 是具体类型(main.Person),Kind 是粗类别(struct)——C++ 里没有直接对应,最接近 typeid 得不到 Kind;Go 里 Kind 用于判断"数据结构形状"。 3.2 从 Value 取回具体值:Interface() v := reflect.ValueOf(p) // 把 Value 还原成 interface{} i := v.Interface() // 再断言回具体类型 p2 := i.(Person) fmt.Println(p2.Name) // 小甲 老代码里 v.Interface() + 类型断言是组合拳;Go 1.x 有 v.CanInterface() 检查(未导出字段、flag 受限时不可 Interface())。 原则:反射只是"绕到类型系统后面看一眼",看完一定要回到具体类型继续干活。 3.3 对 struct 字段遍历与 tag 读取(json 的底层原理) func dumpStruct(s any) { v := reflect.ValueOf(s) t := v.Type() fmt.Printf("类型: %s\n", t) for i := 0; i < v.NumField(); i++ { field := t.Field(i) // reflect.StructField val := v.Field(i) // reflect.Value tag := field.Tag.Get("json") // 读 tag fmt.Printf(" 字段 %s kind=%v 值=%v tag=%q\n", field.Name, field.Type.Kind(), val.Interface(), tag) } } dumpStruct(Person{Name: "张三", Age: 22}) // 输出: // 类型: main.Person // 字段 Name kind=string 值=张三 tag="name" // 字段 Age kind=int 值=22 tag="age" 这就是 encoding/json 的底层机制:遍历 struct 字段 → 看 json:"xxx" tag 决定键名/跳过 → 按字段类型编码/解码。 StructTag.Get("json") 可以返回 name,omitempty 这样的组合串(Lookup 可区分"有但空"和"没有")。 字段可以是导出(大写)或未导出(小写);未导出字段反射读值会 panic,写值 CanSet=false。 3.4 可设置性(CanSet)与修改值 反射改值前提:值必须是可以寻址的(addressable)。reflect.ValueOf(p) 拿到的是拷贝的 p——改它改不到原对象。 ...

2026 年 10 月 13 日

泛型 generics

从 C++ 的 template 到 Go 1.18+ 的类型参数:更克制的泛型设计 一、简述 Go 1.18 正式引入泛型(类型参数 Type Parameters)。与 C++ 模板相比,Go 泛型刻意保持简单:没有特化、偏特化、模板元编程、运算符重载,只有"类型参数 + 约束(constraint)“两个核心概念。 核心要点:Go 泛型的定位是解决"写一遍、多个类型用"的容器与工具函数问题,不是用来做编译期计算的。能用接口解决的场景,优先接口;泛型是接口的补充而非替代。 二、C++ 对照速查表 概念 C++ Go 关键差异 声明 template <typename T> func F[T any](...) Go 用 [T] 方括号 类型约束 std::is_integral_v<T> / requires T int | float64 或 constraints.Ordered Go 用类型集(union) 特化 template<> struct X<int> ❌ 不支持 Go 没有特化 偏特化 template <typename T> struct X<T*> ❌ 不支持 Go 没有偏特化 默认参数 template <typename T = int> ❌ 不支持 无默认类型参数 泛型类 std::vector<T> type Vec[T any] []T 泛型类型 泛型函数 T max(T a, T b) func Max[T constraints.Ordered](a, b T) T 语法不同 编译期计算 模板元编程 / constexpr ❌ 无 Go 泛型是运行时类型,非代码生成 运算符重载 可对类型重载 ❌ 不支持 泛型内不能直接用 + 除非约束允许 三、泛型函数 3.1 基本语法 // [T any] 声明类型参数;T 在参数和返回值中可用 func Min[T any](a, b T) T { // ❌ 编译错误:any 没有约束,不能比较大小 // if a < b { return a } return b } 问题:any 意味着"任意类型”,编译器不知道 T 是否支持 <。必须用约束声明"T 是支持比较的有序类型": ...

2026 年 10 月 13 日

复合类型

数组、切片、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 日