标准库精选:io、net/http与JSON处理 — Go的”百宝箱”
Go的标准库就像一个精心设计的工具箱——不像某些语言需要装一堆第三方库才能干活,Go自带的标准库就已经覆盖了绝大多数日常需求。今天我们打开这个工具箱,取出三件最常用的工具。
📋 开篇自测:你已经知道多少?
io.Reader接口只有一个方法,为什么它却是Go中最重要的接口之一?- 用Go标准库搭一个HTTP服务器需要几行代码?
- 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.File | os | 文件 |
*http.Response.Body | net/http | HTTP响应体 |
*bytes.Buffer | bytes | 内存缓冲区 |
*strings.Reader | strings | 字符串 |
*gzip.Reader | compress/gzip | 压缩流 |
*bufio.Reader | bufio | 带缓冲的读取器 |
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.Encoder 和 json.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.Marshaler 和 json.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、""、false、nil都会被省略吗?
四、综合实战:构建一个简单的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
📝 掌握度自测
-
Go标准库中
io.Reader接口包含几个方法?- A) 3个:Read、ReadAll、ReadByte
- B) 2个:Read、Close
- C) 1个:Read
- D) 4个:Read、Write、Close、Seek
-
以下哪种方式是创建Go HTTP服务器的标准做法?
- A)
http.StartServer(":8080") - B)
http.ListenAndServe(":8080", handler) - C)
http.Run(":8080") - D)
http.Serve(":8080")
- A)
-
结构体标签
json:"-"的含义是:- A) JSON键名为 ”-”
- B) 序列化时忽略该字段
- C) 反序列化时忽略该字段
- D) 该字段为必填
-
HTTP响应的Body需要手动关闭,通常用什么方式?
- A)
resp.Body.Destroy() - B)
resp.Close() - C)
defer resp.Body.Close() - D) Go会自动关闭
- A)
-
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