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