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 继承
- 嵌入是
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 工厂函数,替代构造函数
豫公网安备41108202000982号