跳到主要内容
预计阅读 35 分钟

标准库精选:io、net/http与JSON处理 — Go的”百宝箱”

Go的标准库就像一个精心设计的工具箱——不像某些语言需要装一堆第三方库才能干活,Go自带的标准库就已经覆盖了绝大多数日常需求。今天我们打开这个工具箱,取出三件最常用的工具。

📋 开篇自测:你已经知道多少?

  1. io.Reader 接口只有一个方法,为什么它却是Go中最重要的接口之一?
  2. 用Go标准库搭一个HTTP服务器需要几行代码?
  3. Go中JSON序列化时,结构体字段名是怎么映射成JSON键名的?

一、io包:数据流的统一语言

1.1 为什么io包如此重要

在编程世界中,数据总是在”流动”——从文件读到内存,从网络写到磁盘,从一个进程传到另一个进程。Go的 io 包用两个极简的接口统一了所有数据流动的方式。

这就像规定了全世界的水龙头和水管都使用同一种标准接口——不管水从哪里来(自来水、矿泉水、雨水),也不管水要到哪里去(杯子、浴缸、游泳池),连接方式都是一样的。

1.2 io.Reader:万物皆可读

type Reader interface {
    Read(p []byte) (n int, err error)
}

就这一个方法,但实现它的类型遍布整个Go生态:

类型说明
*os.Fileos文件
*http.Response.Bodynet/httpHTTP响应体
*bytes.Bufferbytes内存缓冲区
*strings.Readerstrings字符串
*gzip.Readercompress/gzip压缩流
*bufio.Readerbufio带缓冲的读取器

1.3 实战:用io.Reader读取不同来源的数据

package main

import (
    "fmt"
    "io"
    "os"
    "strings"
)

// 这个函数能处理任何实现了io.Reader的数据源
func countBytes(r io.Reader) (int64, error) {
    var total int64
    buf := make([]byte, 1024)
    for {
        n, err := r.Read(buf)
        total += int64(n)
        if err == io.EOF {
            break // 读完了
        }
        if err != nil {
            return total, err
        }
    }
    return total, nil
}

func main() {
    // 读取字符串
    strReader := strings.NewReader("Hello, Go标准库!")
    n, _ := countBytes(strReader)
    fmt.Printf("字符串:%d 字节\n", n)

    // 读取文件
    file, err := os.Open("test.txt")
    if err == nil {
        defer file.Close()
        n, _ = countBytes(file)
        fmt.Printf("文件:%d 字节\n", n)
    }
}

1.4 io包的常用工具函数

// 读取全部内容
data, err := io.ReadAll(reader)

// 复制数据(从Reader到Writer)
written, err := io.Copy(dst, src) // dst是Writer,src是Reader

// 限制读取的字节数
limited := io.LimitReader(reader, 1024) // 最多读1024字节

// 组合多个Reader
multi := io.MultiReader(reader1, reader2, reader3) // 依次读取

// 同时写入多个Writer
multi := io.MultiWriter(writer1, writer2) // 写入时同时写到两个地方

1.5 bufio:带缓冲的IO

直接用 io.Reader 每次读取少量数据效率不高,bufio 包提供了带缓冲的包装:

// 按行读取文件——最常见的文件处理方式
func readLines(filename string) ([]string, error) {
    file, err := os.Open(filename)
    if err != nil {
        return nil, err
    }
    defer file.Close()

    var lines []string
    scanner := bufio.NewScanner(file)
    for scanner.Scan() {
        lines = append(lines, scanner.Text())
    }
    return lines, scanner.Err()
}

bufio.Scanner 就像一个翻译官——把原始的字节流翻译成你想要的格式(行、单词、自定义分隔符)。

🤔 想一想 如果你要写一个函数,把数据写入文件,函数参数应该用 *os.File 还是 io.Writer?为什么?


二、net/http:开箱即用的HTTP能力

2.1 五行代码的HTTP服务器

没有夸张,Go标准库自带的HTTP服务器功能足够强大,甚至可以直接用于生产环境:

package main

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

func main() {
    http.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintf(w, "你好,世界!当前时间:%s", time.Now().Format("15:04:05"))
    })

    fmt.Println("服务器启动在 http://localhost:8080")
    log.Fatal(http.ListenAndServe(":8080", nil))
}

运行 go run main.go,打开浏览器访问 http://localhost:8080/hello,你就拥有了一个HTTP服务器。

这就像Go说:“你不需要安装Nginx或Apache,我自己就是一台Web服务器。“

2.2 理解Handler接口

Go的HTTP核心就是一个接口:

type Handler interface {
    ServeHTTP(ResponseWriter, *Request)
}

http.HandleFunc 只是一个便捷方法,它把一个函数转换成了 Handler。你也可以用结构体来实现:

type APIHandler struct {
    version string
}

func (h *APIHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "API版本:%s\n", h.version)
    fmt.Fprintf(w, "请求方法:%s\n", r.Method)
    fmt.Fprintf(w, "请求路径:%s\n", r.URL.Path)
}

func main() {
    handler := &APIHandler{version: "v1.0"}
    http.Handle("/api/", handler)
    log.Fatal(http.ListenAndServe(":8080", nil))
}

2.3 路由和请求处理

func main() {
    mux := http.NewServeMux() // 创建路由器

    // 静态路由
    mux.HandleFunc("/", homeHandler)
    mux.HandleFunc("/users", usersHandler)
    mux.HandleFunc("/health", healthHandler)

    // Go 1.22+ 新增:支持在路由中指定HTTP方法和路径参数
    mux.HandleFunc("GET /users/{id}", getUserHandler)   // 需要 Go 1.22+
    mux.HandleFunc("POST /users", createUserHandler)    // 需要 Go 1.22+

    log.Fatal(http.ListenAndServe(":8080", mux))
}

func homeHandler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintln(w, "欢迎来到首页")
}

func getUserHandler(w http.ResponseWriter, r *http.Request) {
    id := r.PathValue("id") // Go 1.22+
    fmt.Fprintf(w, "获取用户:%s\n", id)
}

func usersHandler(w http.ResponseWriter, r *http.Request) {
    switch r.Method {
    case http.MethodGet:
        fmt.Fprintln(w, "获取用户列表")
    case http.MethodPost:
        fmt.Fprintln(w, "创建新用户")
    default:
        http.Error(w, "方法不允许", http.StatusMethodNotAllowed)
    }
}

func healthHandler(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(http.StatusOK)
    fmt.Fprintln(w, `{"status": "healthy"}`)
}

2.4 中间件模式

中间件就是包裹在Handler外层的”洋葱皮”——请求先经过外层中间件,然后到达核心Handler,响应再从内到外依次返回:

// 日志中间件
func loggingMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()
        fmt.Printf("[%s] %s %s\n", start.Format("15:04:05"), r.Method, r.URL.Path)

        next.ServeHTTP(w, r) // 调用下一层

        fmt.Printf("[%s] %s %s 耗时 %v\n",
            time.Now().Format("15:04:05"), r.Method, r.URL.Path, time.Since(start))
    })
}

// CORS中间件
func corsMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("Access-Control-Allow-Origin", "*")
        w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE")
        w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")

        if r.Method == "OPTIONS" {
            w.WriteHeader(http.StatusOK)
            return
        }

        next.ServeHTTP(w, r)
    })
}

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/api/data", dataHandler)

    // 洋葱式包裹:请求 -> CORS -> 日志 -> 路由
    handler := corsMiddleware(loggingMiddleware(mux))

    log.Fatal(http.ListenAndServe(":8080", handler))
}

2.5 HTTP客户端

Go的HTTP客户端同样开箱即用:

// 最简单的GET请求
resp, err := http.Get("https://api.example.com/users")
if err != nil {
    log.Fatal(err)
}
defer resp.Body.Close()

body, err := io.ReadAll(resp.Body)
if err != nil {
    log.Fatal(err)
}
fmt.Println(string(body))

自定义请求:

// 创建自定义客户端(设置超时)
client := &http.Client{
    Timeout: 10 * time.Second,
}

// 创建POST请求
payload := strings.NewReader(`{"name":"张三","age":28}`)
req, err := http.NewRequest("POST", "https://api.example.com/users", payload)
if err != nil {
    log.Fatal(err)
}

// 设置请求头
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer your-token-here")

// 发送请求
resp, err := client.Do(req)
if err != nil {
    log.Fatal(err)
}
defer resp.Body.Close()

fmt.Println("状态码:", resp.StatusCode)

⚠️ 常见误区

  • 误区一:忘记关闭Response.Body。HTTP响应的Body必须关闭,否则会导致连接泄漏。用 defer resp.Body.Close()
  • 误区二:在生产环境用默认的 http.DefaultClient。它没有超时设置,一个慢请求会永远挂住。务必设置Timeout。
  • 误区三:认为Go的标准HTTP服务器性能不行。实际上它的性能相当出色,很多公司直接用它来处理生产流量。

三、encoding/json:数据的翻译官

3.1 JSON与Go:天然搭档

在API开发中,JSON是最通用的数据交换格式。Go的 encoding/json 包让JSON和Go结构体之间的转换非常自然。

3.2 序列化:Go对象转JSON

type User struct {
    ID       int    `json:"id"`
    Name     string `json:"name"`
    Email    string `json:"email"`
    Age      int    `json:"age,omitempty"` // omitempty: 零值时省略
    Password string `json:"-"`             // 序列化时完全忽略
}

user := User{
    ID:       1,
    Name:     "张三",
    Email:    "zhangsan@example.com",
    Age:      0, // 会被omitempty省略
    Password: "secret123",
}

// 普通序列化
data, err := json.Marshal(user)
fmt.Println(string(data))
// {"id":1,"name":"张三","email":"zhangsan@example.com"}

// 格式化序列化(方便阅读)
prettyData, err := json.MarshalIndent(user, "", "  ")
fmt.Println(string(prettyData))
// {
//   "id": 1,
//   "name": "张三",
//   "email": "zhangsan@example.com"
// }

3.3 反序列化:JSON转Go对象

jsonStr := `{
    "id": 42,
    "name": "李四",
    "email": "lisi@example.com",
    "age": 30,
    "extra_field": "这个字段Go结构体没有,会被忽略"
}`

var user User
err := json.Unmarshal([]byte(jsonStr), &user)
if err != nil {
    log.Fatal(err)
}

fmt.Printf("姓名:%s,邮箱:%s\n", user.Name, user.Email)

几个重要的规则:

  • JSON中多出来的字段会被忽略
  • Go结构体中多出来的字段保持零值
  • **字段必须是导出的(首字母大写)**才能被JSON包处理

3.4 处理动态JSON

有时你不确定JSON的结构,可以用 map[string]any 来处理(any 是Go 1.18+引入的 interface{} 别名,更简洁):

jsonStr := `{"name":"王五","scores":[90,85,92],"address":{"city":"北京"}}`

var data map[string]any
json.Unmarshal([]byte(jsonStr), &data)

// 访问值时需要类型断言
name := data["name"].(string)
fmt.Println(name) // 王五

// 嵌套数据的访问比较繁琐
address := data["address"].(map[string]any)
city := address["city"].(string)
fmt.Println(city) // 北京

使用 map[string]any 处理JSON就像吃火锅——什么都能往里放,但捞出来的时候需要仔细辨认。尽量定义结构体来反序列化,类型安全又好读。

3.5 流式JSON处理

当处理大量JSON数据时,用 json.Encoderjson.Decoder 比一次性读取全部内容更高效:

// 写入JSON到Writer(比如HTTP响应)
func writeJSON(w http.ResponseWriter, data any) {
    w.Header().Set("Content-Type", "application/json")
    encoder := json.NewEncoder(w)
    encoder.SetIndent("", "  ")
    if err := encoder.Encode(data); err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
    }
}

// 从Reader读取JSON(比如HTTP请求体)
func readJSON(r io.Reader, dest any) error {
    decoder := json.NewDecoder(r)
    decoder.DisallowUnknownFields() // 严格模式:不允许未知字段(注意:如果API需要向后兼容,不应开启此选项)
    return decoder.Decode(dest)
}

3.6 自定义JSON序列化

如果默认的序列化行为不满足需求,可以实现 json.Marshalerjson.Unmarshaler 接口:

type Timestamp struct {
    time.Time
}

// 自定义序列化:输出为Unix时间戳
func (t Timestamp) MarshalJSON() ([]byte, error) {
    return json.Marshal(t.Unix())
}

// 自定义反序列化:从Unix时间戳恢复
func (t *Timestamp) UnmarshalJSON(data []byte) error {
    var unix int64
    if err := json.Unmarshal(data, &unix); err != nil {
        return err
    }
    t.Time = time.Unix(unix, 0)
    return nil
}

type Event struct {
    Name      string    `json:"name"`
    CreatedAt Timestamp `json:"created_at"`
}

🤔 想一想 json:"name,omitempty" 中的 omitempty 对不同类型的零值行为一致吗?0""falsenil 都会被省略吗?


四、综合实战:构建一个简单的JSON API

把本章所学的io、HTTP和JSON能力组合起来,构建一个完整的API:

package main

import (
    "encoding/json"
    "fmt"
    "log"
    "net/http"
    "sync"
    "time"
)

// 数据模型
type Todo struct {
    ID        int       `json:"id"`
    Title     string    `json:"title"`
    Completed bool      `json:"completed"`
    CreatedAt time.Time `json:"created_at"`
}

// 内存存储
type TodoStore struct {
    mu     sync.RWMutex
    todos  map[int]*Todo
    nextID int
}

func NewTodoStore() *TodoStore {
    return &TodoStore{
        todos:  make(map[int]*Todo),
        nextID: 1,
    }
}

func (s *TodoStore) Create(title string) *Todo {
    s.mu.Lock()
    defer s.mu.Unlock()

    todo := &Todo{
        ID:        s.nextID,
        Title:     title,
        Completed: false,
        CreatedAt: time.Now(),
    }
    s.todos[todo.ID] = todo
    s.nextID++
    return todo
}

func (s *TodoStore) List() []*Todo {
    s.mu.RLock()
    defer s.mu.RUnlock()

    list := make([]*Todo, 0, len(s.todos))
    for _, t := range s.todos {
        list = append(list, t)
    }
    return list
}

// JSON响应辅助函数
func respondJSON(w http.ResponseWriter, status int, data any) {
    w.Header().Set("Content-Type", "application/json; charset=utf-8")
    w.WriteHeader(status)
    json.NewEncoder(w).Encode(data)
}

func respondError(w http.ResponseWriter, status int, message string) {
    respondJSON(w, status, map[string]string{"error": message})
}

func main() {
    store := NewTodoStore()
    mux := http.NewServeMux()

    // 获取所有待办
    mux.HandleFunc("GET /todos", func(w http.ResponseWriter, r *http.Request) {
        respondJSON(w, http.StatusOK, store.List())
    })

    // 创建待办
    mux.HandleFunc("POST /todos", func(w http.ResponseWriter, r *http.Request) {
        var input struct {
            Title string `json:"title"`
        }
        if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
            respondError(w, http.StatusBadRequest, "无效的请求体")
            return
        }
        if input.Title == "" {
            respondError(w, http.StatusBadRequest, "标题不能为空")
            return
        }
        todo := store.Create(input.Title)
        respondJSON(w, http.StatusCreated, todo)
    })

    fmt.Println("Todo API 启动在 http://localhost:8080")
    log.Fatal(http.ListenAndServe(":8080", mux))
}

用curl测试:

# 创建待办
curl -X POST http://localhost:8080/todos \
  -H "Content-Type: application/json" \
  -d '{"title":"学习Go标准库"}'

# 获取所有待办
curl http://localhost:8080/todos

📝 掌握度自测

  1. Go标准库中 io.Reader 接口包含几个方法?

    • A) 3个:Read、ReadAll、ReadByte
    • B) 2个:Read、Close
    • C) 1个:Read
    • D) 4个:Read、Write、Close、Seek
  2. 以下哪种方式是创建Go HTTP服务器的标准做法?

    • A) http.StartServer(":8080")
    • B) http.ListenAndServe(":8080", handler)
    • C) http.Run(":8080")
    • D) http.Serve(":8080")
  3. 结构体标签 json:"-" 的含义是:

    • A) JSON键名为 ”-”
    • B) 序列化时忽略该字段
    • C) 反序列化时忽略该字段
    • D) 该字段为必填
  4. HTTP响应的Body需要手动关闭,通常用什么方式?

    • A) resp.Body.Destroy()
    • B) resp.Close()
    • C) defer resp.Body.Close()
    • D) Go会自动关闭
  5. json.NewDecoder(r.Body).Decode(&user) 相比 json.Unmarshal 的优势是:

    • A) 速度更快
    • B) 流式读取,不需要一次性把所有数据加载到内存
    • C) 类型更安全
    • D) 支持更多数据格式

💡 自我评估

  • 答对5题:标准库基础扎实,准备好用框架构建更复杂的API了!
  • 答对3-4题:核心概念理解不错,建议动手写一个完整的HTTP服务来巩固。
  • 答对0-2题:这三个包是Go开发的核心工具,建议逐个练习,先掌握io.Reader/Writer的概念。

参考答案: 1-C, 2-B, 3-B, 4-C, 5-B

购买课程解锁全部内容

高并发不踩坑:Go 语言从语法到微服务

¥29.90