Go資深工程師講解(慕課) 006_函數式編程

Go 函數式編程

對應視頻 Ch6(6-2 函數式編程例一),在 002.md 基礎上擴展更多函數式編程模式

1. 回顧:Go 中函數是一等公民

Go 不是純函數式語言,但函數可以作爲:
- 變量
- 參數
- 返回值
- 存放在數據結構中

// 函數作爲變量
var add = func(a, b int) int { return a + b }

// 函數作爲參數
func apply(op func(int, int) int, a, b int) int {
    return op(a, b)
}

// 函數作爲返回值
func makeGreeter(greeting string) func(string) string {
    return func(name string) string {
        return greeting + ", " + name + "!"
    }
}

func main() {
    fmt.Println(apply(add, 3, 4))       // 7
    hello := makeGreeter("Hello")
    fmt.Println(hello("Go"))             // Hello, Go!
}

2. 閉包(Closure)深入

閉包 = 函數 + 它引用的外部變量(自由變量)。函數和它引用的變量共同構成閉包。

2.1 累加器(002.md 已有)

func adder() func(int) int {
    sum := 0 // 自由變量,被閉包捕獲
    return func(v int) int {
        sum += v
        return sum
    }
}
// a := adder()
// a(1) -> 1, a(2) -> 3, a(10) -> 13

2.2 斐波那契生成器(002.md 已有)

func fibonacci() func() int {
    a, b := 0, 1
    return func() int {
        a, b = b, a+b
        return a
    }
}

2.3 閉包實現迭代器模式

// 通用的整數序列迭代器
func intRange(start, end int) func() (int, bool) {
    current := start
    return func() (int, bool) {
        if current > end {
            return 0, false
        }
        val := current
        current++
        return val, true
    }
}

func main() {
    next := intRange(1, 5)
    for v, ok := next(); ok; v, ok = next() {
        fmt.Println(v) // 1 2 3 4 5
    }
}

2.4 閉包的陷阱:循環變量捕獲

// 錯誤示範
func main() {
    var funcs []func()
    for i := 0; i < 3; i++ {
        funcs = append(funcs, func() {
            fmt.Println(i) // 捕獲的是變量 i 的地址,不是值
        })
    }
    for _, f := range funcs {
        f() // 輸出 3 3 3,不是 0 1 2
    }
}

// 正確做法:通過參數傳值
func main() {
    var funcs []func()
    for i := 0; i < 3; i++ {
        i := i // 重新聲明局部變量(Go 的慣用寫法)
        funcs = append(funcs, func() {
            fmt.Println(i)
        })
    }
    for _, f := range funcs {
        f() // 輸出 0 1 2
    }
}

3. 高階函數模式

3.1 Map / Filter / Reduce

Go 標準庫沒有泛型版本的 map/filter/reduce(Go 1.18+ 可以用泛型自己實現)。

// Go 1.18+ 泛型實現
func Map[T, U any](s []T, f func(T) U) []U {
    result := make([]U, len(s))
    for i, v := range s {
        result[i] = f(v)
    }
    return result
}

func Filter[T any](s []T, f func(T) bool) []T {
    var result []T
    for _, v := range s {
        if f(v) {
            result = append(result, v)
        }
    }
    return result
}

func Reduce[T, U any](s []T, init U, f func(U, T) U) U {
    result := init
    for _, v := range s {
        result = f(result, v)
    }
    return result
}

func main() {
    nums := []int{1, 2, 3, 4, 5}

    // 每個元素 *2
    doubled := Map(nums, func(n int) int { return n * 2 })
    fmt.Println(doubled) // [2 4 6 8 10]

    // 過濾偶數
    evens := Filter(nums, func(n int) bool { return n%2 == 0 })
    fmt.Println(evens) // [2 4]

    // 求和
    sum := Reduce(nums, 0, func(acc, n int) int { return acc + n })
    fmt.Println(sum) // 15
}

3.2 函數組合(Compose)

// 將兩個函數組合成一個
func Compose[T any](f, g func(T) T) func(T) T {
    return func(x T) T {
        return f(g(x))
    }
}

func main() {
    double := func(x int) int { return x * 2 }
    addOne := func(x int) int { return x + 1 }

    doubleAndAddOne := Compose(addOne, double)
    fmt.Println(doubleAndAddOne(3)) // (3*2)+1 = 7
}

4. 裝飾器 / 中間件模式

這是 Go 中函數式編程最常用的實際場景,廣泛應用於 HTTP 中間件。

4.1 基本裝飾器

// 給任意函數添加執行時間日誌
func timeTrack(fn func()) func() {
    return func() {
        start := time.Now()
        fn()
        fmt.Printf("執行耗時: %v\n", time.Since(start))
    }
}

func main() {
    slowFunc := func() {
        time.Sleep(100 * time.Millisecond)
        fmt.Println("done")
    }
    tracked := timeTrack(slowFunc)
    tracked()
    // done
    // 執行耗時: 100.xxxms
}

4.2 HTTP 中間件鏈(實際項目最常見)

type Middleware func(http.HandlerFunc) http.HandlerFunc

// 日誌中間件
func LoggingMiddleware(next http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        log.Printf("%s %s", r.Method, r.URL.Path)
        next(w, r)
    }
}

// 認證中間件
func AuthMiddleware(next http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        token := r.Header.Get("Authorization")
        if token == "" {
            http.Error(w, "Unauthorized", http.StatusUnauthorized)
            return
        }
        next(w, r)
    }
}

// 鏈式組合中間件
func Chain(f http.HandlerFunc, middlewares ...Middleware) http.HandlerFunc {
    for i := len(middlewares) - 1; i >= 0; i-- {
        f = middlewares[i](f)
    }
    return f
}

func main() {
    hello := func(w http.ResponseWriter, r *http.Request) {
        w.Write([]byte("Hello!"))
    }
    // 請求流經: Logging → Auth → hello
    http.HandleFunc("/api", Chain(hello, LoggingMiddleware, AuthMiddleware))
    http.ListenAndServe(":8080", nil)
}

4.3 對比 002.md 中的 errWrapper

002.md 中的 errWrapper 本質就是裝飾器模式:

// 002.md 中的模式
type appHandler func(http.ResponseWriter, *http.Request) error

func errWrapper(handler appHandler) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        // 統一錯誤處理邏輯
        if err := handler(w, r); err != nil {
            // 處理各種錯誤...
        }
    }
}

5. 選項模式(Functional Options)

這是 Go 社區最流行的函數式設計模式之一,由 Dave Cheney 和 Rob Pike 提出。

type Server struct {
    host    string
    port    int
    timeout time.Duration
    maxConn int
}

// Option 是一個函數類型
type Option func(*Server)

// 每個選項返回一個 Option 函數
func WithHost(host string) Option {
    return func(s *Server) {
        s.host = host
    }
}

func WithPort(port int) Option {
    return func(s *Server) {
        s.port = port
    }
}

func WithTimeout(timeout time.Duration) Option {
    return func(s *Server) {
        s.timeout = timeout
    }
}

func WithMaxConn(maxConn int) Option {
    return func(s *Server) {
        s.maxConn = maxConn
    }
}

// 構造函數接收可變數量的 Option
func NewServer(opts ...Option) *Server {
    // 默認值
    s := &Server{
        host:    "localhost",
        port:    8080,
        timeout: 30 * time.Second,
        maxConn: 100,
    }
    // 應用所有選項
    for _, opt := range opts {
        opt(s)
    }
    return s
}

func main() {
    // 靈活創建,只傳需要的選項
    s1 := NewServer()
    s2 := NewServer(WithPort(9090), WithTimeout(60*time.Second))
    s3 := NewServer(WithHost("0.0.0.0"), WithPort(443), WithMaxConn(1000))
    fmt.Println(s1, s2, s3)
}

優勢:
- 不需要記參數順序
- 新增選項不破壞已有代碼
- 有清晰的默認值
- 比 builder 模式更 Go 風格

6. 函數式編程用於樹遍歷(002.md 擴展)

002.md 中已有 TraverseFunc,這裏展示更多應用:

// 002.md 中的基礎
func (node *Node) TraverseFunc(f func(*Node)) {
    if node == nil { return }
    node.Left.TraverseFunc(f)
    f(node)
    node.Right.TraverseFunc(f)
}

// 應用1:統計節點數
func (node *Node) Count() int {
    count := 0
    node.TraverseFunc(func(n *Node) {
        count++
    })
    return count
}

// 應用2:求最大值
func (node *Node) Max() int {
    maxVal := math.MinInt64
    node.TraverseFunc(func(n *Node) {
        if n.Value > maxVal {
            maxVal = n.Value
        }
    })
    return maxVal
}

// 應用3:收集所有值
func (node *Node) Collect() []int {
    var result []int
    node.TraverseFunc(func(n *Node) {
        result = append(result, n.Value)
    })
    return result
}

7. 惰性求值與無限序列

利用 channel + goroutine 模擬惰性求值:

// 無限自然數序列
func naturals() <-chan int {
    ch := make(chan int)
    go func() {
        for i := 0; ; i++ {
            ch <- i
        }
    }()
    return ch
}

// take: 從 channel 中取前 n 個值
func take(n int, ch <-chan int) []int {
    result := make([]int, n)
    for i := 0; i < n; i++ {
        result[i] = <-ch
    }
    return result
}

// 篩法求素數(經典的 CSP 併發+函數式示例)
func sieve() <-chan int {
    out := make(chan int)
    go func() {
        ch := naturals()
        <-ch // 跳過 0
        <-ch // 跳過 1
        for {
            prime := <-ch
            out <- prime
            // 創建新的過濾器 goroutine
            next := make(chan int)
            go func(src <-chan int, p int) {
                for v := range src {
                    if v%p != 0 {
                        next <- v
                    }
                }
            }(ch, prime)
            ch = next
        }
    }()
    return out
}

func main() {
    // 取前 20 個素數
    primes := take(20, sieve())
    fmt.Println(primes)
    // [2 3 5 7 11 13 17 19 23 29 31 37 41 43 47 53 59 61 67 71]
}

8. 函數式錯誤處理

避免大量 if err != nil 的一種思路:

// 鏈式操作,遇到錯誤就停止
type Result struct {
    val interface{}
    err error
}

func (r Result) Then(f func(interface{}) (interface{}, error)) Result {
    if r.err != nil {
        return r // 已經有錯誤,直接透傳
    }
    val, err := f(r.val)
    return Result{val, err}
}

// 使用示例
func processFile(path string) Result {
    return Result{path, nil}.
        Then(func(v interface{}) (interface{}, error) {
            return os.Open(v.(string))
        }).
        Then(func(v interface{}) (interface{}, error) {
            return io.ReadAll(v.(*os.File))
        }).
        Then(func(v interface{}) (interface{}, error) {
            data := v.([]byte)
            return strings.ToUpper(string(data)), nil
        })
}

注意:這種模式在 Go 中不推薦在生產代碼中大量使用,因爲它犧牲了類型安全和可讀性。Go 社區更推崇顯式的 if err != nil 風格。這裏僅作爲函數式思想的演示。

9. 總結

模式 Go 中的應用場景 常見程度
閉包 延遲計算、狀態封裝、生成器 非常常見
高階函數 sort.Slice、http.HandleFunc 非常常見
裝飾器/中間件 HTTP中間件、日誌、認證、限流 非常常見
Functional Options 庫/框架的配置 API 非常常見
Map/Filter/Reduce 數據處理(Go 1.18+泛型後更實用) 常見
函數組合 管道處理 偶爾
惰性求值 channel + goroutine 模擬 偶爾
函數式錯誤處理 實驗性,不推薦生產使用 少見

主題測試文章,只做測試使用。發佈者:Walker,轉轉請注明出處:https://walker-learn.xyz/archives/6748

(0)
Walker的頭像Walker
上一篇 14小時前
下一篇 2025年4月20日 19:12

相關推薦

  • Go工程師體系課 003

    grpc grpc grpc-go grpc 無縫集成了 protobuf protobuf 習慣用 Json、XML 數據存儲格式的你們,相信大多都沒聽過 Protocol Buffer。 Protocol Buffer 其實是 Google 出品的一種輕量 & 高效的結構化數據存儲格式,性能比 Json、XML 真的強!太!多! protobuf…

    後端開發 14小時前
    500
  • Go工程師體系課 010

    es 安裝 elasticsearch(理解爲庫) kibana(理解爲連接工具)es 和 kibana(5601) 的版本要保持一致 MySQL 對照學習 Elasticsearch(ES) 術語對照 MySQL Elasticsearch database index(索引) table type(7.x 起固定爲 _doc,8.x 徹底移除多 type…

    後端開發 7小時前
    200
  • Go資深工程師講解(慕課) 003

    003 測試 吐槽別人家的,go語言採用表格驅動測試 測試數據和測試邏輯混在一些 出錯信息不明確 一旦一個數據出錯測試全部結束 表格驅動測試 test:=[]struct{ a,b,c int32 }{ {1,2,3}, {0,2,0}, {0,0,0}, {0,0,0}, {-1,1,0}, {math.MaxInt32,1,math.MinInt32},…

  • Go工程師體系課 protoc-gen-validate

    protoc-gen-validate 簡介與使用指南 ✅ 什麼是 protoc-gen-validate protoc-gen-validate(簡稱 PGV)是一個 Protocol Buffers 插件,用於在生成的 Go 代碼中添加結構體字段的驗證邏輯。 它通過在 .proto 文件中添加 validate 規則,自動爲每個字段生成驗證代碼,避免你手…

  • Go工程師體系課 014

    rocketmq 快速入門 去我們的各種配置(podman)看是怎麼安裝的 概念介紹 RocketMQ 是阿里開源、Apache 頂級項目的分佈式消息中間件,核心組件: NameServer:服務發現與路由 Broker:消息存儲、投遞、拉取 Producer:消息生產者(發送消息) Consumer:消息消費者(訂閱並消費消息) Topic/Tag:主題/…

簡體中文 繁體中文 English