Appearance
结构体与接口
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。这样的结构比直接拼字符串更适合后续写文件、发接口或接监控。