编程语言

Go教程(二)结构体、接口与错误处理

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. 练习

  1. 为 Dispatcher 增加 WebhookSender,使用 net/http 发送 POST。
  2. 定义 Shape 接口(Area() float64),实现 Circle、Rect,并按面积排序。
  3. 实现泛型 GroupBy[T any, K comparable](items []T, key func(T) K) map[K][]T。

文章评论 (0)