前言
Go 没有 class、没有继承,但通过结构体 + 方法 + 接口实现了更轻量的面向对象。核心哲学一句话:组合优于继承,隐式优于显式。
一、结构体
type Server struct {
Host string // 大写开头 = 导出(公开)
Port int // 小写 = 包内私有
Tags []string
metadata map[string]string
}
// 字面量初始化(推荐: 带字段名, 可读性好且不怕字段调整)
s := Server{
Host: "10.0.0.1",
Port: 8080,
Tags: []string{"prod", "web"},
}
// 零值可用(Go 设计哲学!)
var s2 Server // Host="" Port=0 Tags=nil, 直接用不会崩
s2.Port = 9090
// 结构体是值类型:赋值即拷贝
s3 := s2
s3.Port = 1000
fmt.Println(s2.Port) // 9090, 不受影响
构造函数惯例(Go 没有 constructor,用普通函数模拟):
func NewServer(host string, port int) *Server {
return &Server{
Host: host,
Port: port,
}
}
二、方法与接收者
// 值接收者
func (s Server) Addr() string {
return fmt.Sprintf("%s:%d", s.Host, s.Port)
}
// 指针接收者:方法内能修改原对象
func (s *Server) SetPort(port int) {
s.Port = port
}
怎么选? 记住两条规则:
- 需要修改接收者 → 必须指针接收者
- 结构体较大或包含 mutex 等字段 → 指针接收者(避免拷贝开销/拷贝导致锁失效)
- 同一结构体的方法保持统一:要有一个用指针,全用指针
s := NewServer("10.0.0.1", 80)
s.SetPort(8080) // 语法糖: Go 自动 (&s).SetPort(8080)
fmt.Println(s.Addr()) // 10.0.0.1:8080
三、组合与嵌入(Go 的"继承")
type BaseLogger struct {
Prefix string
}
func (l BaseLogger) Log(msg string) {
fmt.Println(l.Prefix, msg)
}
// 嵌入(不是继承!): 直接把类型写进来
type App struct {
BaseLogger // 匿名嵌入
Name string
}
app := App{BaseLogger: BaseLogger{Prefix: "[app]"}, Name: "web"}
app.Log("started") // 方法被"提升", 像自己的方法一样调用
嵌入 vs 继承的区别:没有父类子类的多态链条,只是能力复用。需要覆盖时显式重定义:
func (a App) Log(msg string) { // 覆盖提升的方法
a.BaseLogger.Log(a.Name + " " + msg) // 显式调用内层的
}
四、接口:隐式实现
Go 接口最独特之处:不需要 implements 声明,只要实现了接口要求的方法集,就自动满足该接口:
type Describer interface {
Describe() string
}
func (s Server) Describe() string { // Server 实现了 Describer
return "server at " + s.Addr()
}
func Print(d Describer) { // 面向接口编程
fmt.Println(d.Describe())
}
Print(s) // ✅ 编译器自动确认 Server 满足 Describer
接口是"能力清单",惯例是小而专:
// 标准库的例子
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
}
接口值 = 动态类型 + 动态值:
var d Describer
fmt.Println(d == nil) // true
d = s
fmt.Printf("%T\n", d) // main.Server —— 动态类型
// 类型断言与 type switch
v, ok := d.(Server) // 断言成具体类型
if ok { fmt.Println(v.Host) }
switch x := d.(type) {
case Server:
fmt.Println("是 Server", x.Host)
default:
fmt.Println("未知类型")
}
⚠️ 经典坑——nil 接口 ≠ 装着 nil 指针的接口:
var p *Server // p == nil
var d Describer = p // 接口里装了 (类型=*Server, 值=nil)
fmt.Println(d == nil) // false!
返回 error 时如果 var e *MyErr; return e,调用方 if err != nil 会误判——返回明确的 nil。
五、两个必会接口
Stringer:自定义打印
func (s Server) String() string {
return fmt.Sprintf("Server{%s:%d}", s.Host, s.Port)
}
fmt.Println(s) // Server{10.0.0.1:8080} —— %v 自动调用
error:标准错误接口
type error interface { Error() string }
// 自定义错误类型(比 fmt.Errorf 多了类型信息, 可断言分类处理)
type ConfigError struct {
File string
Err error
}
func (e *ConfigError) Error() string {
return fmt.Sprintf("config %s: %v", e.File, e.Err)
}
六、实战:用接口解耦
type Notifier interface {
Notify(title, body string) error
}
type DingtalkNotifier struct{ Webhook string }
func (d DingtalkNotifier) Notify(t, b string) error { /* ... */ return nil }
type EmailNotifier struct{ SMTP string }
func (e EmailNotifier) Notify(t, b string) error { /* ... */ return nil }
// 告警系统只依赖接口, 不关心具体渠道
type Alerter struct{ N Notifier }
func (a Alerter) Fire(title string) {
_ = a.N.Notify(title, time.Now().String())
}
// 切换渠道只改这一行
alert := Alerter{N: DingtalkNotifier{Webhook: "https://..."}}
alert.Fire("CPU high")
这就是 Go 风格的多态:小接口 + 组合 + 依赖注入。
小结
| 概念 | Go 的做法 |
|---|---|
| 类 | struct + 方法 |
| 继承 | 嵌入组合(无多态链) |
| 接口 | 隐式实现、小接口、面向能力 |
| 多态 | 接口变量持有动态类型 |
| 陷阱 | nil 接口、值/指针接收者混用 |
本文是「Golang」系列第 4 篇。