第 5 章 · 结构体与方法
本章目标:定义 struct、字段标签、方法 receiver(值 vs 指针);实现与全站对齐的 Product(slug、is_published、价格分)与 OrderLine;理解 Go 组合优于继承;掌握 JSON 序列化基础;对照 java-dev ch05 OOP 与 python-dev ch05 dataclass。
学时建议:3~4 小时(含 2 小时跟练)
前置:完成 go-dev ch04;建议已学 java-dev ch05。
5.1 场景说明:商品与订单行模型
toolkit-go(~/learn-go/toolkit-go,模块 example.com/toolkit-go)在 ch11 将从 JSON/CSV 读商品与日志;字段与 django-web / gin-web api-go-demo 对齐:
| 字段 | 类型 | 说明 |
|---|---|---|
Slug | string | URL 友好标识,唯一 |
Name | string | 展示名称 |
Price | int64 | 分,如 6800 = 68.00 元 |
IsPublished | bool | 是否上架(非 is_active) |
internal/model/
├── product.go
├── order_line.go
├── order.go
└── catalog.go # 嵌入组合示例
5.2 定义结构体与 JSON 标签
// internal/model/product.go
package model
import (
"encoding/json"
"fmt"
)
type Product struct {
ID uint `json:"id"`
Slug string `json:"slug"`
Name string `json:"name"`
Price int64 `json:"price"` // 分
IsPublished bool `json:"is_published"`
}
func (p Product) PriceYuan() float64 {
return float64(p.Price) / 100
}
func (p Product) DisplayPrice() string {
return fmt.Sprintf("%.2f", p.PriceYuan())
}
func (p Product) IsOnSale() bool {
return p.IsPublished && p.Price > 0
}
| 标签 | 用途 |
|---|---|
` json:"slug" ` | encoding/json 序列化字段名 |
` json:"price,omitempty" ` | 零值省略(选修) |
| 导出字段 | 首字母大写才可被外部包 / JSON 访问 |
| Java | Go | Python |
|---|---|---|
class Product | type Product struct | @dataclass class Product |
| getter/setter | 方法 + 导出字段 | 属性 |
| Jackson 注解 | struct tag | pydantic Field |
5.3 值 receiver vs 指针 receiver
// 指针 receiver:可修改字段
func (p *Product) Publish() {
p.IsPublished = true
}
func (p *Product) Unpublish() {
p.IsPublished = false
}
func (p *Product) SetPriceCents(cents int64) error {
if cents < 0 {
return fmt.Errorf("negative price")
}
p.Price = cents
return nil
}
func (p *Product) DiscountPercent(pct int) error {
if pct < 0 || pct > 100 {
return fmt.Errorf("invalid discount: %d", pct)
}
discounted := p.Price * int64(100-pct) / 100
p.Price = discounted
return nil
}
| receiver | 何时用 |
|---|---|
值 (p Product) | 只读、小对象 |
指针 (p *Product) | 修改字段、避免大 struct 拷贝 |
一致性建议:若任一方法用指针 receiver,同一类型的方法尽量统一用指针,避免混用导致困惑。Go 会自动对值调用指针方法(取地址),反之亦然。
5.4 OrderLine 与 Order 模型
// internal/model/order_line.go
package model
type OrderLine struct {
Product Product
Quantity int
}
func (l OrderLine) SubtotalCents() int64 {
return l.Product.Price * int64(l.Quantity)
}
// internal/model/order.go
type Order struct {
ID uint
Lines []OrderLine
}
func (o Order) TotalCents() int64 {
var total int64
for _, ln := range o.Lines {
total += ln.SubtotalCents()
}
return total
}
func (o Order) ItemCount() int {
var n int
for _, ln := range o.Lines {
n += ln.Quantity
}
return n
}
5.5 构造辅助函数 NewProduct
Go 无构造器关键字,惯用 NewXxx 返回 (*Xxx, error):
func NewProduct(slug, name string, priceCents int64) (*Product, error) {
if slug == "" {
return nil, fmt.Errorf("slug required")
}
if priceCents < 0 {
return nil, fmt.Errorf("invalid price")
}
return &Product{
Slug: slug,
Name: name,
Price: priceCents,
IsPublished: false,
}, nil
}
| Java | Go |
|---|---|
new Product(...) | NewProduct(...) |
| 构造器 overload | 不同函数名或 Functional Options(进阶) |
5.6 嵌入 struct(组合)
Go 无类继承,用嵌入复用字段(promoted fields):
// internal/model/catalog.go
package model
type Timestamped struct {
CreatedAt int64 // Unix 秒
UpdatedAt int64
}
type CatalogProduct struct {
Product
Timestamped
Tags []string
}
func NewCatalogProduct(p Product, tags []string) CatalogProduct {
now := int64(1700000000) // 教学固定值
return CatalogProduct{
Product: p,
Timestamped: Timestamped{CreatedAt: now, UpdatedAt: now},
Tags: tags,
}
}
cp := NewCatalogProduct(Product{Slug: "go-book", Name: "Go 实战", Price: 6800}, []string{"book", "go"})
_ = cp.Slug // promoted:直接访问嵌入字段
_ = cp.CreatedAt
对照 java-dev:extends Product → Go embedding + interface(ch06)。
5.7 JSON 往返
func ProductFromJSON(data []byte) (Product, error) {
var p Product
err := json.Unmarshal(data, &p)
return p, err
}
func (p Product) ToJSON() ([]byte, error) {
return json.Marshal(p)
}
func FilterPublished(products []Product) []Product {
out := make([]Product, 0, len(products))
for _, p := range products {
if p.IsPublished {
out = append(out, p)