Go 02:结构体、接口与错误处理
Go 没有类和继承。它的组合方式是:struct(数据)+ 方法 + 隐式接口 + 嵌入(组合)。
1. 结构体与方法
type Post struct {
ID int
Title string
Content string
Tags []string
CreatedAt time.Time
views int // 小写:包外不可见(私有)
}
// 构造函数是普通函数,约定命名 NewXxx(≈ __init__)
func NewPost(title, content string) *Post {
return &Post{Title: title, Content: content, CreatedAt: time.Now()}
}
// 值接收者:只读
func (p Post) Summary() string {
if len(p.Content) > 20 {
return p.Content[:20] + "..."
}
return p.Content
}
// 指针接收者:需要修改字段(最常用)
func (p *Post) AddView() {
p.views++
}
post := NewPost("Hello", "Go is simple and fast")
post.AddView()
fmt.Println(post.Summary())
fmt.Printf("%+v\n", *post) // 打印字段名和值,调试很有用
经验:只要有一个方法需要指针接收者,该类型的所有方法都统一用指针接收者。
2. 结构体标签 —— JSON 序列化
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email,omitempty"` // 空值时不输出
Password string `json:"-"` // 永不输出
}
u := User{ID: 1, Name: "bjiang", Password: "secret"}
data, _ := json.Marshal(u) // ≈ json.dumps
fmt.Println(string(data)) // {"id":1,"name":"bjiang"}
var u2 User
err := json.Unmarshal([]byte(`{"id":2,"name":"tom"}`), &u2) // ≈ json.loads
3. 嵌入(组合代替继承)
type BaseModel struct {
ID int
CreatedAt time.Time
}
func (b BaseModel) Age() time.Duration { return time.Since(b.CreatedAt) }
type Comment struct {
BaseModel // 嵌入:Comment 自动拥有 ID、CreatedAt、Age()
PostID int
Body string
}
c := Comment{BaseModel: BaseModel{ID: 1, CreatedAt: time.Now()}, Body: "nice"}
fmt.Println(c.ID, c.Age())
类似 Django 的 abstract Model 基类,但它是"has-a"而不是"is-a"。
4. 接口:隐式实现(编译期检查的 duck typing)
type Notifier interface {
Send(to, message string) error
}
type EmailNotifier struct{ SMTPHost string }
func (e EmailNotifier) Send(to, msg string) error {
fmt.Printf("[email via %s] to=%s msg=%s\n", e.SMTPHost, to, msg)
return nil
}
type SMSNotifier struct{ APIKey string }
func (s SMSNotifier) Send(to, msg string) error {
if len(msg) > 70 {
return fmt.Errorf("sms too long: %d chars", len(msg))
}
fmt.Printf("[sms] to=%s msg=%s\n", to, msg)
return nil
}
// 不需要写 implements,只要方法签名对上就自动满足接口
func NotifyAll(notifiers []Notifier, to, msg string) {
for _, n := range notifiers {
if err := n.Send(to, msg); err != nil {
fmt.Println("send failed:", err)
}
}
}
NotifyAll([]Notifier{EmailNotifier{"smtp.local"}, SMSNotifier{"key"}}, "bjiang", "hello")
Go 接口设计原则:
- 接口要小:标准库里大量只有 1 个方法的接口,如
io.Reader、io.Writer、fmt.Stringer。 - 在使用方定义接口,而不是在实现方(与 C# 习惯相反)。
- "接受接口,返回结构体"。
常用的标准库接口
// fmt.Stringer ≈ Python 的 __str__
func (p Post) String() string { return fmt.Sprintf("Post(%d, %q)", p.ID, p.Title) }
// io.Writer:任何能"写"的东西(文件、网络连接、HTTP 响应、buffer)
func writeReport(w io.Writer, posts []Post) {
for _, p := range posts {
fmt.Fprintln(w, p)
}
}
writeReport(os.Stdout, posts) // 输出到屏幕
f, _ := os.Create("report.txt")
writeReport(f, posts) // 输出到文件
类型断言与 type switch
var x any = 42 // any = interface{},≈ Python 的 object
if n, ok := x.(int); ok {
fmt.Println("int:", n)
}
switch v := x.(type) {
case int:
fmt.Println("int", v)
case string:
fmt.Println("string", v)
default:
fmt.Println("unknown")
}
5. 错误处理
Go 没有 try/except。错误是一个普通的值(实现了 Error() string 的接口)。
// 1) 简单错误
errors.New("not found")
fmt.Errorf("user %d not found", id)
// 2) 包装错误(保留原因链)≈ raise ... from e
if err != nil {
return fmt.Errorf("load config: %w", err)
}
// 3) 哨兵错误 + errors.Is ≈ except SpecificError
var ErrNotFound = errors.New("not found")
func FindPost(id int) (*Post, error) {
return nil, ErrNotFound
}
_, err := FindPost(1)
if errors.Is(err, ErrNotFound) {
// 返回 404
}
// 4) 自定义错误类型 + errors.As(需要携带额外信息时)
type ValidationError struct {
Field string
Msg string
}
func (e *ValidationError) Error() string { return e.Field + ": " + e.Msg }
func validate(p Post) error {
if p.Title == "" {
return &ValidationError{Field: "title", Msg: "required"}
}
return nil
}
var ve *ValidationError
if errors.As(validate(Post{}), &ve) {
fmt.Println("invalid field:", ve.Field)
}
panic / recover
panic 只用于"程序无法继续"的错误(如程序员 bug),业务错误一律用 error 返回。recover 通常只在 HTTP 中间件等最外层使用(见 04 篇)。
6. 泛型(Go 1.18+)
func Map[T, R any](items []T, fn func(T) R) []R {
result := make([]R, 0, len(items))
for _, item := range items {
result = append(result, fn(item))
}
return result
}
func Filter[T any](items []T, fn func(T) bool) []T {
var result []T
for _, item := range items {
if fn(item) {
result = append(result, item)
}
}
return result
}
type Number interface{ ~int | ~int64 | ~float64 }
func Sum[T Number](nums []T) T {
var total T
for _, n := range nums {
total += n
}
return total
}
titles := Map(posts, func(p Post) string { return p.Title })
Go 的泛型故意比 C#/TS 弱,用于工具函数和容器即可,不要过度设计。
7. 应用场景:可插拔的消息发送服务(类似 EAAS 的通道抽象)
package notify
import (
"errors"
"fmt"
)
type Channel string
const (
ChannelEmail Channel = "email"
ChannelSMS Channel = "sms"
)
type Message struct {
Channel Channel
To string
Body string
}
type Sender interface {
Send(msg Message) error
}
var ErrUnsupportedChannel = errors.New("unsupported channel")
type Dispatcher struct {
senders map[Channel]Sender
}
func NewDispatcher() *Dispatcher {
return &Dispatcher{senders: make(map[Channel]Sender)}
}
func (d *Dispatcher) Register(ch Channel, s Sender) {
d.senders[ch] = s
}
func (d *Dispatcher) Dispatch(msg Message) error {
s, ok := d.senders[msg.Channel]
if !ok {
return fmt.Errorf("dispatch %q: %w", msg.Channel, ErrUnsupportedChannel)
}
if err := s.Send(msg); err != nil {
return fmt.Errorf("dispatch to %s: %w", msg.To, err)
}
return nil
}
测试时可以注入一个假的 Sender(≈ Python 的 mock):
type fakeSender struct{ sent []Message }
func (f *fakeSender) Send(m Message) error {
f.sent = append(f.sent, m)
return nil
}
8. 练习
- 为
Dispatcher增加WebhookSender,使用net/http发送 POST。 - 定义
Shape接口(Area() float64),实现 Circle、Rect,并按面积排序。 - 实现泛型
GroupBy[T any, K comparable](items []T, key func(T) K) map[K][]T。
文章评论 (0)