Go協程揭秘:輕量、併發與性能的完美結合

来源:https://www.cnblogs.com/xfuture/archive/2023/09/19/17715023.html
-Advertisement-
Play Games

Go協程為併發編程提供了強大的工具,結合輕量級、高效的特點,為開發者帶來了獨特的編程體驗。本文深入探討了Go協程的基本原理、同步機制、高級用法及其性能與最佳實踐,旨在為讀者提供全面、深入的理解和應用指導。 關註公眾號【TechLeadCloud】,分享互聯網架構、雲服務技術的全維度知識。作者擁有10 ...


Go協程為併發編程提供了強大的工具,結合輕量級、高效的特點,為開發者帶來了獨特的編程體驗。本文深入探討了Go協程的基本原理、同步機制、高級用法及其性能與最佳實踐,旨在為讀者提供全面、深入的理解和應用指導。

關註公眾號【TechLeadCloud】,分享互聯網架構、雲服務技術的全維度知識。作者擁有10+年互聯網服務架構、AI產品研發經驗、團隊管理經驗,同濟本復旦碩,復旦機器人智能實驗室成員,阿裡雲認證的資深架構師,項目管理專業人士,上億營收AI產品研發負責人。

file

1. Go協程簡介

Go協程(goroutine)是Go語言中的併發執行單元,它比傳統的線程輕量得多,並且是Go語言併發模型中的核心組成部分。在Go中,你可以同時運行成千上萬的goroutine,而不用擔心常規操作系統線程帶來的開銷。

什麼是Go協程?

Go協程是與其他函數或方法並行運行的函數或方法。你可以認為它類似於輕量級的線程。其主要優勢在於它的啟動和停止開銷非常小,相比於傳統的線程來說,可以更有效地實現併發。

package main

import (
    "fmt"
    "time"
)

func sayHello() {
    for i := 0; i < 5; i++ {
        time.Sleep(100 * time.Millisecond)
        fmt.Println("Hello!")
    }
}

func main() {
    go sayHello() // 啟動一個Go協程
    for i := 0; i < 5; i++ {
        time.Sleep(150 * time.Millisecond)
        fmt.Println("Hi!")
    }
}

輸出:

Hi!
Hello!
Hi!
Hello!
Hello!
Hi!
Hello!
Hi!
Hello!

處理過程:
在上面的代碼中,我們定義了一個sayHello函數,它在一個迴圈中列印“Hello!”五次。在main函數中,我們使用go關鍵字啟動了sayHello作為一個goroutine。此後,我們又在main中列印“Hi!”五次。因為sayHello是一個goroutine,所以它會與main中的迴圈並行執行。因此,輸出中“Hello!”和“Hi!”的列印順序可能會變化。

Go協程與線程的比較

  1. 啟動開銷:Go協程的啟動開銷遠小於線程。因此,你可以輕鬆啟動成千上萬個goroutine。
  2. 記憶體占用:每個Go協程的堆棧大小開始時很小(通常在幾KB),並且可以根據需要增長和縮小,而線程通常需要固定的、較大的堆棧記憶體(通常為1MB或更多)。
  3. 調度:Go協程是由Go運行時系統而不是操作系統調度的。這意味著Go協程之間的上下文切換開銷更小。
  4. 安全性:Go協程為開發者提供了簡化的併發模型,配合通道(channels)等同步機制,減少了併發程式中常見的錯誤。

示例代碼:

package main

import (
    "fmt"
    "time"
)

func worker(id int, ch chan int) {
    for {
        fmt.Printf("Worker %d received data: %d\n", id, <-ch)
    }
}

func main() {
    ch := make(chan int)

    for i := 0; i < 3; i++ {
        go worker(i, ch) // 啟動三個Go協程
    }

    for i := 0; i < 10; i++ {
        ch <- i
        time.Sleep(100 * time.Millisecond)
    }
}

輸出:

Worker 0 received data: 0
Worker 1 received data: 1
Worker 2 received data: 2
Worker 0 received data: 3
...

處理過程:
在這個示例中,我們啟動了三個工作goroutine來從同一個通道接收數據。在main函數中,我們發送數據到通道。每當通道中有數據時,其中一個工作goroutine會接收並處理它。由於goroutines是併發運行的,所以哪個goroutine接收數據是不確定的。

Go協程的核心優勢

  1. 輕量級:如前所述,Go協程的啟動開銷和記憶體使用都遠遠小於傳統線程。
  2. 靈活的調度:Go協程是協同調度的,允許用戶在適當的時機進行任務切換。
  3. 簡化的併發模型:Go提供了多種原語(如通道和鎖),使併發編程變得更加簡單和安全。

總的來說,Go協程為開發者提供了一個高效、靈活且安全的併發模型。與此同時,Go的標準庫提供了豐富的工具和包,進一步簡化了併發程式的開發過程。


2. Go協程的基本使用

在Go中,協程是構建併發程式的基礎。創建協程非常簡單,並且使用go關鍵字就可以啟動。讓我們探索一些基本用法和與之相關的示例。

創建並啟動Go協程

啟動一個Go協程只需使用go關鍵字,後跟一個函數調用。這個函數即可以是匿名的,也可以是預定義的。

示例代碼:

package main

import (
    "fmt"
    "time"
)

func printNumbers() {
    for i := 1; i <= 5; i++ {
        time.Sleep(200 * time.Millisecond)
        fmt.Println(i)
    }
}

func main() {
    go printNumbers()  // 啟動一個Go協程
    time.Sleep(1 * time.Second)
    fmt.Println("End of main function")
}

輸出:

1
2
3
4
5
End of main function

處理過程:
在這個示例中,我們定義了一個printNumbers函數,它會簡單地列印數字1到5。在main函數中,我們使用go關鍵字啟動了這個函數作為一個新的Go協程。主函數與Go協程並行執行。為確保主函數等待Go協程執行完成,我們使主函數休眠了1秒鐘。

使用匿名函數創建Go協程

除了啟動預定義的函數,你還可以使用匿名函數直接啟動Go協程。

示例代碼:

package main

import (
    "fmt"
    "time"
)

func main() {
    go func() {
        fmt.Println("This is a goroutine!")
        time.Sleep(500 * time.Millisecond)
    }()
    fmt.Println("This is the main function!")
    time.Sleep(1 * time.Second)
}

輸出:

This is the main function!
This is a goroutine!

處理過程:
在這個示例中,我們在main函數中直接使用了一個匿名函數來創建Go協程。在匿名函數中,我們簡單地列印了一條消息並使其休眠了500毫秒。主函數先列印其消息,然後等待1秒來確保Go協程有足夠的時間完成執行。

Go協程與主函數

值得註意的是,如果主函數(main)結束,所有的Go協程都會被立即終止,不論它們的執行狀態如何。

示例代碼:

package main

import (
    "fmt"
    "time"
)

func main() {
    go func() {
        time.Sleep(500 * time.Millisecond)
        fmt.Println("This will not print!")
    }()
}

處理過程:
在上面的代碼中,Go協程在列印消息前休眠了500毫秒。但由於主函數在此期間已經結束,所以Go協程也被終止,因此我們不會看到任何輸出。

總結,Go協程的基本使用非常簡單和直觀,但需要註意確保主函數在所有Go協程執行完畢之前不會結束。


3. Go協程的同步機制

在併發編程中,同步是確保多個協程能夠有效、安全地共用資源或協同工作的關鍵。Go提供了幾種原語,幫助我們實現這一目標。

1. 通道 (Channels)

通道是Go中用於在協程之間傳遞數據和同步執行的主要方式。它們提供了一種在一個協程中發送數據,併在另一個協程中接收數據的機制。

示例代碼:

package main

import "fmt"

func sendData(ch chan string) {
    ch <- "Hello from goroutine!"
}

func main() {
    messageChannel := make(chan string)
    go sendData(messageChannel) // 啟動一個Go協程發送數據
    message := <-messageChannel
    fmt.Println(message)
}

輸出:

Hello from goroutine!

處理過程:
我們創建了一個名為messageChannel的通道。然後啟動了一個Go協程sendData,將字元串"Hello from goroutine!"發送到這個通道。在主函數中,我們從通道接收這個消息並列印它。

2. sync.WaitGroup

sync.WaitGroup是一個等待一組協程完成的結構。你可以增加一個計數來表示應等待的協程數量,併在每個協程完成時減少計數。

示例代碼:

package main

import (
    "fmt"
    "sync"
    "time"
)

func worker(id int, wg *sync.WaitGroup) {
    defer wg.Done()
    fmt.Printf("Worker %d starting\n", id)
    time.Sleep(time.Second)
    fmt.Printf("Worker %d done\n", id)
}

func main() {
    var wg sync.WaitGroup

    for i := 1; i <= 5; i++ {
        wg.Add(1)
        go worker(i, &wg)
    }

    wg.Wait()
    fmt.Println("All workers completed.")
}

輸出:

Worker 1 starting
Worker 2 starting
Worker 3 starting
Worker 4 starting
Worker 5 starting
Worker 1 done
Worker 2 done
Worker 3 done
Worker 4 done
Worker 5 done
All workers completed.

處理過程:
我們定義了一個名為worker的函數,它模擬一個需要一秒鐘才能完成的工作任務。在這個函數中,我們使用defer wg.Done()來確保在函數退出時減少WaitGroup的計數。在main函數中,我們啟動了5個這樣的工作協程,每啟動一個,我們就使用wg.Add(1)來增加計數。wg.Wait()則會阻塞,直到所有工作協程都通知WaitGroup它們已完成。

3. 互斥鎖 (sync.Mutex)

當多個協程需要訪問共用資源時(例如,更新一個共用變數),使用互斥鎖可以確保同時只有一個協程能訪問資源,防止數據競態。

示例代碼:

package main

import (
    "fmt"
    "sync"
)

var counter int
var lock sync.Mutex

func increment() {
    lock.Lock()
    counter++
    lock.Unlock()
}

func main() {
    var wg sync.WaitGroup

    for i := 0; i < 1000; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            increment()
        }()
    }

    wg.Wait()
    fmt.Println("Final Counter:", counter)
}

輸出:

Final Counter: 1000

處理過程:
我們有一個全局變數counter,我們希望在多個Go協程中併發地增加它。為了確保每次只有一個Go協程能夠更新counter,我們使用了互斥鎖lock來同步訪問。

這些是Go協程同步機制的一些基本方法。正確地使用它們可以幫助你編寫更安全、更高效的併發程式。


4. Go協程的高級用法

Go協程的高級用法涉及更複雜的併發模式、錯誤處理和協程式控制制。我們將探索一些常見的高級用法和它們的具體應用示例。

1. 選擇器 (select)

select語句是Go中處理多個通道的方法。它允許你等待多個通道操作,執行其中一個可以進行的操作。

示例代碼:

package main

import (
    "fmt"
    "time"
)

func main() {
    ch1 := make(chan string)
    ch2 := make(chan string)

    go func() {
        time.Sleep(1 * time.Second)
        ch1 <- "Data from channel 1"
    }()

    go func() {
        time.Sleep(2 * time.Second)
        ch2 <- "Data from channel 2"
    }()

    for i := 0; i < 2; i++ {
        select {
        case msg1 := <-ch1:
            fmt.Println(msg1)
        case msg2 := <-ch2:
            fmt.Println(msg2)
        }
    }
}

輸出:

Data from channel 1
Data from channel 2

處理過程:
我們創建了兩個通道ch1ch2。兩個Go協程分別向這兩個通道發送數據,但它們的休眠時間不同。在select語句中,我們等待兩個通道中的任何一個準備好數據,然後進行處理。由於ch1的數據先到達,因此它的消息首先被列印。

2. 超時處理

使用select,我們可以輕鬆實現對通道操作的超時處理。

示例代碼:

package main

import (
    "fmt"
    "time"
)

func main() {
    ch := make(chan string)

    go func() {
        time.Sleep(3 * time.Second)
        ch <- "Data from goroutine"
    }()

    select {
    case data := <-ch:
        fmt.Println(data)
    case <-time.After(2 * time.Second):
        fmt.Println("Timeout after 2 seconds")
    }
}

輸出:

Timeout after 2 seconds

處理過程:
Go協程會休眠3秒鐘後再向ch發送數據。在select語句中,我們等待這個通道的數據或2秒的超時。由於Go協程在超時之前沒有發送數據,因此超時的消息被列印。

3. 使用context進行協程式控制制

context包允許我們共用跨多個協程的取消信號、超時和其他設置。

示例代碼:

package main

import (
    "context"
    "fmt"
    "time"
)

func work(ctx context.Context) {
    for {
        select {
        case <-ctx.Done():
            fmt.Println("Received cancel signal, stopping the work")
            return
        default:
            fmt.Println("Still working...")
            time.Sleep(1 * time.Second)
        }
    }
}

func main() {
    ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
    defer cancel()

    go work(ctx)

    time.Sleep(5 * time.Second)
}

輸出:

Still working...
Still working...
Still working...
Received cancel signal, stopping the work

處理過程:
在這個示例中,我們創建了一個帶有3秒超時的context。Go協程work會持續工作,直到接收到取消信號或超時。經過3秒後,context的超時被觸發,Go協程接收到了取消信號並停止工作。

這些高級用法為Go協程提供了強大的功能,使得複雜的併發模式和控製成為可能。掌握這些高級技巧可以幫助你編寫更健壯、更高效的Go併發程式。


5. Go協程的性能與最佳實踐

Go協程為併發編程提供了輕量級的解決方案。但為了充分利用其性能優勢並避免常見的陷阱,瞭解一些最佳實踐和性能考慮因素是很有必要的。

1. 限制併發數

雖然Go協程是輕量級的,但無節制地創建大量的Go協程可能會導致記憶體耗盡或調度開銷增大。

示例代碼:

package main

import (
    "fmt"
    "sync"
)

func worker(id int, wg *sync.WaitGroup) {
    defer wg.Done()
    fmt.Printf("Worker %d started\n", id)
}

func main() {
    var wg sync.WaitGroup
    numWorkers := 1000

    for i := 1; i <= numWorkers; i++ {
        wg.Add(1)
        go worker(i, &wg)
    }

    wg.Wait()
    fmt.Println("All workers done")
}

輸出:

Worker 1 started
Worker 2 started
...
Worker 1000 started
All workers done

處理過程:
這個示例創建了1000個工作Go協程。儘管這個數字可能不會導致問題,但如果不加限制地創建更多的Go協程,可能會導致問題。

2. 避免競態條件

多個Go協程可能會同時訪問共用資源,導致不確定的結果。使用互斥鎖(Mutex)或其他同步機制來確保數據的一致性。

示例代碼:

package main

import (
    "fmt"
    "sync"
)

var (
    counter int
    mu      sync.Mutex
)

func increment(wg *sync.WaitGroup) {
    defer wg.Done()
    mu.Lock()
    counter++
    mu.Unlock()
}

func main() {
    var wg sync.WaitGroup

    for i := 0; i < 1000; i++ {
        wg.Add(1)
        go increment(&wg)
    }

    wg.Wait()
    fmt.Println("Final counter value:", counter)
}

輸出:

Final counter value: 1000

處理過程:
我們使用sync.Mutex確保在增加計數器時的互斥訪問。這確保了併發訪問時的數據一致性。

3. 使用工作池模式

工作池模式是創建固定數量的Go協程來執行任務的方法,避免過度創建Go協程。任務通過通道發送。

示例代碼:

package main

import (
    "fmt"
    "sync"
)

func worker(tasks <-chan int, wg *sync.WaitGroup) {
    defer wg.Done()
    for task := range tasks {
        fmt.Printf("Worker processed task %d\n", task)
    }
}

func main() {
    var wg sync.WaitGroup
    tasks := make(chan int, 100)

    // Start 5 workers.
    for i := 0; i < 5; i++ {
        wg.Add(1)
        go worker(tasks, &wg)
    }

    // Send 100 tasks.
    for i := 1; i <= 100; i++ {
        tasks <- i
    }

    close(tasks)
    wg.Wait()
}

輸出:

Worker processed task 1
Worker processed task 2
...
Worker processed task 100

處理過程:
我們創建了5個工作Go協程,它們從tasks通道中接收任務。這種模式可以控制併發數並重覆使用Go協程。

遵循這些最佳實踐不僅可以使你的Go協程代碼更加健壯,而且還可以更有效地利用系統資源,提高程式的整體性能。


6.總結

隨著計算技術的進步,併發和並行成為了現代軟體開發中的關鍵元素。Go語言作為一個現代編程語言,通過其內置的goroutine為開發者提供了一種簡潔而強大的併發編程模式。但正如我們在前面的章節中所看到的,理解其工作原理、同步機制、高級用法及性能與最佳實踐是至關重要的。

從本文中,我們不僅瞭解了Go協程的基礎知識和工作原理,還探討了一些關於如何最大限度地發揮其性能的高級主題。關鍵的洞察包括:

  1. 輕量與高效:Go協程是輕量級的線程,但它們在實現上的特點使其在大量併發場景下更為高效。
  2. 同步與通信:Go的哲學是“不通過共用記憶體來通信,而是通過通信來共用記憶體”。這反映在其強大的channel機制中,這也是避免許多併發問題的關鍵。
  3. 性能與最佳實踐:理解並遵循最佳實踐不僅可以確保代碼的健壯性,而且還可以顯著提高性能。

最後,雖然Go提供了強大的工具和機制來處理併發,但真正的藝術在於如何正確地使用它們。正如我們在軟體工程中經常看到的那樣,工具只是手段,真正的力量在於瞭解它們的工作原理並正確地應用它們。

希望本文為您提供了關於Go協程的深入、全面的認識,併為您的併發編程之旅提供了有價值的洞見和指導。正如在雲服務、互聯網服務架構和其他複雜的系統中經常可以看到的那樣,真正掌握併發是提高性能、擴展性和響應速度的關鍵。

關註公眾號【TechLeadCloud】,分享互聯網架構、雲服務技術的全維度知識。作者擁有10+年互聯網服務架構、AI產品研發經驗、團隊管理經驗,同濟本復旦碩,復旦機器人智能實驗室成員,阿裡雲認證的資深架構師,項目管理專業人士,上億營收AI產品研發負責人。

如有幫助,請多關註
個人微信公眾號:【TechLeadCloud】分享AI與雲服務研發的全維度知識,談談我作為TechLead對技術的獨特洞察。
TeahLead KrisChang,10+年的互聯網和人工智慧從業經驗,10年+技術和業務團隊管理經驗,同濟軟體工程本科,復旦工程管理碩士,阿裡雲認證雲服務資深架構師,上億營收AI產品業務負責人。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 早上看到一篇關於Spring Boot虛擬線程和Webflux性能對比的文章,覺得還不錯。內容較長,我就不翻譯了,抓重點給大家介紹一下這篇文章的核心內容,方便大家快速閱讀。 測試場景 作者採用了一個儘可能貼近現實操作的場景: 從授權頭信息中提取JWT 驗證JWT並從中提取用戶的Email 使用用戶的 ...
  • 你可能會為不同版本的工具鏈而煩惱,就算是 ruster 也一樣。介紹一個非常好用的管理工具給大伙,就是使用 Rustup 安裝和更新 Rust 工具鏈。安裝 Rustup 之後,可以在 stable, beta,和 nightly 等版本的 Rust 編譯器工具之間無縫切換。 ...
  • MyBatis中#{}和${}的用法 說一下為什麼要寫這篇文章,最近面試有被問到,一下子想不出來有啥區別,想記錄一下加深自己的理解,同時自己也經常用MyBatis-Plus忽略了XML文件的編寫和使用,所以需要加深一下這塊的知識 一、例子 1、#{}將傳入的數據當作一個字元串,會對傳入的數據加上一個 ...
  • Caffeine Cache以其高性能和可擴展性贏得 本地緩存之王 的稱號,它是一個Java緩存庫。它的設計目標是優化計算速度、記憶體效率和實用性,以符合現代軟體開發者的需求。 ...
  • C++和Rust通過wasmtime實現相互調用實例 1 wasmtime介紹 wasmtime是一個可以運行WebAssembly代碼的運行時環境。 WebAssembly是一種可移植的二進位指令集格式,其本身與平臺無關,類似於Java的class文件位元組碼。 WebAssembly本來的設計初衷 ...
  • 哈嘍大家好,我是鹹魚 當談到編程效率和性能優化時,Python 常常被調侃為“慢如蝸牛” 有趣的是,Python 代碼在函數中運行往往比在全局範圍內運行要快得多 小伙伴們可能會有這個疑問:為什麼在函數中運行的 Python 代碼速度更快? 今天這篇文章將會解答大家心中的疑惑 原文鏈接:https:/ ...
  • Arrays.asList() 是一個 Java 的靜態方法,它可以把一個數組或者多個參數轉換成一個 List 集合。這個方法可以作為數組和集合之間的橋梁,方便我們使用集合的一些方法和特性。本文將介紹 Arrays.asList() 的語法、應用場景、坑點和總結。 語法 應用場景 坑點 總結 語法 ...
  • 協程 & asyncio & 非同步 1. 協程 (coroutine) 協程不是電腦提供,而是程式員人為創造。 協程(coroutine),也可以被稱為微線程,是一種用戶態內的上下文切換技術。簡而言之,其實就是通過一個線程實現代碼塊互相切換運行。例如: def func1(): print(1) ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...