上一篇聊了 Go 基础语法,这篇进入面向对象。Go 没有 class,但它的结构体+方法组合能实现几乎所有 OOP 能力——只是思维方式要转弯。


Q1:Go 的结构体和 Java/C# 的 class 有什么区别?

A: 核心区别就一张表:

特性 Java Class Go Struct
定义 class Person { } type Person struct { }
继承 extends / implements 没有继承,用组合替代
构造函数 有构造方法 没有,用工厂函数模拟
this 指针 隐式传递 接收者显式声明
可见性 public / private 首字母大写 = 导出,小写 = 包内私有
1
2
3
4
5
6
7
8
9
10
// 定义结构体
type Person struct {
Name string // 导出字段(首字母大写)
age int // 未导出字段(仅同包可访问)
}

// 工厂函数(替代构造函数)
func NewPerson(name string) *Person {
return &Person{Name: name}
}

注意那个 &——Go 建议返回指针,避免值拷贝。

Q2:方法的接收者到底该用值还是指针?这个我踩过坑!

A: 这是 Go 新手第一高频坑。直接看代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
type Counter struct {
count int
}

// 值接收者 —— 操作的是副本!
func (c Counter) Increment() {
c.count++ // 改的是副本,原对象不变
}

// 指针接收者 —— 操作的是原对象
func (c *Counter) Increment() {
c.count++ // 改的是原对象
}

选择规则很简单:

场景 用什么 理由
需要修改结构体字段 *T 指针接收者 必须的,否则改的是副本
结构体较大(>64字节) *T 避免拷贝开销
一致性原则 如果某方法用了 *T,其余方法也统一用 *T 混用会让人困惑
小的不变对象(如 time.Time) T 值接收者 安全、语义清晰

实战口诀:不确定就用指针接收者,90% 场景都不会错。

Q3:Go 的”构造函数”怎么写?有几种模式?

A: 三种常见模式,从简单到高级:

模式一:简单工厂函数

1
2
3
func NewUser(name string) *User {
return &User{Name: name}
}

最常用,够用了。

模式二:带选项模式的工厂(推荐)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 选项函数类型
type UserOption func(*User)

func WithAge(age int) UserOption {
return func(u *User) { u.Age = age }
}

func WithEmail(email string) UserOption {
return func(u *User) { u.Email = email }
}

// 工厂函数接受可变选项
func NewUser(name string, opts ...UserOption) *User {
u := &User{Name: name}
for _, opt := range opts {
opt(u)
}
return u
}

// 使用方式
u := NewUser("Izzy",
WithAge(99),
WithEmail("izzy@example.com"),
)

为什么推荐选项模式? 参数多了之后不会出现 NewUser("", 0, "", true, false) 这种鬼东西。

模式三:必须校验的场景

1
2
3
4
5
6
7
8
9
10
func NewDatabase(url string) (*sql.DB, error) {
if url == "" {
return nil, errors.New("database URL cannot be empty")
}
db, err := sql.Open("postgres", url)
if err != nil {
return nil, fmt.Errorf("connect failed: %w", err)
}
return db, nil
}

返回 (T, error) 是 Go 的惯用法。

Q4:结构体能有 JSON/数据库标签吗?实际项目怎么用的?

A: 可以,而且这是日常开发最高频的操作:

1
2
3
4
5
6
7
type Article struct {
ID int `json:"id" db:"id"`
Title string `json:"title" db:"title"`
Content string `json:"content,omitempty" db:"content"`
CreatedAt time.Time `json:"created_at" db:"created_at"`
IsDeleted bool `json:"-" db:"is_deleted"` // json 中忽略
}

几个关键标签含义:

标签 含义 示例
json:"field" JSON 序列化字段名 snake_case → camelCase 映射
omitempty 零值时省略不输出 Content "" → JSON 里不出现
json:"-" 序列化时忽略此字段 密码、内部状态等
db:"field" ORM 映射(GORM/xorm 等) 数据库列名
1
2
3
4
5
a := Article{ID: 1, Title: "Hello", Content: "", IsDeleted: true}
data, _ := json.Marshal(a)
fmt.Println(string(data))
// 输出: {"id":1,"title":"Hello","created_at":"0001-01-01T00:00:00Z"}
// 注意: Content 被忽略(空值+omitempty),IsDeleted 被忽略(-)

Q5:匿名嵌入是什么?和继承有什么区别?

A: Go 没有继承,但有 嵌入(Embedding),效果类似:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
type Animal struct {
Name string
}

func (a *Animal) Speak() string {
return "..."
}

// 匿名嵌入 Animal
type Dog struct {
Animal // 嵌入,Dog 自动获得 Name 字段和 Speak() 方法
Breed string
}

d := &Dog{
Animal: Animal{Name: "旺财"},
Breed: "柴犬",
}

fmt.Println(d.Name) // 直接访问嵌入的字段 ✅
fmt.Println(d.Speak()) // 直接调用嵌入的方法 ✅

但和真正的继承有本质区别:

1
2
3
4
5
6
7
8
// Dog 可以覆盖 Speak()
func (d *Dog) Speak() string {
return "汪汪!"
}

// 但不能"多态调用"
var a Animal = d // ❌ 编译错误!Dog 不是 Animal 的子类
// Go 不支持隐式类型转换

关键区别总结:

继承(Java) 嵌入(Go)
is-a 关系 Cat is a Animal Dog has an Animal
类型兼容 父类引用指向子类 不兼容,是不同类型
方法覆盖 @Override 同名方法直接覆盖
多态 支持 不支持

Q6:结构体比较能用 == 吗?

A: 看情况:

1
2
3
4
5
6
7
8
9
10
11
// ✅ 可以比较:所有字段都是可比较类型
type Point struct{ X, Y int }
p1, p2 := Point{1, 2}, Point{1, 2}
fmt.Println(p1 == p2) // true

// ❌ 不能比较:包含不可比较类型(slice、map、func)
type Data struct {
Items []int // slice 不可比较
}
d1, d2 := Data{Items: []int{1}}, Data{Items: []int{1}}
// fmt.Println(d1 == d2) // 编译错误!

需要比较复杂结构体怎么办?

1
2
3
4
5
import "reflect"

func deepEqual(a, b Data) bool {
return reflect.DeepEqual(a, b)
}

不过 reflect.DeepEqual 性能不高,热路径上别滥用。

Q7:空结构体 struct{}{} 有什么用?

A: 这是个很巧妙的技巧,主要用于:

1. Set 集合(只关心 key,不需要 value)

1
2
3
4
5
6
7
8
// 用户去重
seen := map[string]struct{}{}
ids := []string{"a", "b", "a", "c", "b"}

for _, id := range ids {
seen[id] = struct{}{} // value 不占内存
}
fmt.Println(len(seen)) // 3: a, b, c

struct{} 大小为 0 字节,比用 map[string]bool 或 map[string]int 省内存。

2. Channel 信号(纯通知)

1
2
3
4
5
6
done := make(chan struct{})
go func() {
doWork()
done <- struct{}{} // 发送完成信号
}()
<-done // 等待完成

3. Method Set 占位

某些接口只需要方法存在而不关心数据时。

Q8:结构体的内存对齐是什么?为什么要关注?

A: Go 编译器会对结构体字段做 内存对齐,影响大小:

1
2
3
4
5
6
7
8
9
10
11
type Bad struct {    // 24 bytes
B byte // 1 byte + 7 padding
I int64 // 8 bytes
C byte // 1 byte + 7 padding
}

type Good struct { // 16 bytes
I int64 // 8 bytes
B byte // 1 byte
C byte // 1 byte
} // + 6 padding at end

规则:按字段大小从大到小排列,减少填充(padding)。

在 slg-go 这种高并发服务中,如果玩家状态结构体被大量实例化,对齐优化能显著降低内存使用。

下期预告

下一篇聊 接口(interface)与鸭子类型——Go 最具标志性的设计之一。为什么说”接口是隐式的”既是优点也是陷阱?error 接口的底层原理是什么?

TODO: 如果你手头有正在写的 Go 项目,可以试试把其中某个 struct 重构一下——比如给它的方法加个选项模式工厂,或者调整字段顺序看看 sizeof 变化。