[7天实战入门Go语言后端] Day 2:用 Go 写一个 HTTP 服务——net/http 入门

本日关键词(实战):net/http、ListenAndServe、HandleFunc、路由、Request、ResponseWriter、JSON、编码解码、curl 调试

本日语法/概念(实战):

语法/概念 实战用途 本日示例
http.ListenAndServe(addr, nil) 启动 HTTP 服务,日常写 API 必用 server/main.go
http.HandleFunc(path, handler) 注册路由,一个路径一个处理函数 server/main.go
*http.Request 读 Method、URL、Path、Header、Body(如 JSON) 解析路径参数、请求体
http.ResponseWriter 写状态码、写响应头、写 Body、写 JSON 返回 JSON 响应
r.Header.Get / w.Header().Set 读请求头、写响应头(如 X-Trace-Id 串联调用与日志) server/main.go setTraceID
json.Marshal / json.Unmarshal 结构体 ↔ JSON,前后端联调必备 返回/解析 JSON

获取实战代码 :如需在本地跑通本文示例,请克隆仓库 WenSongWang/go-quickstart-7days,本文示例在 day2 目录,克隆后在项目根目录执行下文中的命令即可。


一、本篇目标

学完本文并跑通本目录下的示例,你将掌握:

模块 内容
服务 用标准库 net/http 起一个 HTTP 服务
路由 http.HandleFunc 或自建 mux 匹配路径
请求 Method、URL、Body、Header 的读取
响应 状态码、响应头(如 X-Trace-Id)、JSON、Write 的写法

二、前置要求

  • 已完成 Day 1 (至少会 go run、能看懂基本语法)。
  • 本系列命令均在项目根目录执行。

三、示例与知识点(先混个眼熟)

示例目录 主要知识点
server/ http.HandleFunc 注册路由、(ResponseWriter, *Request) 处理函数、读 Path/Method/Header、设 Content-Type 与 X-Trace-IdWriteHeader/Writejson.NewEncoder(w).Encode 写 JSON、路径参数用 TrimPrefix+Atoi 解析

四、核心概念与最小示例(不看代码也能懂)

下面用极短说明 + 一句话把写 HTTP 接口时最容易懵的点说清。

Handler 的签名为什么是 (w http.ResponseWriter, r *http.Request)

标准库规定:你注册的处理函数必须长这样,第一个参数写响应,第二个参数读请求w 用来设置状态码、响应头、响应体;r 用来读 Method、URL、Header、Body。名字可以改,类型不能改。

为什么结构体字段要写 `json:"id"`?

Go 里结构体字段默认转成 JSON 时用的是首字母大写的字段名 (如 ID),但前端通常习惯小写(id)。用 struct tag `json:"id"` 告诉编码器:转成 JSON 时用 "id" 这个键。这样 User{ID:1, Name:"张三"} 会变成 {"id":1,"name":"张三"},前后端联调更顺手。

为什么要在 http.Error 之前先 Set Content-Type?

http.Error 内部会调用 WriteHeader,而 响应头必须在第一次 WriteHeader 之前全部设好 ,之后就不能再改。所以若想返回 JSON 格式的错误体,必须先 w.Header().Set("Content-Type", "application/json"),再调用 http.Error,否则浏览器可能按纯文本解析。

Marshal / Encode 一句话

  • json.Marshal :把结构体转成 []byte,自己再 w.Write(data);适合要改一改再写出的场景。
  • json.NewEncoder(w).Encode(v) :把结构体直接编码并写入 w,一步到位,写 API 响应时常用。

线上项目更多用哪个:HandleFunc 还是 mux?为什么?

线上项目更多用「第三方路由库」(如 chi、gin、echo)或「自建 mux」 ,而不是只用 http.HandleFunc + 默认 mux。原因主要有:

需求 说明
路径参数 /api/users/:id,标准库要自己 TrimPrefix + 解析,第三方库直接提供 Param(r, "id")
中间件链 日志、鉴权、超时等需要一层层包装 Handler,第三方库有现成的 Use/Group 等模式(Day 5 会学)。
路由分组与前缀 大量接口时按模块分组、统一加前缀更清晰,标准库要自己拼路径。

本日用 HandleFunc 是为了先吃透标准库的 Request/Response 模型,后面再用框架会更容易理解它在帮你封装什么。

本系列约定 :7 天全程使用标准库 net/http ,不在 Day 2 也不在后续某天引入 chi/gin 等第三方路由框架。后面 Day 5、Day 7 会循序渐进地用 http.NewServeMux + 自建中间件(日志、鉴权),仍是标准库写法,便于理解「框架在帮你封装什么」后再按需选型。

和实战上线项目的区别实战上线项目里很多会用框架(chi、gin、echo 等)或公司自研路由层,因为路径参数、中间件、分组、性能与可维护性更省事。本系列故意不用框架,是为了先打好标准库基础,将来上手框架或做技术选型时心里有数;真正做生产项目时,可以按团队规范选用框架或继续用标准库(小服务、内部工具用标准库也常见)。


五、Day 2 示例代码全文与逐段解读

下面把 day2/server/main.go 的完整代码贴出,并分段做简短解读。读者无需打开项目,按顺序看下去即可把 Day 2 学完。


完整代码(即 day2/server/main.go,含注释)

go 复制代码
// Day2 示例:标准库 HTTP 服务 + 简单路由 + JSON + trace_id
package main

import (
	"crypto/rand"
	"encoding/hex"
	"encoding/json"
	"net/http"
	"strconv"
	"strings"
)

// setTraceID 从请求头读 X-Trace-Id,没有则生成,并写回响应头(便于调用方与日志串联)
func setTraceID(w http.ResponseWriter, r *http.Request) {
	id := r.Header.Get("X-Trace-Id")
	if id == "" {
		b := make([]byte, 8)
		rand.Read(b)
		id = hex.EncodeToString(b)
	}
	w.Header().Set("X-Trace-Id", id)
}

// User 返回给前端的结构体
type User struct {
	ID   int    `json:"id"`
	Name string `json:"name"`
}

// handleHello 处理 /hello
func handleHello(w http.ResponseWriter, r *http.Request) {
	setTraceID(w, r)
	if r.URL.Path != "/hello" {
		http.NotFound(w, r)
		return
	}
	w.Header().Set("Content-Type", "text/plain; charset=utf-8")
	w.WriteHeader(http.StatusOK)
	w.Write([]byte("Hello, Go HTTP!"))
}

// handleGetUser 处理 /api/users/1 这种路径
func handleGetUser(w http.ResponseWriter, r *http.Request) {
	setTraceID(w, r)
	if r.Method != http.MethodGet {
		http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
		return
	}
	// 从 /api/users/1 里取出 "1",再转成数字
	path := strings.TrimPrefix(r.URL.Path, "/api/users/")
	id, err := strconv.Atoi(path)
	if err != nil || id <= 0 {
		w.Header().Set("Content-Type", "application/json") // 须在 WriteHeader 前设置头
		http.Error(w, `{"error":"invalid id"}`, http.StatusBadRequest)
		return
	}
	user := User{ID: id, Name: "用户" + strconv.Itoa(id)}
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(http.StatusOK)
	json.NewEncoder(w).Encode(user) // 把结构体编码成 JSON 写到 w
}

func main() {
	// 注册路径与处理函数的对应关系:访问 /hello 调 handleHello,访问 /api/users/xxx 调 handleGetUser
	http.HandleFunc("/hello", handleHello)
	http.HandleFunc("/api/users/", handleGetUser)
	// 监听 8080 端口,nil 表示用默认的路由(就是上面 HandleFunc 注册的)
	http.ListenAndServe(":8080", nil)
}

逐段解读

  • setTraceID :从请求头读 X-Trace-Id,没有则用 crypto/rand 生成一个并写回响应头。这样调用方(或网关)可以把同一次请求的 trace_id 串联起来,日志里也方便按 trace_id 查一整条链路;实战里通常会在中间件里统一做(Day 5/7 会见到)。

  • User 与 json tagUser 和 Day 1 的 basics 里结构体概念一致,这里多了 `json:"id"`、`json:"name"`,表示序列化成 JSON 时键名为 idname,方便前端或 curl 直接使用。

  • handleHello :先调用 setTraceID(w, r) 再处理业务,保证响应头里都带 trace_id。再判断 r.URL.Path != "/hello" 则返回 404;否则设置 Content-Type 为纯文本、状态码 200,再 Write 一段字节。顺序不能反 :先 Header().Set,再 WriteHeader,再 Write

  • handleGetUser :同样先 setTraceID(w, r),再只接受 GET;用 strings.TrimPrefix(r.URL.Path, "/api/users/") 得到路径后缀(如 "1"),再用 strconv.Atoi 转成数字。若转换失败或 id≤0,先设 Content-Type 再 http.Error 返回 400 和 JSON 错误体;成功则构造 User,设 JSON 头、200,用 json.NewEncoder(w).Encode(user) 写回 JSON。易踩坑 :若先调用 http.ErrorSet("Content-Type", "application/json"),响应头已发出无法修改,浏览器会按纯文本解析;解法即本段写法:先设头再写 body。

  • mainHandleFunc 把路径和函数绑在一起:/hello → handleHello,/api/users/ 注意带末尾斜杠,这样 /api/users/1 会匹配到 handleGetUser。易踩坑 :误以为注册 /api/users/ 只能匹配该精确路径;解法 :标准库规定末尾有 / 为前缀匹配,无 / 为精确匹配,所以 /api/users/1 会进同一 handler。ListenAndServe(":8080", nil) 表示监听 8080,nil 表示使用默认的 DefaultServeMux(即刚才注册的路由)。


六、运行当天代码

在项目根目录执行:

bash 复制代码
go run ./day2/server

启动后:

  • 浏览器或 curl 访问:http://localhost:8080/hello
  • 访问:http://localhost:8080/api/users/1 看 JSON 响应

示例里会演示:注册路由、读请求、写 JSON 等,可直接对照源码学习。

七、学习建议

  1. 先跑起来 :确保 8080 端口没被占用,跑通后再看 day2/server 下的 main.go
  2. 动手改 :改路径(如 /hello/hi)、改返回的 JSON 字段,再请求一次观察变化。
  3. 理解「一个 Handler 对应一个路径」和「如何从 *http.Request 里拿参数、Body」。

八、小结

Day 2 用标准库就能写出可用的 HTTP 服务,不依赖框架。后面 Day 3 做配置、Day 5 加中间件、Day 7 做完整 API,都会基于同样的 net/http 模型。

相关推荐
我不是8神6 小时前
go-zero微服务框架总结
开发语言·微服务·golang
3秒一个大6 小时前
JWT 登录:原理剖析与实战应用
前端·http·代码规范
遇见你的雩风11 小时前
【Golang】--- Goroutine
开发语言·golang
橙露12 小时前
计算机网络核心:HTTP/HTTPS 协议原理与抓包分析实战
计算机网络·http·https
YGGP13 小时前
【Golang】LeetCode 189. 轮转数组
开发语言·leetcode·golang
桂花很香,旭很美13 小时前
[7天实战入门Go语言后端] Day 3:项目结构与配置——目录组织、环境变量与 viper
开发语言·数据库·golang
桂花很香,旭很美14 小时前
[7天实战入门Go语言后端] Day 4:Go 数据层入门——database/sql 与简单 CRUD
数据库·sql·golang
小同志001 天前
⽹络原理-HTTP/HTTPS(一)
网络协议·http·https
tang777891 天前
一张图看懂 HTTP、SOCKS5 代理的区别
网络·网络协议·http