Go 没有 class,结构体(struct) + 方法(method) 是面向对象的核心替代方案


一、C++ 对照速查表

C++ 概念Go 对应说明
structstruct默认 public,字段首字母大写才导出
classstruct + 方法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 继承

  • 嵌入是 has-a(组合),不是 is-a(继承)
  • Employee 不是 Person 的子类,不能做类型替换
  • func Process(p Person) 不能直接传 Employee(需要显式转换 emp.Person)
  • 目的是代码复用,不是多态

八、方法集(Method Set)

方法集决定了类型能否实现某个接口,是 Go 类型系统的核心概念。

type MyType struct {
    Name string
}

func (m MyType) Get() string   { return m.Name }
func (m *MyType) Set(name string) { m.Name = name }
类型方法集说明
MyType(值)只有值 receiver 方法 {Get}不能实现包含 Set 的接口
*MyType(指针)值 + 指针 receiver 方法 {Get, Set}能实现更广泛的接口

实际影响:

type Getter interface {
    Get() string
}

type Setter interface {
    Set(string)
}

type Both interface {
    Getter
    Setter
}

// MyType 值类型 —— 只实现 Getter
var g Getter = MyType{}       // OK
// var s Setter = MyType{}    // 编译错误!MyType 没有 Set 方法
// var b Both = MyType{}      // 编译错误!

// *MyType 指针类型 —— 实现 Getter + Setter
var g2 Getter = &MyType{}     // OK
var s2 Setter = &MyType{}     // OK
var b2 Both = &MyType{}       // OK

经验法则:如果方法需要修改字段,就用指针 receiver,且该类型的所有方法都用指针 receiver,保持一致。


九、匿名结构体

package main

import "fmt"

func main() {
    // 匿名结构体:临时使用,不需要预先定义类型
    point := struct {
        X, Y int
    }{X: 1, Y: 2}
    fmt.Println(point) // {1 2}

    // 常见场景:函数内部临时组合数据
    result := struct {
        Name   string
        Score  float64
        Passed bool
    }{
        Name:   "张三",
        Score:  85.5,
        Passed: true,
    }
    fmt.Println(result)

    // 在 map 中使用匿名结构体作为值
    data := map[string]struct {
        IP   string
        Port int
    }{
        "server1": {IP: "192.168.1.1", Port: 8080},
        "server2": {IP: "192.168.1.2", Port: 9090},
    }
    fmt.Println(data)
}

十、空结构体 struct{}

// 空结构体不占内存(大小为 0),有独特用途

// 1. 用作 map 的值类型实现 set
set := make(map[string]struct{})
set["apple"] = struct{}{}  // 添加元素
delete(set, "apple")       // 删除元素
if _, ok := set["apple"]; ok {
    fmt.Println("存在")
}

// 2. 用作 channel 的信号传递(不需要数据)
ch := make(chan struct{})
go func() {
    // ... 工作完成
    ch <- struct{}{} // 发送信号
}()
<-ch // 接收信号

// 3. 作为方法 receiver 实现纯行为(无状态的方法集合)
type Signal int
func (Signal) Start() { fmt.Println("start") }
func (Signal) Stop()  { fmt.Println("stop") }

十一、常见坑

1. 值 Receiver 方法不能修改字段

type User struct { Name string }

func (u User) Rename(name string) {
    u.Name = name // 修改的是副本!调用方无感知
}

func main() {
    u := User{"张三"}
    u.Rename("李四")
    fmt.Println(u.Name) // 还是 "张三",没变!
}

2. 复合字面量不可寻址

type T struct{ V int }

func (t *T) Inc() { t.V++ }

func main() {
    // T{1} 是复合字面量,不可寻址
    // T{1}.Inc()  // ❌ 编译错误:cannot call pointer method Inc on T
    // T{1}.V = 2  // ❌ 编译错误:cannot assign to struct field in map
    
    // 正确写法:先赋值给变量
    t := T{1}
    t.Inc()  // ✅ 变量可寻址,自动取地址 (&t).Inc()
}

3. 混用 Receiver 导致方法集不完整

type MyType int
func (m MyType) A() {}
func (m *MyType) B() {}

// 如果接口需要 A 和 B,只有 *MyType 能满足
// 值类型 MyType 只有 A(),没有 B()
// var _ Interface = MyType{}   // 编译错误
var _ Interface = (*MyType)(nil) // OK

4. 嵌入不是继承

type Animal struct{ Name string }
func (a Animal) Speak() string { return "..." }

type Dog struct{ Animal }

// Dog.Speak() 会调用 Animal.Speak(),但:
// func Feed(a Animal) {}
// Feed(dog)  // 编译错误!Dog 不是 Animal
// Feed(dog.Animal) // 必须显式提取嵌入字段

5. 字段导出规则

type Config struct {
    Host     string // 大写:其他包可读写
    port     int    // 小写:仅本包内可访问
    Debug    bool   // 大写:导出
    timeout  int    // 小写:未导出
}
// JSON 序列化时,只有导出字段会被编码
// 结构体标签(tag)可以自定义字段名

十二、练习任务

  • 定义一个 Circle 结构体,实现 Area() 和 Perimeter() 方法
  • 创建 NewCircle(radius float64) *Circle 工厂函数,校验半径 > 0
  • 定义一个 Book 结构体,用值 receiver 实现 Summary() 方法
  • 定义 Library 结构体,嵌入 []Book,实现 AddBook()(指针 receiver)和 TotalBooks() 方法
  • 定义一个接口 Shape 包含 Area() float64,让 Circle 和 Rectangle 都实现它
  • 用匿名结构体写一个函数,接收 name, score 参数,返回包含 Passed bool 的结构体(>=60 及格)
  • 实现一个用 map[string]struct{} 去重的函数:输入 []string,输出去重后的切片
  • 理解方法集:给 MyType 添加一个值 receiver 方法和一个指针 receiver 方法,定义一个接口同时需要两者,验证哪些类型能实现该接口

十三、速记口诀

结构体定义用 struct,首字母大写才导出
初始化用字面量,按名赋值最可靠
new 返回指针值,零值初始化不能少

值 receiver 读数据,指针 receiver 改数据
方法集指针更全,实现接口用指针
混用 receiver 要小心,接口实现可能缺

嵌入不是继承,组合才是王道
字段方法自动提升,has-a 不是 is-a

空结构体零大小,map 做 set 真好用
defer 加 Close,替代析构函数
NewXxx 工厂函数,替代构造函数