CodeRoadMap
首页路线题库资源库文章社区
CodeRoadMap

程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。

由 程序员柏风 独立维护 · 关于作者

学习

  • 路线图
  • 课程
  • 面试题
  • 文章
  • 我

更多

  • 资源
  • 社区
  • 关于作者
  • 内容说明

© 2026 CodeRoadMap

津ICP备2026012044号-1·津公网安备12011002026155号·coderoadmap@126.com
Go 语言入门/课时/Web 与数据库
RawBlame
课时目录12 节

Go 语言入门

  1. 01Go 是什么
  2. 02基础语法与类型
  3. 03数组、切片与 map
  4. 04结构体与接口
  5. 05模块与包管理
  6. 06并发模型
  7. 07错误处理
  8. 08泛型入门
  9. 09I/O 与 CLI
  10. 10Web 与数据库
  11. 11测试与基准
  12. 12综合练习

Web 与数据库

net/http、gin/echo、gorm/pgx 概览。

35 分钟

Web 与数据库

这一课你将能做到

  1. 用标准库 net/http 写出带路由参数、JSON 收发、日志中间件的 HTTP 服务
  2. 理解 http.Handler 接口与 Go 1.22 新路由语法(方法匹配 + {id} 通配符)
  3. 用 database/sql 完成建表与参数化查询 CRUD,说清 SQL 注入与驱动机理

为什么要学

Go 最主流的应用形态就是后端服务。标准库 net/http 强到什么程度?不引入任何框架就能跑生产——API 网关 Caddy、监控系统 Prometheus 的内核都基于它。Go 1.22 又补齐了路由短板(方法匹配、路径通配符),标准库写 REST API 已经完全够用。

数据库方面,Go 只内置了统一的接口层 database/sql,具体数据库由驱动包接入。本课用纯 Go 的 SQLite 驱动跑通完整流程(无需装数据库、无需 CGO,Windows 也零障碍),套路与 MySQL/PostgreSQL 完全一致,换驱动即可无缝切换。

先建立图像

HTTP 请求
   │
   ▼
┌─────────────────────── http.Server ───────────────────────┐
│  ServeMux(路由器)                                        │
│   "GET /hello/{name}" ──▶ handlerFunc                     │
│   "POST /users"       ──▶ handlerFunc                     │
│         │  每个请求一个 goroutine(天然高并发)              │
│         ▼                                                  │
│  middleware(日志) ──▶ handler ──▶ json.NewEncoder(w) ──▶ 响应│
└────────────────────────────────────────────────────────────┘
                        │ database/sql(统一接口层)
                        ▼
              驱动(sqlite / go-sql-driver/mysql / pgx)
                        │ SQL(参数化占位符 ? 或 $1)
                        ▼
                   数据库文件 / 服务

三个关键认知:Handler 是接口(一个 ServeHTTP 方法);每个请求独立 goroutine(不用你写线程池);database/sql 只管接口,驱动才管实现。

跟着做

步骤 1:十行 HTTP 服务器(Go 1.22 路由)

package main

import (
	"encoding/json"
	"log"
	"net/http"
)

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

	// Go 1.22 新语法:方法 + 路径通配符,{name} 用 PathValue 取
	mux.HandleFunc("GET /hello/{name}", func(w http.ResponseWriter, r *http.Request) {
		name := r.PathValue("name")
		w.Header().Set("Content-Type", "application/json; charset=utf-8")
		json.NewEncoder(w).Encode(map[string]string{"msg": "你好, " + name})
	})

	log.Println("服务已启动: http://localhost:8080")
	log.Fatal(http.ListenAndServe(":8080", mux))
}
$ go run .
$ curl http://localhost:8080/hello/gopher
{"msg":"你好, gopher"}

步骤 2:理解 http.Handler 接口

路由能挂任何「实现了 ServeHTTP 的东西」:

package main

import (
	"fmt"
	"net/http"
)

// 任何有 ServeHTTP(w, r) 的类型都是一个 handler
type greeter struct {
	greeting string
}

func (g greeter) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	fmt.Fprintln(w, g.greeting)
}

func main() {
	mux := http.NewServeMux()
	mux.Handle("/", greeter{greeting: "Hello, Handler 接口!"})

	// http.HandlerFunc 把普通函数适配成 handler
	mux.HandleFunc("/ping", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintln(w, "pong")
	})

	http.ListenAndServe(":8081", mux)
}

Handle 收接口(对象),HandleFunc 收函数——两者等价,函数版写起来更顺手。

步骤 3:JSON 收发的规范写法

package main

import (
	"encoding/json"
	"net/http"
)

type User struct {
	ID   int    `json:"id"`
	Name string `json:"name"`
}

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

	// 接收 JSON 请求体
	mux.HandleFunc("POST /users", func(w http.ResponseWriter, r *http.Request) {
		var u User
		if err := json.NewDecoder(r.Body).Decode(&u); err != nil {
			http.Error(w, "请求体不是合法 JSON", http.StatusBadRequest)
			return
		}
		u.ID = 1

		w.Header().Set("Content-Type", "application/json; charset=utf-8")
		w.WriteHeader(http.StatusCreated) // 201
		json.NewEncoder(w).Encode(u)
	})

	// 查询参数:/users?id=1
	mux.HandleFunc("GET /users", func(w http.ResponseWriter, r *http.Request) {
		id := r.URL.Query().Get("id") // 拿不到时是空串
		if id == "" {
			http.Error(w, "缺少 id 参数", http.StatusBadRequest)
			return
		}
		w.Header().Set("Content-Type", "application/json; charset=utf-8")
		json.NewEncoder(w).Encode(User{Name: "user-" + id})
	})

	http.ListenAndServe(":8082", mux)
}
$ curl -X POST http://localhost:8082/users -d '{"name":"张三"}'
{"id":1,"name":"张三"}

步骤 4:中间件——函数套函数

中间件就是「包一层的 handler」:进站前、出站后各有机会做事(日志、鉴权、超时、恢复 panic):

package main

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

// 接收 handler,返回增强后的 handler——这就是 Go 中间件的全部
func logging(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		start := time.Now()
		next.ServeHTTP(w, r) // 调用下一层
		log.Printf("%s %s 耗时 %s", r.Method, r.URL.Path, time.Since(start))
	})
}

func main() {
	mux := http.NewServeMux()
	mux.HandleFunc("GET /", func(w http.ResponseWriter, r *http.Request) {
		w.Write([]byte("hello"))
	})

	log.Fatal(http.ListenAndServe(":8083", logging(mux))) // 整个 mux 被包一层
}

中间件可以链式叠加:logging(recovery(auth(mux))),洋葱模型,和任何框架同构。

步骤 5:database/sql + SQLite 实战

先装纯 Go 驱动(无需 CGO、无需装数据库):

$ go mod init example.com/webdemo
$ go get modernc.org/sqlite

完整可运行程序:

package main

import (
	"database/sql"
	"fmt"
	"log"

	_ "modernc.org/sqlite" // 空白导入:只为触发驱动注册(init 副作用)
)

func main() {
	// 打开连接池(lazy,真正的连接在首次使用时建立)
	db, err := sql.Open("sqlite", "file:demo.db")
	if err != nil {
		log.Fatal(err)
	}
	defer db.Close()

	// 生产数据库的常见连接池设置
	db.SetMaxOpenConns(10)
	db.SetMaxIdleConns(5)

	// 建表
	_, err = db.Exec(`CREATE TABLE IF NOT EXISTS users (
		id INTEGER PRIMARY KEY AUTOINCREMENT,
		name TEXT NOT NULL
	)`)
	if err != nil {
		log.Fatal(err)
	}

	// 插入:占位符 ? 防注入,值绝不拼进 SQL 字符串
	result, err := db.Exec("INSERT INTO users (name) VALUES (?)", "张三")
	if err != nil {
		log.Fatal(err)
	}
	id, _ := result.LastInsertId()
	fmt.Println("新用户 id:", id)

	// 单行查询:QueryRow + Scan
	var name string
	err = db.QueryRow("SELECT name FROM users WHERE id = ?", id).Scan(&name)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println("查到:", name)

	// 查不存在的行:返回 sql.ErrNoRows(正常业务错误,不是 bug)
	err = db.QueryRow("SELECT name FROM users WHERE id = ?", 999).Scan(&name)
	fmt.Println("err == sql.ErrNoRows:", err == sql.ErrNoRows) // true

	// 多行查询:Query 返回 rows,必须 Close!
	rows, err := db.Query("SELECT id, name FROM users")
	if err != nil {
		log.Fatal(err)
	}
	defer rows.Close()
	for rows.Next() {
		var uid int
		var uname string
		if err := rows.Scan(&uid, &uname); err != nil {
			log.Fatal(err)
		}
		fmt.Println(uid, uname)
	}
	if err := rows.Err(); err != nil { // 迭代中发生的错误在这里浮出
		log.Fatal(err)
	}

	// 更新与删除
	db.Exec("UPDATE users SET name = ? WHERE id = ?", "张三丰", id)
	db.Exec("DELETE FROM users WHERE id = ?", id)
}

切换到 MySQL 只需两步:go get github.com/go-sql-driver/mysql、sql.Open("mysql", "user:pass@tcp(127.0.0.1:3306)/dbname"),业务代码一行不改。

步骤 6:框架概览——gin 长什么样

框架是标准库的语法糖,核心模型完全一样:

// go get github.com/gin-gonic/gin
package main

import "github.com/gin-gonic/gin"

func main() {
	r := gin.Default() // 自带日志与 recover 中间件

	r.GET("/hello/:name", func(c *gin.Context) {
		c.JSON(200, gin.H{"msg": "你好, " + c.Param("name")})
	})

	r.Run(":8084")
}

选型一句话:gin 生态最大、资料最多;echo/chi 轻快,chi 路由几乎就是标准库风格。学透标准库后,任何框架半小时上手。

讲清楚要点

  1. http.Handler 是一切的核心:ServeHTTP(ResponseWriter, *Request) 一个方法定义了整个 Web 世界;中间件、路由、框架全是它的排列组合。
  2. 每请求一个 goroutine:handler 里可以放心阻塞(如查数据库),不会拖垮其他请求;但共享数据要自己加锁(第 6 课)。
  3. Go 1.22 路由:"GET /users/{id}" 支持方法匹配与通配符,r.PathValue("id") 取值;{id...} 还能匹配剩余路径。
  4. SQL 永远参数化:占位符交给驱动转义,杜绝拼接注入;?(sqlite/mysql)与 $1(postgres)只是方言差异。
  5. rows 必须Close,rows 迭代完查 Err:不 Close 会占用连接池连接,最终服务卡死。
  6. database/sql 是连接池管理器:SetMaxOpenConns/SetMaxIdleConns 是服务上量前的必修参数。

你可能会懵的地方

  • 为什么标准库不内置 ORM? Go 社区偏好显式 SQL:查询可读、可控、可调优。要 ORM 生态有 GORM(活跃度最高)、sqlc(SQL 生成代码)等,但都建立在 database/sql 之上,接口层不变。

  • import _ "modernc.org/sqlite" 的下划线是干嘛的? 空白导入:代码不直接使用该包,只为执行其 init()——驱动在 init 里调用 sql.Register 注册自己,database/sql 才认识 "sqlite" 这个名字。

  • http.ListenAndServe 为什么要 log.Fatal 包着? 它会一直阻塞,只有出错(如端口被占)才返回;Fatal 打印错误并退出进程,否则错误被静默忽略,服务“看似启动实则没起”。

  • w.WriteHeader 之前写了 Header 就不生效? 隐式 200 的规则:第一次 Write 或 WriteHeader 时响应头定格。所以先 Header().Set(...),再 WriteHeader(code),最后写 body,顺序不能反。

  • SQLite 适合生产吗? 单机小流量(嵌入式、CLI、单文件场景)完全够,WAL 模式下并发也不差;多实例 Web 服务通常上 MySQL/PostgreSQL。本课用它是因为零环境成本,套路通用。

错法对照

错法问题正解
"SELECT ... WHERE id = " + id 拼接SQL 注入db.Query("... id = ?", id)
先 Write body 再设 Content-Type头已定格,Content-Type 丢失先设头,再写 body
rows 忘 Close连接池耗尽,服务假死defer rows.Close()
路由注册 "GET /users" 请求 /users/404(尾斜杠不匹配)统一客户端路径或注册两条
handler 里共享 map 不加锁并发写 fatal errorMutex 保护或改设计
忽略 QueryRow 的 ErrNoRows把「没有数据」当系统故障errors.Is(err, sql.ErrNoRows) 特判

卡点急救

  1. 404 但代码看着没错:方法没匹配(POST 打到 GET 路由)、路径大小写/尾斜杠不一致;打印 mux 注册项逐一核对。
  2. sql: unknown driver "sqlite":忘了空白导入驱动包。
  3. bind: address already in use:端口被占,换端口或杀掉旧进程。
  4. 数据库文件被锁(sqlite: database is locked):并发写冲突,开 WAL(PRAGMA journal_mode=WAL)或控制连接数。

面试常问

  • net/http 为什么高性能? 每请求一个 goroutine(轻量、百万级并发)、内置连接池复用、零拷贝优化与超时控制齐备;相比线程池模型省去线程调度与内存开销。

  • Go 中间件的实现原理? 闭包包装:func(next http.Handler) http.Handler,前置逻辑 → next.ServeHTTP → 后置逻辑,天然支持洋葱模型与链式组合,与框架无关。

  • SQL 注入的原理与防御? 注入 = 用户输入被当作 SQL 语法执行(引号逃逸改写语句)。防御核心是参数化查询(预编译 + 占位符转义),辅以最小权限账号、输入校验。ORM 的绑定参数同样基于此。

  • GORM 和裸 SQL 怎么选? CRUD 密集、模型稳定的业务用 GORM 提效;复杂查询、报表、性能敏感路径写原生 SQL(或 sqlc 生成类型安全代码)。两者共存很常见,关键是不让 ORM 生成失控的 N+1 查询。

自测三问

  1. Go 1.22 如何注册「DELETE /todos/{id}」并取出 id?写出两行代码。
  2. 写出防注入的参数化查询写法,并解释为什么拼接字符串有风险。
  3. rows.Next() 循环结束后还要做哪两件事?各防止什么问题?

小结一句话

Go 的 Web = http.Handler 一个接口派生出的路由、中间件与框架;数据库 = database/sql 统一接口 + 驱动实现 + 参数化查询——掌握这两层,换任何框架、任何数据库都是换个壳。

延伸

  • Go 官方教程:编写 Web 应用
  • database/sql 使用指南
  • 下一课:测试与基准——Go 自带测试框架,表驱动测试是社区的统一审美。
加载本节练习…
← 上一课I/O 与 CLI下一课 →测试与基准