输入关键词开始搜索

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 {
    // 用户不存在或密码错
}
操作写法
创建空 mapmake(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.21mux.HandleFunc("/health", h) + 在 handler 内 if r.Method != "GET"
≥ 1.22mux.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 的三个特性:

  1. LIFO — 多个 defer 按后进先出执行
  2. 参数立即求值defer fmt.Println(i)i 在 defer 语句时取值
  3. 可修改命名返回值 — 闭包内改命名返回值会影响最终结果

Go 没有 try-finallydefer 就是它的 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.RawURLEncodingURL 安全 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 里自动转为对应类型

anyinterface{} 的别名(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++PythonGo
classclassstruct + 接收者方法
构造函数与类同名__init__NewXxx() 函数
继承: public Baseclass D(Base)组合(embedding)
多态virtual鸭子类型interface
错误处理异常异常if err != nil
并发std::threadasynciogo + channel
包管理CMake/vcpkgpip/poetrygo mod
格式化clang-formatblackgo fmt(唯一标准)

核心心法

  1. Go 追求简单 — 没有泛型时写复制粘贴(Go 1.18 才有泛型,但社区仍克制使用)
  2. 错误是值if err != nil 不是啰嗦,是显式控制流
  3. goroutine 不是线程 — 是轻量协程,放心开
  4. channel 是通信方式 — “不要通过共享内存来通信,而要通过通信来共享内存”
  5. defer 就是 finally — 资源清理的 Go 式答案
  6. go fmt 无争议 — 所有 Go 代码长一个样,零风格战争
  7. 隐式接口 — 不需要 implements 关键字,有方法就算实现