Go 快速入门 — 从 Mock Server 学 Go
为什么从一段 server 代码开始
下面这个 Mock Server(~230 行)几乎覆盖了 Go 日常开发 80% 的语法点:
- HTTP 路由与 JSON 接口
- WebSocket 长连接 + Hub 广播
- JWT 签发与解析
- goroutine + channel 优雅关闭
sync.RWMutex并发安全
边拆边学,每个知识点配原代码片段。
一、包声明与导入
package main // ① 可执行程序必须是 main 包
import ( // ② 分组导入(推荐)
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"log"
"net/http"
"os"
"os/signal"
"strings"
"sync"
"syscall"
"time"
"github.com/gorilla/websocket" // ③ 第三方依赖
)
| 点 | 说明 |
|---|---|
package main | 生成可执行文件;库代码用包名等于目录名 |
分组 import (...) | 标准库在上,第三方在下 —— go fmt 自动整理 |
| 路径即导入 | github.com/gorilla/websocket 对应 $GOPATH/pkg/mod 下的目录 |
| 未使用的导入 | 编译错误 —— Go 强迫干净代码 |
Go 没有头文件。包内所有
.go文件天然共享标识符。
二、变量与常量
var jwtSecret = []byte("yunrong-mock-secret") // 包级变量
var upgrader = websocket.Upgrader{ // 结构体字面量
CheckOrigin: func(r *http.Request) bool { return true },
}
// 短声明(仅函数内可用)
hub := newHub()
addr := ":8080"
| 写法 | 作用域 | 说明 |
|---|---|---|
var x = ... | 包级 / 函数内 | 显式声明 |
x := ... | 仅函数内 | 短声明,自动推断类型 |
const | 包级 / 函数内 | 编译期常量 |
Go 的类型推断方向反直觉:var x = expr 时,类型从 expr 推断,不是从变量名。
三、结构体与方法
type Hub struct {
mu sync.RWMutex
clients map[int64]*Client
}
// (h *Hub) 是指针接收者 —— Hub 的方法
func (h *Hub) register(c *Client) {
h.mu.Lock()
defer h.mu.Unlock()
if old, ok := h.clients[c.userID]; ok {
old.conn.Close()
}
h.clients[c.userID] = c
}
Go 没有 class。 用 struct 存数据,用接收者函数绑定行为。
| 接收者 | 语义 |
|---|---|
(h *Hub) 指针 | 可以修改 Hub 内部状态 |
(h Hub) 值 | 操作的是副本,原值不变 |
实践中几乎都用指针接收者,除非类型很小且不可变。
构造函数惯例:
func newHub() *Hub {
return &Hub{clients: make(map[int64]*Client)}
}
Go 没有构造器语法,约定用 NewXxx() 或 newXxx()(小写 = 包内私有)。
四、Map
// 字面量初始化
users := map[string]struct {
password string
userID int64
name string
}{
"admin": {"123456", 1001, "管理员"},
"zhangsan": {"123456", 1002, "张三"},
"lisi": {"123456", 1003, "李四"},
}
// 取值 + 存在性检查
u, ok := users[req.Username]
if !ok || u.password != req.Password {
// 用户不存在或密码错
}
| 操作 | 写法 |
|---|---|
| 创建空 map | make(map[K]V) |
| 取值 | v, ok := m[key] → ok=false 表示不存在 |
| 遍历 | for k, v := range m { ... } |
| 删除 | delete(m, key) |
ok是 Go 的 “comma ok” 惯用法,map、类型断言、channel 都用它。
五、HTTP Server
Go 1.22+ 支持直接在 HandleFunc 里写 HTTP method:
mux := http.NewServeMux()
mux.HandleFunc("GET /health", healthHandler) // ① method + path
mux.HandleFunc("POST /api/v1/auth/login", loginHandler)
mux.HandleFunc("/ws", func(w http.ResponseWriter, r *http.Request) {
wsHandler(w, r, hub) // ② 闭包捕获 hub
})
http.ListenAndServe(":8080", mux) // ③ 阻塞监听
| Go 版本 | 路由写法 |
|---|---|
| ≤ 1.21 | mux.HandleFunc("/health", h) + 在 handler 内 if r.Method != "GET" |
| ≥ 1.22 | mux.HandleFunc("GET /health", h) 直接指定 method |
http.ResponseWriter 和 *http.Request 是每个 handler 的标准参数。
六、JSON 编解码
// 解码请求体:json.NewDecoder(r.Body).Decode(&req)
var req struct {
Username string `json:"username"`
Password string `json:"password"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid json"})
return
}
// 编码响应
func writeJSON(w http.ResponseWriter, status int, v any) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(v)
}
结构体标签(Struct Tag):
type LoginReq struct {
Username string `json:"username"` // 字段名 → JSON key 映射
Password string `json:"password"`
}
| 常见标签 | 用途 |
|---|---|
json:"xxx" | JSON 序列化字段名 |
json:"xxx,omitempty" | 零值时省略 |
json:"-" | 忽略该字段 |
七、WebSocket
var upgrader = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool { return true },
}
func wsHandler(w http.ResponseWriter, r *http.Request, hub *Hub) {
token := r.URL.Query().Get("token")
userID := parseTokenUserID(token) // ① 鉴权
conn, _ := upgrader.Upgrade(w, r, nil) // ② HTTP → WS 升级
client := &Client{conn: conn, userID: userID}
hub.register(client) // ③ 注册到 Hub
defer func() {
hub.unregister(client.userID)
conn.Close() // ④ 离开时清理
}()
for { // ⑤ 读循环
_, raw, _ := conn.ReadMessage()
// 解析 JSON → 路由到 msg / ping / ...
}
}
WebSocket 生命周期:
HTTP Upgrade → register → 读循环 (for { ReadMessage })
│
├── "ping" → 回复 "pong"
├── "msg" → Hub.sendToUser(to, forward)
└── 连接断开 → unregister + Close
八、goroutine 与 channel
// 在后台 goroutine 启动 HTTP server
go func() {
if err := http.ListenAndServe(addr, mux); err != nil {
log.Fatalf("server error: %v", err)
}
}()
// channel 阻塞等待信号 → 优雅关闭
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit // 没有信号到达前,这行一直阻塞
log.Println("Mock Server shutting down...")
| 概念 | 说明 |
|---|---|
go func() {...}() | 启动一个 goroutine,不阻塞当前函数 |
make(chan T, n) | 创建缓冲为 n 的 channel |
<-quit | 从 channel 读取,无数据时阻塞 |
signal.Notify | 把 OS 信号路由到 channel |
goroutine 是 Go 并发的核心——轻量(~2KB 栈),一个程序开几十万 goroutine 很正常。不需要 async/await,也不需要手动管理线程池。
// channel 模式速查
ch := make(chan int) // 无缓冲 —— 发送方阻塞直到接收方就绪
ch := make(chan int, 10) // 有缓冲 —— 满之前不阻塞
ch <- 42 // 发送
v := <-ch // 接收
close(ch) // 关闭(接收方用 v, ok := <-ch 感知)
九、sync.RWMutex — 读写锁
type Hub struct {
mu sync.RWMutex
clients map[int64]*Client
}
func (h *Hub) register(c *Client) {
h.mu.Lock() // 写锁 —— 独占
defer h.mu.Unlock()
h.clients[c.userID] = c
}
func (h *Hub) sendToUser(userID int64, msg []byte) {
h.mu.RLock() // 读锁 —— 多个 goroutine 可同时持有
defer h.mu.RUnlock()
if c, ok := h.clients[userID]; ok {
c.conn.WriteMessage(websocket.TextMessage, msg)
}
}
| 锁 | 方法 | 并发特性 |
|---|---|---|
| 写锁 | Lock() / Unlock() | 独占,与所有读/写互斥 |
| 读锁 | RLock() / RUnlock() | 共享,多个读可同时进行 |
关键搭配:Lock() 后立刻 defer Unlock(),绝不错漏。
sync.Mutex(互斥锁,没读/写之分)和 sync.RWMutex 的选择很简单:读多写少就用 RWMutex。
十、defer — 延迟执行
func (h *Hub) register(c *Client) {
h.mu.Lock()
defer h.mu.Unlock() // 函数返回前一定执行,无论正常还是 panic
// ...
}
defer func() {
hub.unregister(client.userID)
conn.Close()
}()
defer 的三个特性:
- LIFO — 多个 defer 按后进先出执行
- 参数立即求值 —
defer fmt.Println(i)的i在 defer 语句时取值 - 可修改命名返回值 — 闭包内改命名返回值会影响最终结果
Go 没有
try-finally,defer就是它的 finally。
十一、JWT 签发(HMAC-SHA256)
func makeJWT(userID int64, username string) (string, error) {
// ① Header(固定)
header := base64.RawURLEncoding.EncodeToString(
[]byte(`{"alg":"HS256","typ":"JWT"}`))
// ② Payload(可变)
payloadBytes, _ := json.Marshal(map[string]any{
"user_id": userID,
"username": username,
"exp": time.Now().Add(2 * time.Hour).Unix(),
"iat": time.Now().Unix(),
})
payload := base64.RawURLEncoding.EncodeToString(payloadBytes)
// ③ Signature
unsigned := header + "." + payload
mac := hmac.New(sha256.New, jwtSecret)
mac.Write([]byte(unsigned))
sig := base64.RawURLEncoding.EncodeToString(mac.Sum(nil))
return unsigned + "." + sig, nil // header.payload.sig
}
JWT 结构:base64(header).base64(payload).base64(HMAC-SHA256(header.payload, secret))
| 函数 | 用途 |
|---|---|
hmac.New(sha256.New, secret) | 创建 HMAC-SHA256 哈希器 |
mac.Write(data) | 喂入待签名数据 |
mac.Sum(nil) | 取出 HMAC 结果 |
base64.RawURLEncoding | URL 安全 Base64(无 = 填充) |
解析端(Mock 简化,仅解码不验签):
func parseTokenUserID(token string) int64 {
parts := strings.Split(token, ".")
if len(parts) != 3 {
return 0
}
payload, _ := base64.RawURLEncoding.DecodeString(parts[1])
var claims map[string]any
json.Unmarshal(payload, &claims)
id, _ := claims["user_id"].(float64) // JSON 数字默认 float64
return int64(id)
}
注意 claims["user_id"].(float64) —— 这是类型断言,Go 的 json.Unmarshal 把 JSON 数字统一解成 float64。
十二、类型断言与类型 switch
func toFloat64(v any) float64 {
switch n := v.(type) {
case float64:
return n
case float32:
return float64(n)
case json.Number:
f, _ := n.Float64()
return f
}
return 0
}
| 语法 | 含义 |
|---|---|
v.(T) | 断言 v 是 T 类型,不是则 panic |
v, ok := x.(T) | 安全断言,ok=false 表示不是 |
switch v := x.(type) | 类型分支,v 在每个 case 里自动转为对应类型 |
any 是 interface{} 的别名(Go 1.18+),表示”任意类型”。
十三、init() — 包初始化钩子
func init() {
tok, _ := makeJWT(1001, "admin")
tok2, _ := makeJWT(1002, "zhangsan")
log.Println(strings.Repeat("=", 60))
log.Println("Test tokens (valid 2h):")
log.Printf(" admin: ?token=%s", tok)
log.Printf(" zhangsan: ?token=%s", tok2)
log.Println(strings.Repeat("=", 60))
}
init()在main()之前自动执行- 一个包可以有多个
init()(按文件名顺序执行) - 用来做初始化:注册驱动、验证配置、打印启动信息
十四、完整架构回顾
把上面的知识点拼回 Mock Server:
main()
├── hub := newHub() // struct 构造
├── mux := http.NewServeMux() // HTTP 路由
│ ├── GET /health // JSON 响应
│ ├── POST /api/v1/auth/login // JWT 签发
│ └── /ws // WebSocket 升级
│
├── go ListenAndServe() // goroutine 启动 HTTP
│ │
│ ├── loginHandler
│ │ ├── json.Decode(req) // 读请求体
│ │ ├── map 查用户 // 内建用户表
│ │ └── makeJWT() → 200 // hmac.New + base64
│ │
│ └── wsHandler
│ ├── parseTokenUserID // JWT 解码
│ ├── upgrader.Upgrade // HTTP → WS
│ ├── hub.register // RWMutex.Lock
│ ├── defer unregister // defer 清理
│ └── for { ReadMessage } // 读循环 + JSON 路由
│
└── <-quit // channel 阻塞等 SIGTERM
十五、Go 工具链速查
go mod init <module-path> # 初始化模块
go mod tidy # 整理依赖
go build -o server ./cmd/server # 编译
go run ./cmd/server # 编译+运行
go fmt ./... # 格式化所有代码
go vet ./... # 静态分析
go test ./... # 运行测试
十六、与其他语言的思维转换
| 概念 | C++ | Python | Go |
|---|---|---|---|
| 类 | class | class | struct + 接收者方法 |
| 构造函数 | 与类同名 | __init__ | NewXxx() 函数 |
| 继承 | : public Base | class D(Base) | 组合(embedding) |
| 多态 | virtual | 鸭子类型 | interface |
| 错误处理 | 异常 | 异常 | if err != nil |
| 并发 | std::thread | asyncio | go + channel |
| 包管理 | CMake/vcpkg | pip/poetry | go mod |
| 格式化 | clang-format | black | go fmt(唯一标准) |
核心心法
- Go 追求简单 — 没有泛型时写复制粘贴(Go 1.18 才有泛型,但社区仍克制使用)
- 错误是值 —
if err != nil不是啰嗦,是显式控制流 - goroutine 不是线程 — 是轻量协程,放心开
- channel 是通信方式 — “不要通过共享内存来通信,而要通过通信来共享内存”
- defer 就是 finally — 资源清理的 Go 式答案
go fmt无争议 — 所有 Go 代码长一个样,零风格战争- 隐式接口 — 不需要
implements关键字,有方法就算实现