Skip to content

包管理与标准库

Go 项目依赖由 Go Modules 管理,核心文件是 go.modgo.sum。标准库覆盖了文件、HTTP、JSON、时间、日志、命令行参数、进程调用和上下文控制,很多运维工具不需要引入太多第三方包。

一、go.mod 和 go.sum

创建模块:

bash
mkdir ops-checker
cd ops-checker
go mod init example.com/ops-checker

生成的 go.mod

go
module example.com/ops-checker

go 1.22

module 是模块路径,通常写成代码仓库路径。go 行表示这个模块声明的 Go 语言版本。go.sum 记录依赖校验信息,依赖下载后自动生成。

添加依赖通常不手工编辑 go.mod,而是通过 go get 或代码 import 后执行 go mod tidy

bash
go get github.com/spf13/cobra
go mod tidy

常用命令:

命令作用
go mod init初始化模块
go get添加或升级依赖
go mod tidy清理未使用依赖,补齐缺失依赖
go mod download下载依赖到本地缓存
go list -m all查看依赖列表
go mod vendor把依赖复制到 vendor/

go mod tidy 会根据源码里的 import 自动调整依赖。提交代码前跑一次,能减少无用依赖和缺失依赖。

二、模块代理和私有仓库

国内网络常用代理:

bash
go env -w GOPROXY=https://goproxy.cn,direct
go env GOPROXY

私有仓库:

bash
go env -w GOPRIVATE=git.example.com/*
go env GOPRIVATE

GOPRIVATE 告诉 Go:匹配这些路径的模块属于私有模块,不走公共代理和公共校验数据库。公司内网 GitLab、Gitea、GitHub Enterprise 都会用到这个配置。

三、项目结构

简单命令行工具:

text
ops-checker/
├── go.mod
├── go.sum
├── main.go
├── internal/
│   └── checker/
│       └── http.go
└── README.md

目录约定:

目录用途
main.go程序入口
internal/只允许当前模块内部引用的包
cmd/多命令入口,大项目常用
pkg/对外复用包,普通小工具不一定需要

小工具不需要一开始就拆很复杂。单个 main.go 能写清楚时就先保持简单;逻辑变多后再拆到 internal/

四、defer

defer 用来延迟执行,常用于关闭文件、关闭连接、释放锁。

go
file, err := os.Open("/etc/hosts")
if err != nil {
	return err
}
defer file.Close() // 函数返回前关闭文件

defer 的执行顺序是后进先出。多个资源依次打开时,关闭顺序会反过来。

五、context

context.Context 用于传递取消信号、超时和请求级信息。HTTP 请求、数据库请求、K8s Client 调用都会用到它。

go
package main

import (
	"context"
	"fmt"
	"time"
)

func main() {
	ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
	defer cancel()

	select {
	case <-time.After(1 * time.Second):
		fmt.Println("work done")
	case <-ctx.Done():
		fmt.Println("timeout:", ctx.Err())
	}
}

带超时的上下文能防止网络请求无限等待。client-go、HTTP Client、数据库 Client 里都要重视超时。

六、JSON

解析 JSON:

go
package main

import (
	"encoding/json"
	"fmt"
)

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

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

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

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

输出 JSON:

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

字段要能被 JSON 包访问,首字母需要大写。tag 控制输出字段名。

七、HTTP 标准库

Go 标准库自带 HTTP Client。

go
package main

import (
	"context"
	"fmt"
	"net/http"
	"time"
)

func main() {
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()

	req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://example.com/health", nil)
	if err != nil {
		fmt.Println("create request failed:", err)
		return
	}

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		fmt.Println("request failed:", err)
		return
	}
	defer resp.Body.Close()

	fmt.Println(resp.StatusCode)
}

HTTP 请求要带超时。运维巡检工具如果没有超时,一批目标里一两个地址卡住,整个任务会一直等。

八、命令行参数

标准库 flag 可以处理简单参数。

go
package main

import (
	"flag"
	"fmt"
)

func main() {
	host := flag.String("host", "127.0.0.1", "target host")
	port := flag.Int("port", 80, "target port")
	flag.Parse()

	fmt.Printf("host=%s port=%d\n", *host, *port)
}

运行:

bash
go run . -host 192.168.10.11 -port 8080

参数很多、需要子命令时,可以考虑 cobra 这类第三方库。小工具先用标准库 flag 足够。

九、日志

标准库 log/slog 支持结构化日志。

go
package main

import (
	"log/slog"
	"os"
)

func main() {
	logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))

	logger.Info(
		"check result",
		"host", "web01",
		"service", "nginx",
		"status", "ok",
	)
}

结构化日志比纯字符串更适合被日志平台检索。字段名稳定,后面按 hostservicestatus 查起来更方便。

十、调用系统命令

go
package main

import (
	"bytes"
	"context"
	"fmt"
	"os/exec"
	"time"
)

func main() {
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()

	cmd := exec.CommandContext(ctx, "systemctl", "is-active", "nginx")

	var stdout bytes.Buffer
	var stderr bytes.Buffer
	cmd.Stdout = &stdout
	cmd.Stderr = &stderr

	if err := cmd.Run(); err != nil {
		fmt.Println("command failed:", err, stderr.String())
		return
	}

	fmt.Println("state:", stdout.String())
}

调用命令时优先用参数列表,不拼一整段 shell 字符串。外部输入混进 shell 字符串时,容易引出命令注入问题。