Go Wiki: GoForCPPProgrammers

Go 是一種系統程式語言,旨在成為一種通用的系統語言,類似於 C++。以下是一些面向經驗豐富的 C++ 程式設計師的 Go 筆記。本文件討論了 Go 和 C++ 之間的區別,而很少或根本不討論它們的相似之處。

需要牢記的一個重要觀點是,要精通這兩種語言,需要一些基本的思維過程差異。最顯著的是,C++ 的物件模型基於類和類層次結構,而 Go 的物件模型基於介面(並且本質上是扁平的)。因此,C++ 的設計模式很少能直接照搬到 Go。要有效地使用 Go 程式設計,人們必須考慮要解決的*問題*,而不是用來解決問題的 C++ 機制。

有關 Go 的更通用介紹,請參閱 Go Tour如何編寫 Go 程式碼Effective Go

有關 Go 語言的詳細描述,請參閱 Go 規範

概念上的差異

  • Go 沒有帶有建構函式或解構函式的類。Go 使用*介面*代替類方法、類繼承層次結構和虛擬函式,介面將在下文詳細討論。介面也用於 C++ 使用模板的場景。
  • Go 提供自動記憶體垃圾回收。無需(也無法)顯式釋放記憶體。無需擔心堆分配與棧分配儲存、newmalloc,或 deletedelete[]free。也無需單獨管理 std::unique_ptrstd::shared_ptrstd::weak_ptrstd::auto_ptr 以及普通、非智慧的“原生”指標。Go 的執行時系統會替程式設計師處理所有這些易出錯的程式碼。
  • Go 具有指標但不支援指標算術。因此,Go 指標更接近 C++ 的引用。不能使用 Go 指標變數來遍歷字串的位元組。切片(將在下文進一步討論)可以滿足指標算術的大部分需求。
  • Go 預設是“安全”的。指標不能指向任意記憶體,緩衝區溢位會導致崩潰,而不是安全漏洞。unsafe 包允許程式設計師在明確要求時繞過 Go 的一些保護機制。
  • Go 中的陣列是第一類值。當陣列用作函式引數時,函式會接收陣列的副本,而不是指向陣列的指標。然而,在實際中,函式通常使用切片作為引數;切片包含指向底層陣列的指標。切片將在下文進一步討論。
  • 語言本身提供了字串。建立後,字串不可更改。
  • 語言本身提供了雜湊表。它們被稱為對映(map)。
  • 語言本身提供了獨立的執行執行緒以及它們之間的通訊通道。這將在下文進一步討論。
  • 某些型別(對映和通道,將在下文進一步討論)按引用傳遞,而不是按值傳遞。也就是說,將對映傳遞給函式不會複製對映,如果函式更改了對映,呼叫者將看到更改。用 C++ 的術語來說,可以認為這些是引用型別。
  • Go 不使用標頭檔案。相反,每個原始檔都屬於一個已定義的*包*。當一個包定義了一個以大寫字母開頭的名稱的物件(型別、常量、變數、函式)時,該物件對匯入該包的任何其他檔案都是可見的。
  • Go 不支援隱式型別轉換。混合不同型別的操作需要強制轉換(在 Go 中稱為轉換)。即使是相同底層型別的不同使用者定義別名也需要轉換。這確保了額外的安全性。
  • Go 不支援函式過載,也不支援使用者定義的運算子。
  • Go 不支援 constvolatile 限定符。
  • Go 在 C++ 使用 NULL0(或 C++11 中的 nullptr)的地方使用 nil 表示無效指標。
  • 慣用的 Go 使用多個返回值來傳達錯誤——一個或多個數據結果加上一個錯誤程式碼——而不是使用哨兵值(例如 -1)或結構化異常處理(C++ 的 trycatchthrow 或 Go 的 panicrecover)。

語法

宣告語法與 C++ 相反。先寫名稱,後寫型別。與 C++ 不同,型別的語法與變數的使用方式不匹配。型別宣告可以從左到右輕鬆讀取。(var v1 int →“變數 v1 是一個 int。”)

//Go                      C++
var v1 int                // int v1;
var v2 string             // const std::string v2;  (approximately)
var v3 [10]int            // int v3[10];
var v4 []int              // int* v4;  (approximately)
var v5 struct { f int }   // struct { int f; } v5;
var v6 *int               // int* v6;  (but no pointer arithmetic)
var v7 map[string]int     // unordered_map<string, int>* v7;  (approximately)
var v8 func(a int) int    // int (*v8)(int a);

宣告通常採用關鍵字後跟被宣告物件名稱的形式。關鍵字是 varfuncconsttype 之一。方法宣告是一個小的例外,其中接收者出現在被宣告物件名稱之前;請參閱關於介面的討論

您還可以使用關鍵字後跟括號中的一系列宣告。

var (
    i int
    m float64
)

宣告函式時,必須為每個引數提供名稱,或者不為任何引數提供名稱。(也就是說,C++ 允許 void f(int i, int);,但 Go 不允許類似的 func f(i int, int)。)然而,為了方便起見,在 Go 中您可以將多個具有相同型別的名稱分組。

func f(i, j, k int, s, t string)

宣告變數時可以對其進行初始化。執行此操作時,允許指定型別,但不是必需的。未指定型別時,變數的型別是初始化表示式的型別。

var v = *p

另請參閱下文關於常量的討論。如果變數未顯式初始化,則必須指定型別。在這種情況下,它將隱式初始化為型別的零值(0nil 等)。Go 中沒有未初始化的變數。

在函式內部,可以使用 := 提供簡短的宣告語法。

v1 := v2 // C++11: auto v1 = v2;

這等同於

var v1 = v2 // C++11: auto v1 = v2;

Go 允許並行執行多個賦值。也就是說,首先計算右側的所有值,然後將這些值賦給左側的變數。

i, j = j, i // Swap i and j.

函式可以有多個返回值,由括號中的列表表示。返回的值可以賦值給變數列表來儲存。

func f() (i int, j int) { ... }
v1, v2 = f()

多個返回值是 Go 處理錯誤的主要機制。

result, ok := g()
if !ok {
  // Something bad happened.
  return nil
}
// Continue as normal.
…

或者,更簡潔地說,

if result, ok := g(); !ok {
  // Something bad happened.
  return nil
}
// Continue as normal.
…

實際上,Go 程式碼很少使用分號。嚴格來說,所有 Go 語句都以分號結尾。然而,Go 將非空行的末尾視為分號,除非該行明顯不完整(確切規則請參閱語言規範)。其後果是,在某些情況下 Go 不允許您使用換行符。例如,您不能寫

func g()
{                  // INVALID
}

將在 g() 之後插入分號,使其成為函式宣告而不是函式定義。同樣,您也不能寫

if x {
}
else {             // INVALID
}

將在 else 前面的 } 之後插入分號,導致語法錯誤。

由於分號確實會結束語句,因此您可以像在 C++ 中一樣繼續使用它們。然而,這不符合推薦的風格。慣用的 Go 程式碼省略了不必要的分號,實際上就是除了初始 for 迴圈子句和您希望在同一行上放置多個短語句的情況之外的所有分號。

既然談到這個話題,我們建議您不要擔心分號和花括號的位置,而是使用 gofmt 程式來格式化您的程式碼。這將產生一種標準的 Go 風格,讓您專注於程式碼本身,而不是格式化。雖然這種風格起初可能看起來奇怪,但與其他任何風格一樣好,並且熟悉了就會感到舒適。

在使用結構體指標時,您使用 . 而不是 ->。因此,從語法上講,結構體和結構體指標的使用方式相同。

type myStruct struct{ i int }
var v9 myStruct  // v9 has structure type
var p9 *myStruct // p9 is a pointer to a structure
f(v9.i, p9.i)

Go 在 if 語句的條件、for 語句的表示式或 switch 語句的值周圍不需要括號。另一方面,它在 iffor 語句的正文中需要花括號。

if a < b { f() }             // Valid
if (a < b) { f() }           // Valid (condition is a parenthesized expression)
if (a < b) f()               // INVALID
for i = 0; i < 10; i++ {}    // Valid
for (i = 0; i < 10; i++) {}  // INVALID

Go 沒有 while 語句,也沒有 do/while 語句。for 語句可以與單個條件一起使用,這使其等同於 while 語句。完全省略條件則是一個無限迴圈。

Go 允許 breakcontinue 指定標籤。標籤必須引用 forswitchselect 語句。

switch 語句中,case 標籤不會向下穿透。您可以使用 fallthrough 關鍵字使其向下穿透。這甚至適用於相鄰的 case。

switch i {
case 0: // empty case body
case 1:
    f() // f is not called when i == 0!
}

但是一個 case 可以有多個值。

switch i {
case 0, 1:
    f() // f is called if i == 0 || i == 1.
}

case 中的值不一定是常量——甚至不是整數;可以使用任何支援相等比較運算子的型別,如字串或指標——如果省略 switch 值,則預設為 true

switch {
case i < 0:
    f1()
case i == 0:
    f2()
case i > 0:
    f3()
}

defer 語句可用於在包含 defer 語句的函式返回後呼叫一個函式。defer 通常可以替代 C++ 中的解構函式,但它與呼叫程式碼相關聯,而不是與任何特定的類或物件關聯。

fd := open("filename")
defer close(fd) // fd will be closed when this function returns.

運算子

++-- 運算子只能用在語句中,不能用在表示式中。不能寫 c = *p++*p++ 被解析為 (*p)++

運算子的優先順序不同。例如,4 & 3 << 1 在 Go 中求值為 0,在 C++ 中求值為 4

Go operator precedence:
1. *   /   %  <<  >>  &  &^
2. +   -   |  ^
3. ==  !=  <  <=  >   >=
4. &&
5. ||
C++ operator precedence (only relevant operators):
1.  *    /   %
2.  +    -
3.  <<   >>
4.  <    <=  >   >=
5.  ==   !=
6.  &
7.  ^
8.  |
9.  &&
10. ||

常量

在 Go 中,常量可以是*無型別的*。即使是使用 const 宣告命名的常量也是如此,如果宣告中沒有給出型別,並且初始化表示式只使用無型別常量。來自無型別常量的值在需要型別值的上下文中進行轉換時會變為有型別的。這允許常量相對自由地使用,而無需普遍的隱式型別轉換。

var a uint
f(a + 1) // untyped numeric constant "1" becomes typed as uint

語言不對無型別數值常量或常量表達式的大小施加任何限制。只有當常量用於需要型別的場合時,才會應用限制。

const huge = 1 << 100
f(huge >> 98)

Go 不支援列舉。相反,您可以在單個 const 宣告中使用特殊名稱 iota 來獲得一系列遞增的值。當省略 const 的初始化表示式時,它會重用前面的表示式。

const (
    red   = iota // red == 0
    blue         // blue == 1
    green        // green == 2
)

型別

C++ 和 Go 提供類似但又不完全相同的內建型別:各種位數的有符號和無符號整數、32 位和 64 位浮點數(實數和複數)、struct、指標等。在 Go 中,uint8int64 以及類似命名的整數型別是語言的一部分,而不是基於依賴於實現的整數(例如 long long)。Go 另外還提供原生的 stringmapchan(通道)型別,以及第一類陣列和切片(下文將介紹)。字串使用 Unicode 編碼,而不是 ASCII。

Go 的型別系統比 C++ 嚴格得多。特別是,Go 中沒有隱式型別轉換,只有顯式型別轉換。這提供了額外的安全性和避免了一類錯誤,但代價是需要更多的輸入。Go 中也沒有 union 型別,因為這會允許對型別系統進行規避。然而,Go 的 interface{}(見下文)提供了一種型別安全的選擇。

C++ 和 Go 都支援類型別名(C++ 中的 typedef,Go 中的 type)。但是,與 C++ 不同,Go 將它們視為不同的型別。因此,以下程式碼在 C++ 中是有效的

// C++
typedef double position;
typedef double velocity;

position pos = 218.0;
velocity vel = -9.8;

pos += vel;

但如果沒有顯式型別轉換,等效的程式碼在 Go 中是無效的

type position float64
type velocity float64

var pos position = 218.0
var vel velocity = -9.8

pos += vel // INVALID: mismatched types position and velocity
// pos += position(vel)  // Valid

即使對於未別名化的型別也是如此:intuint 不能在表示式中組合,而不先將其中一個顯式轉換為另一個。

與 C++ 不同,Go 不允許將指標轉換為整數或從整數轉換為指標。然而,Go 的 unsafe 包允許在必要時顯式繞過此安全機制(例如,用於低階系統程式碼)。

切片

切片在概念上是一個包含三個欄位的結構體:指向陣列的指標、長度和容量。切片支援 [] 運算子來訪問底層陣列的元素。內建的 len 函式返回切片的長度。內建的 cap 函式返回切片的容量。

給定一個數組或另一個切片,透過 a[i:j] 建立一個新切片。這會建立一個引用 a 的新切片,從索引 i 開始,到索引 j 之前結束。其長度為 j-i。如果省略 i,則切片從 0 開始。如果省略 j,則切片以 len(a) 結束。新切片引用 a 引用的同一個陣列。此陳述的兩個含義是:① 使用新切片所做的更改可以透過 a 看到,② 切片建立是(意圖上)廉價的;無需複製底層陣列。新切片的容量是 a 的容量減去 i。陣列的容量是陣列的長度。

這意味著 Go 在 C++ 使用指標的某些情況下會使用切片。如果您建立了一個 [100]byte 型別的值(一個包含 100 個位元組的陣列,可能是一個緩衝區),並且想在不復制它的情況下將其傳遞給函式,您應該將函式引數宣告為 []byte 型別,並傳遞陣列的切片(a[:] 將傳遞整個陣列)。與 C++ 不同,無需傳遞緩衝區的長度;可以透過 len 高效地訪問它。

切片語法也可以用於字串。它返回一個新字串,其值是原始字串的子字串。因為字串是不可變的,所以字串切片可以在不分配新儲存空間來儲存切片內容的情況下實現。

值建立

Go 有一個內建函式 new,它接受一個型別並分配堆上的空間。分配的空間將根據該型別進行零初始化。例如,new(int) 在堆上分配一個新的 int,將其初始化為值 0,並返回其地址,該地址的型別為 *int。與 C++ 不同,new 是一個函式,而不是一個運算子;new int 是一個語法錯誤。

可能令人驚訝的是,new 在 Go 程式中並不常用。在 Go 中,取變數的地址始終是安全的,並且永遠不會產生懸空指標。如果程式獲取了變數的地址,它將在必要時在堆上分配。所以這些函式是等效的

type S struct { I int }

func f1() *S {
    return new(S)
}

func f2() *S {
    var s S
    return &s
}

func f3() *S {
    // More idiomatic: use composite literal syntax.
    return &S{}
}

相反,在 C++ 中返回指向區域性變數的指標是不安全的

// C++
S* f2() {
  S s;
  return &s;   // INVALID -- contents can be overwritten at any time
}

對映和通道值必須使用內建函式 make 分配。一個宣告為對映或通道型別但沒有初始化的變數將被自動初始化為 nil。呼叫 make(map[int]int) 返回一個新分配的 map[int]int 型別的值。請注意,make 返回一個值,而不是指標。這與對映和通道值按引用傳遞的事實一致。呼叫 make 並傳入對映型別時,可以有一個可選引數,即對映的預期容量。呼叫 make 並傳入通道型別時,可以有一個可選引數來設定通道的緩衝容量;預設值為 0(無緩衝)。

make 函式也可用於分配切片。在這種情況下,它會為底層陣列分配記憶體並返回一個引用它的切片。有一個必需的引數,即切片中的元素數量。第二個可選引數是切片的容量。例如,make([]int, 10, 20)。這與 new([20]int)[0:10] 相同。由於 Go 使用垃圾回收,新分配的陣列將在不再有對返回切片的引用後被丟棄。

介面

在 C++ 提供類、子類和模板的地方,Go 提供了介面。Go 介面類似於 C++ 的純抽象類:一個沒有資料成員的類,其所有方法都是純虛擬函式。然而,在 Go 中,任何提供介面中命名的方法的型別都可以被視為介面的實現。不需要顯式宣告繼承。介面的實現與介面本身完全分離。

方法看起來像一個普通函式定義,只是它有一個*接收者*。接收者類似於 C++ 類方法中的 this 指標。

type myType struct{ i int }

func (p *myType) Get() int { return p.i }

這會宣告一個與 myType 關聯的 Get 方法。在函式體內,接收者被命名為 p

方法定義在命名型別上。如果您將值轉換為另一種型別,新值將擁有新型別的*方法*,而不是舊型別的。

您可以透過宣告一個派生自內建型別的新命名型別來為內建型別定義方法。新型別與內建型別是不同的。

type myInteger int

func (p myInteger) Get() int { return int(p) } // Conversion required.
func f(i int)                {}

var v myInteger

// f(v) is invalid.
// f(int(v)) is valid; int(v) has no defined methods.

給定這個介面

type myInterface interface {
    Get() int
    Set(i int)
}

透過新增以下內容,我們可以讓 myType 實現該介面

func (p *myType) Set(i int) { p.i = i }

現在,任何將 myInterface 作為引數的函式都將接受 *myType 型別的變數。

func GetAndSet(x myInterface) {}
func f1() {
    var p myType
    GetAndSet(&p)
}

換句話說,如果我們把 myInterface 看作一個 C++ 純抽象基類,那麼為 *myType 定義 SetGet 會讓 *myType 自動繼承自 myInterface。一個型別可以實現多個介面。

匿名欄位可用於實現非常類似於 C++ 子類的方法。

type myChildType struct {
    myType
    j int
}

func (p *myChildType) Get() int { p.j++; return p.myType.Get() }

這有效地實現了 myChildType 作為 myType 的子類。

func f2() {
    var p myChildType
    GetAndSet(&p)
}

Set 方法實際上是從 myType 繼承的,因為與匿名欄位關聯的方法會被提升為包圍型別的方法。在這種情況下,因為 myChildType 有一個型別為 myType 的匿名欄位,所以 myType 的方法也成為 myChildType 的方法。在此示例中,Get 方法被重寫,而 Set 方法被繼承。

這與 C++ 中的子類不完全相同。當呼叫匿名欄位的方法時,其接收者是欄位,而不是外層結構體。換句話說,匿名欄位上的方法不是虛擬函式。當您想要等同於虛擬函式時,請使用介面。

具有介面型別的變數可以使用一種稱為型別斷言的特殊構造轉換為具有不同介面型別。這在執行時動態實現,類似於 C++ 的 dynamic_cast。與 dynamic_cast 不同,這兩個介面之間不需要有任何宣告的關係。

type myPrintInterface interface {
    Print()
}

func f3(x myInterface) {
    x.(myPrintInterface).Print() // type assertion to myPrintInterface
}

轉換為 myPrintInterface 是完全動態的。只要 x 的動態型別定義了 Print 方法,轉換就會成功。

由於轉換是動態的,因此可以用於實現類似於 C++ 模板的泛型程式設計。這是透過操作 Any 的值來實現的。透過操作 Any 的值來實現這一點。

type Any interface{}

容器可以用 Any 來編寫,但呼叫者必須透過型別斷言來取消裝箱以恢復包含型別的值。由於型別是動態的而不是靜態的,因此沒有等同於 C++ 模板可以內聯相關操作的方式。這些操作在執行時進行了完全的型別檢查,但所有操作都將涉及函式呼叫。

type Iterator interface {
    Get() Any
    Set(v Any)
    Increment()
    Equal(arg Iterator) bool
}

請注意,Equal 的引數型別是 Iterator。這不像 C++ 模板。請參閱FAQ

函式閉包

在 C++11 之前的版本中,建立具有隱藏狀態的函式的常見方法是使用“仿函式”——一個過載 operator() 的類,使其例項看起來像函式。例如,以下程式碼定義了一個 my_transform 函式(STL 的 std::transform 的簡化版本),該函式將給定的一元運算子(op)應用於陣列(in)的每個元素,並將結果儲存在另一個數組(out)中。為了實現字首和(即 {x[0], x[0]+x[1], x[0]+x[1]+x[2], ...}),程式碼會建立一個仿函式(MyFunctor),該仿函式跟蹤執行總計(total)並將該仿函式的例項傳遞給 my_transform

// C++
#include <iostream>
#include <cstddef>

template <class UnaryOperator>
void my_transform (size_t n_elts, int* in, int* out, UnaryOperator op)
{
  size_t i;

  for (i = 0; i < n_elts; i++)
    out[i] = op(in[i]);
}

class MyFunctor {
public:
  int total;
  int operator()(int v) {
    total += v;
    return total;
  }
  MyFunctor() : total(0) {}
};

int main (void)
{
  int data[7] = {8, 6, 7, 5, 3, 0, 9};
  int result[7];
  MyFunctor accumulate;
  my_transform(7, data, result, accumulate);

  std::cout << "Result is [ ";
  for (size_t i = 0; i < 7; i++)
    std::cout << result[i] << ' ';
  std::cout << "]\n";
  return 0;
}

C++11 添加了匿名(“lambda”)函式,這些函式可以儲存在變數中並傳遞給函式。它們可以充當閉包,這意味著它們可以引用父作用域中的狀態。此功能極大地簡化了 my_transform

// C++11
#include <iostream>
#include <cstddef>
#include <functional>

void my_transform (size_t n_elts, int* in, int* out, std::function<int(int)> op)
{
  size_t i;

  for (i = 0; i < n_elts; i++)
    out[i] = op(in[i]);
}

int main (void)
{
  int data[7] = {8, 6, 7, 5, 3, 0, 9};
  int result[7];
  int total = 0;
  my_transform(7, data, result, [&total] (int v) {
      total += v;
      return total;
    });

  std::cout << "Result is [ ";
  for (size_t i = 0; i < 7; i++)
    std::cout << result[i] << ' ';
  std::cout << "]\n";
  return 0;
}

典型的 Go 版本 my_transform 在很多方面都類似於 C++11 版本。

package main

import "fmt"

func my_transform(in []int, xform func(int) int) (out []int) {
    out = make([]int, len(in))
    for idx, val := range in {
        out[idx] = xform(val)
    }
    return
}

func main() {
    data := []int{8, 6, 7, 5, 3, 0, 9}
    total := 0
    fmt.Printf("Result is %v\n", my_transform(data, func(v int) int {
        total += v
        return total
    }))
}

(請注意,我們選擇從 my_transform 返回 out,而不是將其傳遞一個 out 來寫入。這是一個美學決定;在這方面,程式碼本可以寫得更像 C++ 版本。)

在 Go 中,函式始終是完整的閉包,等同於 C++11 中的 [&]。一個重要的區別是,在 C++11 中,閉包引用作用域已消失的變數(可能由向上 funarg 引起——返回一個引用區域性變數的 lambda 的函式)是無效的。在 Go 中,這是完全有效的。

併發

與 C++11 的 std::thread 類似,Go 允許啟動在共享地址空間中併發執行的新執行執行緒。這些稱為*goroutine*,使用 go 語句生成。雖然典型的 std::thread 實現啟動重量級的作業系統執行緒,但 goroutine 實現為輕量級的使用者級執行緒,這些執行緒被多路複用到多個作業系統執行緒上。因此,goroutine 是(意圖上)廉價的,並且可以在程式中廣泛使用。

func server(i int) {
    for {
        fmt.Print(i)
        time.Sleep(10 * time.Second)
    }
}
go server(1)
go server(2)

(請注意,server 函式中的 for 語句等同於 C++ 的 while (true) 迴圈。)

函式字面量(Go 將其實現為閉包)可以與 go 語句一起使用。

var g int
go func(i int) {
    s := 0
    for j := 0; j < i; j++ {
        s += j
    }
    g = s
}(1000) // Passes argument 1000 to the function literal.

與 C++11 類似,但與 C++ 早期版本不同,Go 為未同步的記憶體訪問定義了一個記憶體模型。儘管 Go 在其 sync 包中提供了 std::mutex 的類似物,但這並不是 Go 程式中實現執行緒間通訊和同步的常規方式。相反,Go 執行緒更通常地透過訊息傳遞進行通訊,這是一種與鎖和屏障根本不同的方法。關於此主題的 Go 箴言是:

不要透過共享記憶體來通訊;相反,透過通訊來共享記憶體。

也就是說,*通道*用於 goroutine 之間的通訊。任何型別的值(包括其他通道!)都可以透過通道傳送。通道可以是無緩衝的,也可以是緩衝的(使用在通道建立時指定的緩衝區長度)。

通道是第一類值;它們可以像其他任何值一樣儲存在變數中,並像其他任何值一樣傳遞給函式和從函式返回。(當提供給函式時,通道是按引用傳遞的。)通道也是有型別的:chan intchan string 不同。

由於通道在 Go 程式中被廣泛使用,因此它們是(意圖上)高效且廉價的。要透過通道傳送值,請使用 <- 作為二元運算子。要從通道接收值,請使用 <- 作為一元運算子。通道可以在多個傳送者和多個接收者之間共享,並保證每個傳送的值最多被一個接收者接收。

這是一個使用管理器函式控制對單個值的訪問的示例。

type Cmd struct {
    Get bool
    Val int
}

func Manager(ch chan Cmd) {
    val := 0
    for {
        c := <-ch
        if c.Get {
            c.Val = val
            ch <- c
        } else {
            val = c.Val
        }
    }
}

在該示例中,同一個通道用於輸入和輸出。如果同時有多個 goroutine 與管理器通訊,這會是錯誤的:等待管理器響應的 goroutine 可能會收到來自另一個 goroutine 的請求。解決方案是傳遞一個通道。

type Cmd2 struct {
    Get bool
    Val int
    Ch  chan<- int
}

func Manager2(ch <-chan Cmd2) {
    val := 0
    for {
        c := <-ch
        if c.Get {
            c.Ch <- val
        } else {
            val = c.Val
        }
    }
}

要使用 Manager2,請給定一個通道給它

func getFromManagedChannel(ch chan<- Cmd2) int {
    myCh := make(chan int)
    c := Cmd2{true, 0, myCh} // Composite literal syntax.
    ch <- c
    return <-myCh
}

func main() {
    ch := make(chan Cmd2)
    go Manager2(ch)
    // ... some code ...
    currentValue := getFromManagedChannel(ch)
    // ... some more code...
}

此內容是 Go Wiki 的一部分。