Skip to content

结构体与接口

Go 里没有传统面向对象的 class。它用结构体组织数据,用方法绑定行为,用接口描述“一个对象需要具备哪些方法”。运维工具里常见的主机、检查项、告警结果、K8s 资源摘要,都适合用结构体表达。

结构体解决的是“多字段数据怎么放在一起”的问题;接口解决的是“不同实现怎么用同一套调用方式”的问题。

一、结构体

结构体把一组字段放在一起:

go
package main

import "fmt"

type Host struct {
	Name string
	IP   string
	Role string
}

func main() {
	host := Host{
		Name: "web01",
		IP:   "192.168.10.11",
		Role: "web",
	}

	fmt.Println(host.Name, host.IP, host.Role)
}

字段名首字母大写表示导出,其他包可以访问;首字母小写表示只在当前包内可见。

二、结构体方法

方法是带接收者的函数。

go
type Host struct {
	Name string
	IP   string
	Role string
}

func (h Host) Address() string {
	// 值接收者适合只读,不修改结构体字段
	return h.Name + "(" + h.IP + ")"
}

调用:

go
host := Host{Name: "web01", IP: "192.168.10.11", Role: "web"}
fmt.Println(host.Address())

需要修改字段时用指针接收者:

go
type CheckResult struct {
	Name    string
	OK      bool
	Message string
}

func (r *CheckResult) MarkFailed(message string) {
	// 指针接收者会修改原对象
	r.OK = false
	r.Message = message
}

经验上,结构体比较大、需要修改字段、或者方法里要保持一致行为时,优先用指针接收者。

三、JSON 标签

运维工具经常读写 JSON。结构体字段可以通过 tag 控制 JSON 字段名。

go
type Host struct {
	Name string `json:"name"`
	IP   string `json:"ip"`
	Role string `json:"role"`
}

解析 JSON:

go
package main

import (
	"encoding/json"
	"fmt"
)

type Host struct {
	Name string `json:"name"`
	IP   string `json:"ip"`
	Role string `json:"role"`
}

func main() {
	raw := []byte(`{"name":"web01","ip":"192.168.10.11","role":"web"}`)

	var host Host
	if err := json.Unmarshal(raw, &host); err != nil {
		fmt.Println("parse json failed:", err)
		return
	}

	fmt.Println(host.Name, host.IP, host.Role)
}

输出 JSON:

go
data, err := json.MarshalIndent(host, "", "  ")
if err != nil {
	return err
}
fmt.Println(string(data))

json tag 和字段导出有关。字段首字母小写时,encoding/json 默认无法写入这个字段。

四、组合

Go 用组合复用字段和方法。

go
type Metadata struct {
	Name   string
	Labels map[string]string
}

type ServiceCheck struct {
	Metadata
	URL     string
	Timeout int
}

使用:

go
check := ServiceCheck{
	Metadata: Metadata{
		Name: "api-health",
		Labels: map[string]string{
			"env": "prod",
		},
	},
	URL:     "https://example.com/health",
	Timeout: 5,
}

fmt.Println(check.Name)
fmt.Println(check.URL)

组合不是继承。它只是把一个结构体嵌进另一个结构体里,让字段和方法能被外层直接访问。

五、接口

接口定义一组方法。只要某个类型实现了这些方法,就自动满足接口,不需要显式声明。

go
type Checker interface {
	Check() CheckResult
}

实现接口:

go
type HTTPChecker struct {
	Name string
	URL  string
}

func (h HTTPChecker) Check() CheckResult {
	// 示例里省略真实 HTTP 请求
	return CheckResult{Name: h.Name, OK: true, Message: "ok"}
}

统一调用:

go
func RunCheck(c Checker) CheckResult {
	return c.Check()
}

接口适合把“怎么检查”隐藏起来。HTTP 检查、TCP 检查、磁盘检查都可以实现 Checker,上层只关心结果结构。

六、常见接口:error

error 是 Go 标准库里最常见的接口:

go
type error interface {
	Error() string
}

自定义错误类型:

go
type CheckError struct {
	Target string
	Reason string
}

func (e CheckError) Error() string {
	return e.Target + ": " + e.Reason
}

使用:

go
func validateTarget(target string) error {
	if target == "" {
		return CheckError{Target: target, Reason: "empty target"}
	}
	return nil
}

日常工具里大多数场景用 fmt.Errorf 足够,自定义错误类型适合调用方需要区分错误类别的情况。

七、接口不要过早抽象

接口不是越多越好。一个类型只有一个实现时,直接用结构体更清楚。接口适合这些场景:

场景例子
有多个实现HTTP 检查、TCP 检查、命令检查都实现 Checker
需要替换外部依赖真实 K8s Client 和测试 Fake Client
调用方只关心行为只关心 Check(),不关心具体类型

运维工具里最容易过度设计的是一上来就写很多接口。先把数据结构、输入输出和错误处理写清楚,出现第二个实现时再抽接口更稳。

八、一个检查结果模型

go
package main

import (
	"encoding/json"
	"fmt"
	"time"
)

type CheckResult struct {
	Name      string        `json:"name"`
	Target    string        `json:"target"`
	OK        bool          `json:"ok"`
	Message   string        `json:"message"`
	Cost      time.Duration `json:"-"`
	CostMilli int64         `json:"cost_ms"`
}

func NewResult(name, target string, ok bool, message string, cost time.Duration) CheckResult {
	return CheckResult{
		Name:      name,
		Target:    target,
		OK:        ok,
		Message:   message,
		Cost:      cost,
		CostMilli: cost.Milliseconds(),
	}
}

func main() {
	result := NewResult("api", "https://example.com/health", true, "ok", 120*time.Millisecond)

	data, err := json.MarshalIndent(result, "", "  ")
	if err != nil {
		fmt.Println("marshal result failed:", err)
		return
	}

	fmt.Println(string(data))
}

Cost 用于程序内部计算,CostMilli 用于 JSON 输出。json:"-" 表示这个字段不输出到 JSON。这样的结构比直接拼字符串更适合后续写文件、发接口或接监控。