Go:标准库里最值得先掌握的能力,为什么是 io、os、context、net/http、json

学 Go 学到这里时,很容易开始有一种错觉:

  • 语法已经差不多了
  • structinterfaceerror 也都看过了
  • 接下来是不是该直接去学 goroutine、框架、微服务、ORM

这条路不算错,但很容易出一个现实问题:

  • 能看懂语法
  • 能写几个函数
  • 也知道怎么定义类型
  • 可一旦开始做真实项目,马上又卡回基础层

最常见的卡点不是“不会写 for”,而是这些:

  • 配置文件怎么读
  • 环境变量怎么接
  • HTTP 请求怎么设超时和取消
  • JSON 怎么解码才不至于一把梭
  • 结果怎么写回文件、标准输出或别的 writer

换句话说,问题往往不在 Go 语法本身,而在最常用的标准库能力还没接成一条完整工程链路

如果只能先从 Go 标准库里挑几块最该优先掌握的能力,更值得优先抓这五个:

  • io
  • os
  • context
  • net/http
  • encoding/json

原因很简单:它们几乎会同时出现在绝大多数真实程序里。

你写命令行工具会用到它们。
你写 HTTP 服务会用到它们。
你写测试平台脚本、数据采集器、巡检器、任务调度器,也会用到它们。

这一篇不按“文档目录”讲,而是围绕一个真实小项目来讲:做一个最小的服务巡检器

它要完成这些动作:

  1. 从本地 JSON 文件读取服务清单
  2. 结合环境变量和命令行参数确定输出路径与超时
  3. context 控制整个巡检批次的超时
  4. net/http 请求服务健康检查接口
  5. json 解码返回结果、编码巡检报告
  6. ioos 把输入输出真正接起来

这个项目不大,但足够把这五块能力串成一条很像工程代码的主线。

一、这篇文章要解决什么问题

读完这一篇,应该能独立回答这些问题:

  1. 为什么 iooscontextnet/httpjson 比很多“更高级”的主题更值得先掌握
  2. io.Readerio.Writer 在 Go 项目里到底解决什么问题
  3. os 除了打开文件,还负责哪些与运行环境相关的动作
  4. context 为什么不是“高级并发技巧”,而是请求边界控制的基础设施
  5. net/http 为什么不只是“发个 GET 请求”
  6. json 为什么不能只停留在 json.Unmarshal 会用
  7. 怎么把这几块能力组织成一份最小但真实的项目骨架

如果这些问题能说清楚,后面再学并发、HTTP 服务、中间件、数据库访问,会顺很多。

二、先把一句话结论说清楚

Go 标准库里最值得先掌握的这五块能力,本质上分别在解决五类最常见的工程问题:

  • io 解决“数据从哪里来、往哪里去、如何抽象输入输出边界”
  • os 解决“程序如何和操作系统环境打交道”
  • context 解决“一个请求或任务的生命周期如何被控制”
  • net/http 解决“如何可靠地发起或承接 HTTP 通信”
  • json 解决“结构化数据如何在程序内外流动”

它们不是彼此独立的知识点,而是一条链:

os 提供文件和环境入口,io 统一读写抽象,json 负责结构化编解码,net/http 负责网络交互,context 负责把超时、取消、截止时间贯穿整条调用链。

真实项目里,这五个包往往是同时出现的。

三、先看这篇文章最终要做的小项目

假设你在做一个最小的服务巡检器,它每天读取一份服务清单,对每个服务发起健康检查请求,然后把巡检结果写成报告文件。

输入文件 services.json

1
2
3
4
5
6
7
8
9
10
[
{
"name": "order-api",
"url": "http://127.0.0.1:8080/health"
},
{
"name": "user-api",
"url": "http://127.0.0.1:8081/health"
}
]

健康检查接口返回:

1
2
3
4
{
"status": "ok",
"version": "1.2.3"
}

最终输出报告 report.json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"generated_at": "2026-04-18T10:30:00Z",
"results": [
{
"name": "order-api",
"status": "ok",
"version": "1.2.3"
},
{
"name": "user-api",
"status": "down",
"error": "context deadline exceeded"
}
]
}

这个小项目里:

  • os 负责打开输入文件、创建输出文件、读取环境变量
  • io 负责把文件、HTTP 响应体和标准输出抽象成统一读写接口
  • json 负责把文件内容和 HTTP 响应解码成 struct,再把报告编码回 JSON
  • net/http 负责发起请求和处理响应
  • context 负责控制整个批次和单次请求的超时边界

这正是标准库能力最典型的联动方式。

四、先给一个最小可运行版本

先看一个最小骨架,别急着一次把所有边界讲满。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
package main

import (
"context"
"encoding/json"
"fmt"
"net/http"
"os"
"time"
)

type Service struct {
Name string `json:"name"`
URL string `json:"url"`
}

type HealthResponse struct {
Status string `json:"status"`
Version string `json:"version"`
}

func main() {
file, err := os.Open("services.json")
if err != nil {
panic(err)
}
defer file.Close()

var services []Service
if err := json.NewDecoder(file).Decode(&services); err != nil {
panic(err)
}

client := &http.Client{Timeout: 2 * time.Second}
ctx := context.Background()

for _, svc := range services {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, svc.URL, nil)
if err != nil {
fmt.Println("build request failed:", err)
continue
}

resp, err := client.Do(req)
if err != nil {
fmt.Println("request failed:", err)
continue
}

var body HealthResponse
if err := json.NewDecoder(resp.Body).Decode(&body); err != nil {
fmt.Println("decode failed:", err)
resp.Body.Close()
continue
}
resp.Body.Close()

fmt.Printf("%s -> %s (%s)\n", svc.Name, body.Status, body.Version)
}
}

这个例子已经把主线带出来了:

  • os.Open 开始接触文件
  • json.NewDecoder 从 reader 解码
  • http.Client 发起请求
  • context 作为请求生命周期入口
  • 用响应体这个 reader 再次做 JSON 解码

它还不够好,但已经很像真实项目的最小切面。

五、为什么 io 应该最先掌握

io 很容易被误解成“读写文件的包”。

这不够准确。

io 真正重要的地方,是它用极少的接口把“输入输出边界”统一了。

最核心的两个接口是:

1
2
3
4
5
6
7
type Reader interface {
Read(p []byte) (n int, err error)
}

type Writer interface {
Write(p []byte) (n int, err error)
}

它们看起来很简单,但意义非常大:

  • 文件可以是 Reader
  • HTTP 响应体可以是 Reader
  • 字符串缓冲区可以是 Reader
  • 标准输出可以是 Writer
  • 文件也可以是 Writer

一旦你脑子里建立了这个抽象,代码组织方式会立刻变稳。

比如下面这个函数就不关心输入来自文件、内存还是网络:

1
2
3
4
5
6
7
func loadServices(r io.Reader) ([]Service, error) {
var services []Service
if err := json.NewDecoder(r).Decode(&services); err != nil {
return nil, err
}
return services, nil
}

这就是 io 的价值:

  • 把逻辑和具体介质解耦
  • 让测试更容易写
  • 让函数职责更清晰

Go 测试之所以常显得顺手,本质上也是因为 io.Readerio.Writer 这类边界抽象用得对。

六、为什么 os 不只是打开文件

如果说 io 在抽象“流”,那 os 更像是在处理“程序运行现场”。

os 常见的职责至少包括:

  • 打开、创建、删除文件和目录
  • 读取环境变量
  • 获取标准输入输出
  • 获取进程退出码
  • 判断文件是否存在

比如在服务巡检器里,os 至少会出现在这些地方:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
inputPath := os.Getenv("INSPECT_INPUT")
if inputPath == "" {
inputPath = "services.json"
}

outputPath := os.Getenv("INSPECT_OUTPUT")
if outputPath == "" {
outputPath = "report.json"
}

file, err := os.Open(inputPath)
if err != nil {
return err
}
defer file.Close()

这里要先建立一个工程判断:

  • os 层负责拿到运行环境资源
  • 业务层不应该直接到处读环境变量

更稳的做法通常是:

  1. 程序入口集中读取 os.Argsos.Getenv
  2. 转成明确配置 struct
  3. 再把配置传给业务逻辑

这样后面补测试、改默认值和定位问题都会轻很多。

七、为什么 context 不是高级主题,而是基础能力

第一次接触 context 时,很容易把它放到“并发进阶”那一栏。

这其实容易学歪。

context 最重要的职责不是炫技,而是:

  • 携带截止时间
  • 触发取消信号
  • 在调用链中传递请求级元数据

最常见的基础用法是超时控制:

1
2
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()

然后把这个 ctx 一路传下去:

1
req, err := http.NewRequestWithContext(ctx, http.MethodGet, svc.URL, nil)

为什么这很重要?

因为真实项目里,最危险的不是请求失败,而是请求不失败也不结束

比如:

  • 下游服务卡住
  • DNS 解析慢
  • TCP 连接一直挂着
  • 某个环节没有超时,整个任务就拖死

如果没有 context,这种问题往往会在项目上线后才开始咬人。

八、为什么 net/http 不能只会“发个请求”

Go 的 net/http 看起来很简单,第一版通常会先写成:

1
resp, err := http.Get(url)

它能跑,但不适合作为工程默认写法。

更稳的方式通常至少要做到这些:

  • 显式创建 http.Client
  • 设定超时
  • 使用 context
  • 检查状态码
  • 关闭响应体

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
client := &http.Client{
Timeout: 2 * time.Second,
}

req, err := http.NewRequestWithContext(ctx, http.MethodGet, svc.URL, nil)
if err != nil {
return HealthResult{}, fmt.Errorf("build request for %s: %w", svc.Name, err)
}

resp, err := client.Do(req)
if err != nil {
return HealthResult{}, fmt.Errorf("request %s: %w", svc.Name, err)
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
return HealthResult{}, fmt.Errorf("service %s returned status %d", svc.Name, resp.StatusCode)
}

这段代码的重点不是语法,而是边界意识:

  • 请求的生命周期谁控制
  • 请求失败时错误上下文够不够
  • HTTP 状态码异常是否被识别
  • 资源是否一定释放

这才是 net/http 该先掌握的部分。

九、为什么 json 不能只停留在 Unmarshal

JSON 学到这里时,最容易只记住这个:

1
2
var services []Service
err := json.Unmarshal(data, &services)

它当然有用,但真实项目里更常见的其实是流式接口:

  • json.NewDecoder(r).Decode(&v)
  • json.NewEncoder(w).Encode(v)

为什么更值得优先掌握它们?

因为真实数据经常不是先完整放到一个 []byte 里,再交给你解码。

常见场景是:

  • 数据来自文件
  • 数据来自 HTTP 响应体
  • 结果要直接写到文件或网络连接

这时候基于 ReaderWriter 的 Decoder/Encoder 会更自然。

例如从文件读服务清单:

1
2
3
4
5
6
7
func loadServices(r io.Reader) ([]Service, error) {
var services []Service
if err := json.NewDecoder(r).Decode(&services); err != nil {
return nil, fmt.Errorf("decode services: %w", err)
}
return services, nil
}

把报告写回文件:

1
2
3
4
5
6
7
8
func writeReport(w io.Writer, report Report) error {
encoder := json.NewEncoder(w)
encoder.SetIndent("", " ")
if err := encoder.Encode(report); err != nil {
return fmt.Errorf("encode report: %w", err)
}
return nil
}

这时候 jsonioos 已经自然连起来了。

十、把这五块能力真正串成一份最小项目

下面给一个更完整的服务巡检器骨架。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
package inspector

import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"time"
)

type Service struct {
Name string `json:"name"`
URL string `json:"url"`
}

type HealthPayload struct {
Status string `json:"status"`
Version string `json:"version"`
}

type HealthResult struct {
Name string `json:"name"`
Status string `json:"status"`
Version string `json:"version,omitempty"`
Error string `json:"error,omitempty"`
}

type Report struct {
GeneratedAt time.Time `json:"generated_at"`
Results []HealthResult `json:"results"`
}

type Config struct {
InputPath string
OutputPath string
Timeout time.Duration
}

func Run(ctx context.Context, client *http.Client, cfg Config) error {
input, err := os.Open(cfg.InputPath)
if err != nil {
return fmt.Errorf("open input file %s: %w", cfg.InputPath, err)
}
defer input.Close()

services, err := loadServices(input)
if err != nil {
return err
}

results := make([]HealthResult, 0, len(services))
for _, svc := range services {
result := inspectService(ctx, client, svc)
results = append(results, result)
}

output, err := os.Create(cfg.OutputPath)
if err != nil {
return fmt.Errorf("create output file %s: %w", cfg.OutputPath, err)
}
defer output.Close()

report := Report{
GeneratedAt: time.Now().UTC(),
Results: results,
}

if err := writeReport(output, report); err != nil {
return err
}

return nil
}

func loadServices(r io.Reader) ([]Service, error) {
var services []Service
if err := json.NewDecoder(r).Decode(&services); err != nil {
return nil, fmt.Errorf("decode services: %w", err)
}
if len(services) == 0 {
return nil, fmt.Errorf("services list can not be empty")
}
return services, nil
}

func inspectService(ctx context.Context, client *http.Client, svc Service) HealthResult {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, svc.URL, nil)
if err != nil {
return HealthResult{Name: svc.Name, Status: "down", Error: err.Error()}
}

resp, err := client.Do(req)
if err != nil {
return HealthResult{Name: svc.Name, Status: "down", Error: err.Error()}
}
defer resp.Body.Close()

if resp.StatusCode != http.StatusOK {
return HealthResult{
Name: svc.Name,
Status: "down",
Error: fmt.Sprintf("unexpected status code: %d", resp.StatusCode),
}
}

var payload HealthPayload
if err := json.NewDecoder(resp.Body).Decode(&payload); err != nil {
return HealthResult{Name: svc.Name, Status: "down", Error: err.Error()}
}

return HealthResult{
Name: svc.Name,
Status: payload.Status,
Version: payload.Version,
}
}

func writeReport(w io.Writer, report Report) error {
encoder := json.NewEncoder(w)
encoder.SetIndent("", " ")
if err := encoder.Encode(report); err != nil {
return fmt.Errorf("encode report: %w", err)
}
return nil
}

这段代码已经能把五块能力接成一条完整主线:

  • os.Openos.Create 负责资源入口
  • io.Readerio.Writer 负责函数边界抽象
  • json.Decoderjson.Encoder 负责结构化数据流转
  • http.Client 和请求构建负责网络调用
  • context.Context 贯穿整个执行过程

十一、这个设计里最值得先看懂的三个边界

这个小项目虽然简单,但已经包含三个非常关键的工程边界。

1. 入口层负责拿配置和系统资源

os.Getenvos.Openos.Create 这种动作,尽量集中在入口层或基础设施层。

不要把“读环境变量”散落在业务函数内部。
否则测试会很难写,配置入口也会更难追踪。

2. 业务函数尽量吃抽象,不直接吃具体实现

例如 loadServices 吃的是 io.ReaderwriteReport 吃的是 io.Writer

这样测试时就能直接喂 strings.NewReaderbytes.Buffer,不用每次都真的创建文件。

3. 生命周期控制从最外层开始传

context 一旦建立,就要沿调用链往下传。
不要在深层函数里随意 context.Background() 重新起一个上下文,否则取消链路会断。

这是最常见的第一版错误之一。

十二、最容易写坏的第一个版本长什么样

下面这段代码是第一版通常会先写出来的版本:

1
2
3
4
5
6
7
8
9
func inspect(url string) string {
resp, _ := http.Get(url)
data, _ := io.ReadAll(resp.Body)
resp.Body.Close()

var payload map[string]any
json.Unmarshal(data, &payload)
return payload["status"].(string)
}

它几乎把几类典型问题一次踩齐了:

  • 没有超时控制
  • 没有 context
  • 错误全被吞掉
  • 没有状态码检查
  • map[string]any 让结构失去约束
  • 断言失败会直接 panic

这类代码在 demo 阶段很常见,但一上真实项目就会变成事故源。

十三、几个非常典型的错误示例

1. 在深层函数里重新起 context.Background()

错误写法:

1
2
3
4
5
func inspectService(client *http.Client, svc Service) HealthResult {
ctx := context.Background()
req, _ := http.NewRequestWithContext(ctx, http.MethodGet, svc.URL, nil)
...
}

问题在于:

  • 外层即使超时或取消
  • 这里的请求也不一定跟着停

更稳的写法是显式接收 ctx

1
func inspectService(ctx context.Context, client *http.Client, svc Service) HealthResult

2. 忘记关闭响应体

错误写法:

1
2
3
4
5
6
7
8
resp, err := client.Do(req)
if err != nil {
return result
}

var payload HealthPayload
json.NewDecoder(resp.Body).Decode(&payload)
return result

这会导致连接无法及时回收。
更稳的写法是拿到 resp 后立刻:

1
defer resp.Body.Close()

3. 把 json 结构全解到 map[string]any

这在探索接口时可以临时用,但不要默认这样写工程代码。

因为它会带来:

  • 字段拼写缺少约束
  • 类型断言容易出错
  • 后续重构成本高

能定义稳定 struct 时,优先定义 struct。

十四、测试为什么要围绕 Reader、Writer 和 HTTP 边界来补

这类代码如果设计得对,测试并不重。

最值得先补的其实不是“全链路自动化”,而是三类小测试:

  1. loadServices 能否正确解码输入
  2. writeReport 能否正确输出结果
  3. inspectService 在不同 HTTP 响应下是否返回正确状态

前两类因为用了 io.Reader / io.Writer,根本不用真实文件。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func TestLoadServices(t *testing.T) {
input := strings.NewReader(`[{"name":"order-api","url":"http://example.com/health"}]`)

services, err := loadServices(input)
if err != nil {
t.Fatalf("expected nil error, got %v", err)
}
if len(services) != 1 {
t.Fatalf("expected 1 service, got %d", len(services))
}
if services[0].Name != "order-api" {
t.Fatalf("unexpected service name: %s", services[0].Name)
}
}

写报告的测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func TestWriteReport(t *testing.T) {
var buf bytes.Buffer

report := Report{
GeneratedAt: time.Date(2026, 4, 18, 10, 0, 0, 0, time.UTC),
Results: []HealthResult{
{Name: "order-api", Status: "ok", Version: "1.2.3"},
},
}

if err := writeReport(&buf, report); err != nil {
t.Fatalf("expected nil error, got %v", err)
}

output := buf.String()
if !strings.Contains(output, "\"order-api\"") {
t.Fatalf("expected output to contain service name, got %s", output)
}
}

HTTP 部分最自然的验证方式,是用 httptest

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
func TestInspectService(t *testing.T) {
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
w.Write([]byte(`{"status":"ok","version":"1.2.3"}`))
}))
defer server.Close()

client := server.Client()
ctx := context.Background()

result := inspectService(ctx, client, Service{
Name: "order-api",
URL: server.URL,
})

if result.Status != "ok" {
t.Fatalf("expected status ok, got %s", result.Status)
}
if result.Version != "1.2.3" {
t.Fatalf("expected version 1.2.3, got %s", result.Version)
}
}

这里的重点不是会不会写测试语法,而是:

  • io 抽象让文件测试脱离真实文件系统
  • net/http 标准库自带 httptest,非常适合补边界测试
  • context 可以在测试里显式控制生命周期

十五、一个更完整的工程版本应该怎么组织

如果这个巡检器再往前走一步,更稳的拆法通常是下面这种结构:

1
2
3
4
5
6
7
8
9
10
11
12
service_inspector/
├── cmd/
│ └── inspector/
│ └── main.go
├── internal/
│ └── inspector/
│ ├── config.go
│ ├── loader.go
│ ├── checker.go
│ └── report.go
└── testdata/
└── services.json

职责大概是:

  • main.go 负责读环境变量、组装配置、处理退出码
  • loader.go 负责从 io.Reader 解码服务清单
  • checker.go 负责用 context + net/http 发起检查
  • report.go 负责把结果编码到 io.Writer

这样拆的好处是:

  • 每层职责更清楚
  • 每层都容易独立测试
  • 后续加并发、重试、限流时不容易散架

十六、排障时最常见的几个问题

1. context deadline exceeded

这通常说明:

  • 超时时间太短
  • 下游服务响应慢
  • DNS 或网络层有延迟
  • 请求链路里某一层没有及时返回

先确认:

  • http.Client.Timeout 多大
  • context.WithTimeout 多大
  • 有没有重复包了一层更短的超时

2. invalid character ... looking for beginning of value

这通常说明你以为对方返回的是 JSON,实际上不是。

常见原因:

  • 返回了 HTML 错误页
  • 返回了纯文本错误信息
  • 状态码不是 200,但你没先检查

所以正确顺序通常是:

  1. 先看 StatusCode
  2. 再决定是否解码 JSON

3. 输出文件是空的或只有半截

优先排查:

  • os.Create 是否成功
  • json.Encoder.Encode 是否报错
  • 文件是否及时 Close
  • 有没有把输出写到错误路径

4. 单测里 HTTP 请求跑不通

先别急着怀疑网络。
这种测试更适合用 httptest.NewServer,而不是依赖外部地址。

十七、工程里什么时候该优先用这些能力,什么时候不必过度设计

这一组标准库能力非常重要,但也不要一上来就写得过重。

例如:

  • 只是一个几十行的小脚本,os.ReadFile 搭配 json.Unmarshal 也完全可以
  • 只是一次性读一个很小的配置文件,不必为了“纯粹”把所有地方都包成复杂接口
  • 只是一个最小工具,先串起来比一开始就抽五层更重要

但下面这些边界最好尽早建立:

  • 涉及超时和取消时,尽早引入 context
  • 涉及 HTTP 请求时,尽早显式使用 http.Client
  • 涉及文件和网络输入输出时,尽量往 io.Reader / io.Writer
  • 涉及稳定 JSON 结构时,尽量定义 struct,而不是到处 map[string]any

简单说就是:

  • 不要为了设计而设计
  • 也不要为了省事把边界全写塌

十八、这五块能力和后续主题是什么关系

把这五块标准库能力吃透后,后面很多主题都会明显顺手:

  • 学并发时,context 会直接参与 goroutine 生命周期控制
  • 学 HTTP 服务时,net/httpjson 会成为最常见的输入输出层
  • 学测试时,io.Reader / io.Writerhttptest 会让你更容易隔离依赖
  • 学配置管理、日志采集、任务调度时,osio 会反复出现

所以它们不是“学完语法后顺手看一下”的附属品,反而是后面很多工程主题的底层支架。

十九、可以自己动手做的练习

如果你想把这篇文章真正吃透,建议至少做下面四个练习:

  1. 给服务巡检器补一个 -timeout 参数,把秒数转成 time.Duration
  2. 把输出目标从固定文件改成“文件或标准输出”二选一,用 io.Writer 统一处理
  3. 给 HTTP 检查补一层状态码分类,把 5xx 和超时错误分开记录
  4. httptest 补三个测试:正常返回、状态码异常、返回非法 JSON

这四个练习不大,但能把这篇文章的主线真正串起来。

二十、最后收个尾

Go 标准库里值得先掌握的东西很多,但如果只能优先抓一组,更值得先抓的是:

  • io
  • os
  • context
  • net/http
  • json

不是因为它们“高级”,恰恰是因为它们太基础、太常见,而且会同时出现在真实工程里。

这五块能力一旦接顺,你的代码会明显出现几个变化:

  • 输入输出边界更清楚
  • 配置和环境依赖更清楚
  • 请求生命周期更可控
  • 网络调用更稳
  • 结构化数据处理更自然

到这一步,Go 才算从“会写语法”开始走向“能写工程代码”。

后面继续往下学并发、标准库测试能力、数据库访问时,很多内容其实不是新知识,而是在这条主线上的继续展开。