前言

在撰寫 Go 程式的過程中,有時候我們會遇到這樣的情境:某些功能已經有穩定的 C 函式庫可以使用,若能直接呼叫這些現有資源,就能避免重複造輪子。
Go 語言本身提供了 cgo 這個工具,讓我們能順利地在 Go 程式中嵌入並呼叫 C 程式碼。

本文會帶你從基礎開始認識 cgo,並透過範例說明如何在 Go 專案中使用它。

cgo 是什麼?

cgo 是 Go 語言官方提供的工具,用來讓 Go 程式可以呼叫 C 語言的程式碼或函式庫。只要在 Go 程式碼中使用特殊的 import "C" 語法,就能在 Go 與 C 之間進行互操作。

基本使用方式

最基本的 cgo 程式碼結構如下:

 1package main
 2
 3/*
 4#include <stdio.h>
 5#include <stdlib.h>
 6*/
 7import "C"
 8import "fmt"
 9
10func main() {
11    C.puts(C.CString("Hello from C!"))
12    fmt.Println("Hello from Go!")
13}

在這段程式中:

  • /* ... */ 區塊中的程式碼會被視為 C 語言程式碼。
  • import "C" 表示我們要使用 cgo
  • C.puts 即呼叫了 C 標準函式庫的 puts

執行結果會依序輸出:

1Hello from C!
2Hello from Go!

C 型別的取用

在使用 cgo 時,Go 與 C 型別必須正確對應。常見的幾個範例如下:

1var a C.int = 10      // C 的 int 型別
2var b C.char = 'A'    // C 的 char 型別
3var c C.size_t = 100  // C 的 size_t 型別

Go 與 C 的字串轉換

C 使用 char* 表示字串,而 Go 使用 string。兩者需要透過轉換:

1cs := C.CString("hello")       // Go string -> C string
2C.free(unsafe.Pointer(cs))      // 使用完畢後必須釋放記憶體
3
4goStr := C.GoString(cs)         // C string -> Go string
5goStrN := C.GoStringN(cs, 5)    // 指定長度的轉換

當 Go 傳 Go pointer 給 C 時: Go 會自動把該區域的記憶體「pin(釘住)」,直到呼叫結束,避免 GC 將內容搬移。
如果 C 要留存 Go pointer,必須透過 runtime.Pinner,或使用 runtime/cgo.Handle 進行安全管理。
另外也有 GODEBUG=cgocheck=1(預設)來做動態檢查,若要關閉可以 GODEBUG=cgocheck=0,而更嚴格檢查則可開 GOEXPERIMENT=cgocheck2

Go 與 C 型別對應表

下表列出常見的 Go 與 C 型別對應:

C 型別Go 對應型別說明
charC.char單一字元
signed charC.schar有號字元
unsigned charC.uchar無號字元
shortC.short短整數
unsigned shortC.ushort無號短整數
intC.int整數
unsigned intC.uint無號整數
longC.long長整數
unsigned longC.ulong無號長整數
long longC.longlong更長的整數
unsigned long longC.ulonglong無號更長的整數
floatC.float單精度浮點
doubleC.double雙精度浮點
size_tC.size_t記憶體大小或索引
void*unsafe.Pointer通用指標

這張表對於撰寫 Cgo 程式相當實用,可以幫助你快速找到對應的型別。

定義編譯器旗標(CFLAGS、LDFLAGS)

在 preamble 中可以用 #cgo 指令指定額外的編譯或連結參數:

1// #cgo CFLAGS: -DPNG_DEBUG=1
2// #cgo LDFLAGS: -lpng
3// #include <png.h>
4import "C"

若透過 pkg-config 取得設定,也支援這種寫法:

1// #cgo pkg-config: png cairo

這些旗標會依系統條件進行串接、合併使用,非常方便

境變數與安全限制技巧

建議盡量把特定包的 CFLAGS/LDFLAGS 用 #cgo 指令設定,而不要透過環境變數設定,因為後者不會套受 cgo 的安全機制限制:

cgo 對於允許的旗標有嚴格限制(例如 -D, -I, -l 等);

若需要放寬限制,可以透過環境變數如 CGO_CFLAGS_ALLOW、CGO_CFLAGS_DISALLOW 設定正則表達式 Go Packages 。

常見錯誤與除錯技巧

在使用 cgo 的過程中,常見的錯誤包括:

  1. 找不到標頭檔

    1#include <foo.h>
    2fatal error: foo.h: No such file or directory
    

    請確認系統已安裝相關開發套件,例如在 Debian/Ubuntu 中:

    1sudo apt-get install libfoo-dev
    
  2. 記憶體釋放問題

    • 使用 C.CString 產生的字串必須手動 C.free,否則會造成 memory leak。
  3. 型別不相容

    • 若將 C.int 直接指定給 Go 的 int,有時會出現編譯錯誤,建議使用明確轉型:

      1var g int = int(C.int(10))
      
  4. 交叉編譯失敗

    • 由於 cgo 依賴 C 編譯器,若要交叉編譯(例如在 Linux 編譯給 Windows),需要安裝對應平台的 cross-compiler。

建議在開發初期就善用 go build -xCGO_ENABLED=0 測試,這樣能更容易找到問題所在。

小結

透過 cgo,我們可以將現有的 C 函式庫整合進 Go 專案,兼顧效能與開發效率。本文介紹了基本語法、C 型別取用方式、Go 與 C 型別對應表,以及常見錯誤的解決方式。未來若需要整合 C 函式庫,不妨動手試試,體驗 Go 與 C 的強大組合。