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 继承
...