天天看點

CGO入門和OCR文字識别(非第三方API,有源碼,效果好)實戰CGO入門和OCR文字識别(非第三方API,有源碼,效果好)實戰

文章目錄

  • CGO入門和OCR文字識别(非第三方API,有源碼,效果好)實戰
    • CGO是什麼
      • 極簡入門
      • CGO基礎類型
      • 字元串、數組和函數調用
    • CGO實戰
      • 分離Go和C代碼
      • 常用cgo編譯指令
      • 調用C靜态庫和動态庫
        • 靜态庫
        • 動态庫
      • 調用C++動态庫
      • CGO的缺陷
      • CGO最佳使用場景總結
    • CGO案例:在Go中調用動态庫實作OCR文字識别
      • chineseocr_lite介紹
      • 識别效果
      • 編譯chineseocr_lite
      • 導出c函數
      • 環境變量設定
      • 運作
    • 參考

CGO入門和OCR文字識别(非第三方API,有源碼,效果好)實戰

CGO是什麼

簡單點來講,如果要調用C++,C寫的庫(動态庫,靜态庫),那麼就需要使用Cgo。其他情況下一般用不到,隻需要知道Go能調用C就行了,當然C也可以回調到Go中。

使用Cgo有2種姿勢:

  1. 直接在go中寫c代碼
  2. go調用so動态庫(c++要用extern “c”導出)

為了熟悉CGO,我們先介紹第一種方法,直接在Go中寫C代碼。

極簡入門

引用:Command cgo

首先,

通過import “C”導入僞包

(這個包并不真實存在,也不會被Go的compile元件見到,它會在編譯前被CGO工具捕捉到,并做一些代碼的改寫和樁檔案的生成)

然後,Go 就可以使用C的變量和函數了, C.size_t 之類的類型、諸如 C.stdout 之類的變量或諸如 C.putchar 之類的函數。

func main(){
    cInt := C.int(1)     // 使用C中的int類型
    fmt.Println(goInt)

    ptr := C.malloc(20)  // 調用C中的函數
    fmt.Println(ptr)     // 列印指針位址
    C.free(ptr)          // 釋放,需要 #include <stdlib.h>
}
           

如果“C”的導入緊跟在注釋之前,則

該注釋稱為序言

。例如:

// #include <stdio.h> 
/* #include <errno.h> */
import "C"
           

序言可以包含任何 C 代碼,包括函數和變量聲明和定義

。然後可以從 Go 代碼中引用它們,就好像它們是在包“C”中定義的一樣。可以使用序言中聲明的所有名稱,即使它們以小寫字母開頭。例外:序言中的靜态變量不能從 Go 代碼中引用;靜态函數是允許的。

是以,你可以直接在裡面寫C代碼(注意,C++不行!):

package main

/*
int add(int a,int b){
   return a+b;
}
*/
import "C"
import "fmt"

func main() {
    a, b := 1, 2
    c := C.add(a, b)
}
           

編譯下,會出現下面的問題( fmt.Println(C.add(1, 2)) 能編譯通過,思考下為什麼? ):

./main.go:20:12: cannot use a (type int) as type _Ctype_int in argument to _Cfunc_add
./main.go:20:12: cannot use b (type int) as type _Ctype_int in argument to _Cfunc_add
           

為什麼呢?因為C沒有辦法使用Go的類型,必須先轉換成CGO類型才可以,改成這樣就行了:

func main() {
   cgoIntA, cgoIntB := C.int(1), C.int(2)

   c := C.add(cgoIntA, cgoIntB)
   fmt.Println(c)
}
           

運作後輸出:

CGO基礎類型

就像上面的代碼一樣,

Go沒有辦法直接使用C的東西,必須先轉換成CGO類型

,下面是一個基礎類型對應表。

C類型 CGO類型 GO類型
char C.char byte
signed char C.schar int8
unsigned char C.uchar uint8
short C.short int16
unsigned short C.ushort uint16
int C.int int32
unsigned int C.uint uint32
long C.long int32
unsigned long C.ulong uint32
long long int C.longlong int64
unsigned long long int C.ulonglong uint64
float C.float float32
double C.double float64
size_t C.size_t uint

如果直接在C中#include <stdint.h>,則類型關系就比較一緻了,例如:

C類型 CGO類型 GO類型
int8_t C.int8_t int8
int16_t C.int16_t int16
uint32_t C.uint32_t uint32
uint64_t C.uint64_t uint64

字元串、數組和函數調用

那麼,在Go要如何傳遞字元串、位元組數組以及指針?

CGO的C虛拟包提供了以下一組函數,用于Go語言和C語言之間數組和字元串的雙向轉換:

// Go string to C string
// The C string is allocated in the C heap using malloc.
// It is the caller's responsibility to arrange for it to be
// freed, such as by calling C.free (be sure to include stdlib.h
// if C.free is needed).
func C.CString(string) *C.char

// Go []byte slice to C array
// The C array is allocated in the C heap using malloc.
// It is the caller's responsibility to arrange for it to be
// freed, such as by calling C.free (be sure to include stdlib.h
// if C.free is needed).
func C.CBytes([]byte) unsafe.Pointer

// C string to Go string
func C.GoString(*C.char) string

// C data with explicit length to Go string
func C.GoStringN(*C.char, C.int) string

// C data with explicit length to Go []byte
func C.GoBytes(unsafe.Pointer, C.int) []byte
           

字元串,可以通過C.CString()函數(

别忘記通過free釋放

):

// 通過C.CString,這裡會發生記憶體拷貝,cgo通過malloc重新開辟了一塊空間,使用完需要釋放,否則記憶體洩露
imagePath := C.CString("a.png")
defer C.free(unsafe.Pointer(imagePath))
           

位元組數組,直接使用go的數組,然後強制轉換即可:

// 隻能使用數組,無法使用切片用作緩沖區給C使用
var buffer [20]byte
// &buffer[0]: 數組在記憶體中是連續存儲的,取首位址
// unsafe.Pointer():轉換為非安全指針,類型是*unsafe.Pointer
// (*C.char)():再強轉一次
cBuffer := (*C.char)(unsafe.Pointer(&buffer[0]))

對應類型的指針,直接使用Cgo類型,然後&取位址即可:
bufferLen := C.int(20)
cPoint := &bufferLen      // cPoint在CGO中是*C.int類型,在C中是*int類型。
           

假如ocr識别函數如下:

有3個參數:

  • image_path:訓示了要識别的圖檔路徑。
  • out_buffer:識别到的文字輸出到這裡,是一個char位元組數組。
  • len:訓示輸出位元組緩沖區大小,調用成功後,值變成字元串長度,便于外界讀取。

在go中調用方式如下:

imagePath := C.CString("a.png")
defer C.free(unsafe.Pointer(imagePath))

var buffer [20]byte
bufferLen := C.int(20)
cInt := C.detect(imagePath, (*C.char)(unsafe.Pointer(&buffer[0])), &bufferLen)
if cInt == 0 {
   fmt.Println(string(buffer[0:bufferLen]))
}
           

CGO實戰

分離Go和C代碼

為了簡化代碼,我們可以把C的代碼放到xxx.h和xxx.c中實作。

有以下結構:

├── hello.c
├── hello.h
└── main.go
           

hello.h的内容:

#include <stdio.h>

void sayHello(const char* text);
           

hello.c:

#include "hello.h"

void sayHello(const char* text){
    printf("%s", text);
}
           

main.go中調用hello.h中的函數:

#include "hello.h"
import "C" // 必須放在導入c代碼活頭檔案的注釋後面,否則不生效

func main() {
    cStr := C.CString("hello from go")
    defer C.free(unsafe.Pointer(cStr))
    C.sayHello(cStr)
}
           

常用cgo編譯指令

如果我們把h和c檔案放到其他目錄,則編譯會報錯:

├── main.go
└── mylib
    ├── hello.c
    └── hello.h
           
Undefined symbols for architecture x86_64:
  "_sayHello", referenced from:
      __cgo_7ab15a91ce47_Cfunc_sayHello in _x002.o
     (maybe you meant: __cgo_7ab15a91ce47_Cfunc_sayHello)
ld: symbol(s) not found for architecture x86_64
clang: error: linker command failed with exit code 1 (use -v to see invocation)
           

這裡應該可以使用

#cgo預編譯解決

(CFLAGS、CPPFLAGS、CXXFLAGS、FFLAGS和LDFLAGS) :

// #cgo CFLAGS: -DPNG_DEBUG=1 -I ./include
// #cgo LDFLAGS: -L /usr/local/lib -lpng
// #include <png.h>
import "C"
           
  • CFLAGS:-D部分定義了宏PNG_DEBUG,值為1。-I定義了頭檔案包含的檢索目錄
  • LDFLAGS:-L指定了連結時庫檔案檢索目錄,-l指定了連結時需要連結png庫

通常實際的工作中遇到要使用cgo的場景,都是調用動态庫的方式,是以這裡沒有繼續往下深究上面的錯誤如何解決了。

調用C靜态庫和動态庫

目錄結構如下:

├── call_shared_lib
│   └── main.go
├── call_static_lib
│   └── main.go
└── mylib
    ├── hello.c
    ├── hello.h
    ├── libhello.a
    └── libhello.so
           

靜态庫

把上面的hello.h 和 hello.c 生成為靜态庫(需要安裝gcc,省略):

# 生成o對象
$ gcc -c hello.c
# 生成靜态庫
$ ar crv libhello.a hello.o

# 檢視裡面包含的内容
# ar -t libhello.a
# 使用靜态庫
#gcc main.c libhello.a -o main
           

在go中調用:

package main

/*
#cgo CFLAGS: -I ../mylib
#cgo LDFLAGS: -L ../mylib -lhello
#include <stdlib.h>
#include "hello.h"
 */
import "C"
import "unsafe"

func main() {
   cStr := C.CString("hello from go")
   defer C.free(unsafe.Pointer(cStr))

   C.sayHello(cStr)
}
           
  • CFLAGS:C的編譯參數,-I訓示 include路徑
  • LDFLAGS: 連結參數,-L 訓示搜尋靜态庫的路徑,-lhello表示連結libhello.a,記住lib這裡不用寫,編譯器會自己補全。

動态庫

生成

# 生成o對象
$ gcc -fPIC -c hello.c
# 生成動态庫
$ gcc -shared -fPIC -o libhello.so hello.o

# 使用動态庫
#gcc main.c -L. -lhello -o main
           

Go中調用和靜态庫一樣:

package main


// 注意,生成的so檔案一定的得增加lib字首,如libhello.so
// 然後在Go中隻需要寫-lhello(代表libhello.a或libhello.so)即可
// linux下會自動增加lib字首。

/*
#cgo CFLAGS: -I ../mylib
#cgo LDFLAGS: -L ../mylib -lhello
#include <stdlib.h>
#include "hello.h"
 */
import "C"
import "unsafe"

func main() {
   cStr := C.CString("hello from go")
   defer C.free(unsafe.Pointer(cStr))

   C.sayHello(cStr)
}
           

唯一不同的是,靜态庫需要指定so檔案的搜尋路徑或者

把so動态庫拷貝到/usr/lib

下,在環境變量中配置:

$ export LD_LIBRARY_PATH=../mylib
$ go run main.go

# 也可以在goland中在Run -> Edit Configurations -> Environment 
# 配置 LD_LIBRARY_PATH=../mylib ,友善調試
           

更多關于靜态庫和動态庫的差別:https://segmentfault.com/a/1190000020651578

調用C++動态庫

本質上和調用c動态庫在Go的寫法上是一樣的,隻是需要導出成C風格的即可:

#ifdef __cplusplus
extern "C"
{
#else

// 導出C 命名風格函數,函數名字和定義的一樣,C++因為支援重載,是以導出的函數名被編譯器改變了

#ifdef __cplusplus
}
#endif
           

然後在go的LDFLAGS中增加 -lstdc++:

CGO的缺陷

cgo is not Go中總結了cgo 的缺點:

  1. 編譯變慢,實際會使用 c 語言編譯工具,還要處理 c 語言的跨平台問題
  2. 編譯變得複雜
  3. 不支援交叉編譯
  4. 其他很多 go 語言的工具不能使用
  5. C 與 Go 語言之間的的互相調用繁瑣,是會有

    性能開銷

  6. C 語言是主導,這時候 go 變得不重要,其實和你用 python 調用 c 一樣
  7. 部署複雜,不再隻是一個簡單的二進制

這篇文章描述了CGO通過go去調用C性能開銷大的原因:https://blog.csdn.net/u010853261/article/details/108186405

  • 必須切換go的協程棧到系統線程的主棧去執行C函數
  • 涉及到系統調用以及協程的排程。
  • 由于需要同時保留C/C++的運作時,CGO需要在兩個運作時和兩個ABI(抽象二進制接口)之間做翻譯和協調。這就帶來了很大的開銷。

《GO原本》中進一步通過runtime源碼解讀了原因。

是以,使用的時候,自己靈活根據場景取舍吧。

CGO最佳使用場景總結

CGO的一些缺點:

  1. 記憶體隔離
  2. C函數執行切換到g0(系統線程)
  3. 收到GOMAXPROC線程限制
  4. CGO空調用的性能損耗(50+ns)
  5. 編譯損耗(CGO其實是有個中間層)

CGO 适合的場景:

  1. C 函數是個大計算任務(不在乎CGO調用性能損耗)

  2. C 函數調用不頻繁

  3. C 函數中不存在阻塞IO
  4. C 函數中不存在建立線程(與go裡面協程排程由潛在可能互相影響)
  5. 不在乎編譯以及部署的複雜性

更多可以閱讀:

  • 【Free Style】CGO: Go與C互操作技術(一):Go調C基本原理
  • 【Free Style】CGO: Go與C互操作技術(三):Go調C性能分析及優化
  • Go語言使用cgo時的記憶體管理筆記

CGO案例:在Go中調用動态庫實作OCR文字識别

chineseocr_lite介紹

GitHub: https://github.com/DayBreak-u/chineseocr_lite

Star: 7.1 k

介紹:超輕量級中文ocr,支援豎排文字識别, 支援ncnn、mnn、tnn推理 ( dbnet(1.8M) + crnn(2.5M) + anglenet(378KB)) 總模型僅4.7M。

這個開源項目提供了C++、JVM、Android、.Net等實作,隻依賴OpenCV和微軟的Onnx推理架構,

有源碼,經作者實踐,識别效果中等,越小的圖檔越快

識别效果

比如識别一個發票号碼,隻需要50ms左右:

CGO入門和OCR文字識别(非第三方API,有源碼,效果好)實戰CGO入門和OCR文字識别(非第三方API,有源碼,效果好)實戰

複雜的圖檔識别大概500-900ms左右:

CGO入門和OCR文字識别(非第三方API,有源碼,效果好)實戰CGO入門和OCR文字識别(非第三方API,有源碼,效果好)實戰

表格識别效果一般

CGO入門和OCR文字識别(非第三方API,有源碼,效果好)實戰CGO入門和OCR文字識别(非第三方API,有源碼,效果好)實戰

是以,适合格式一緻的識别場景。比如

發票的某個位置,身份證,銀行卡

等等。

編譯chineseocr_lite

按照 chineseocr_lite/cpp_projects/OcrLiteOnnx 中的README.md文檔編譯即可,推薦在Linux下,我再windows和Macos沒編譯通過。

然後需要改造成動态庫,我改動的内容有:

  • 預設生成動态庫,給ocr_http_server使用
  • 去掉jni的支援
  • 增加ocr.h,導出c風格函數

導出c函數

ocr.h

/** @file ocr.h
  * @brief  封裝給GO調用
  * @author teng.qing
  * @date 8/13/21
  */
#ifndef ONLINE_BASE_OCRLITEONNX_OCR_H
#define ONLINE_BASE_OCRLITEONNX_OCR_H

#ifdef __cplusplus
extern "C"
{
#else
    // c
    typedef enum{
        false, true
    }bool;
#endif

const int kOcrError = 0;
const int kOcrSuccess = 1;
const int kDefaultPadding = 50;
const int kDefaultMaxSideLen = 1024;
const float kDefaultBoxScoreThresh = 0.6f;
const float kDefaultBoxThresh = 0.3f;
const float kDefaultUnClipRatio = 2.0f;
const bool kDefaultDoAngle = true;
const bool kDefaultMostAngle = true;

/**@fn ocr_init
  *@brief 初始化OCR
  *@param numThread: 線程數量,不超過CPU數量
  *@param dbNetPath: dbnet模型路徑
  *@param anglePath: 角度識别模型路徑
  *@param crnnPath: crnn推理模型路徑
  *@param keyPath: keys.txt樣本路徑
  *@return <0: error, >0: instance
  */
int ocr_init(int numThread, const char *dbNetPath, const char *anglePath, const char *crnnPath, const char *keyPath);

/**@fn ocr_cleanup
  *@brief 清理,退出程式前執行
  */
void ocr_cleanup();

/**@fn ocr_detect
  *@brief 識别圖檔
  *@param image_path: 圖檔完整路徑,會在同路徑下生成圖檔識别框選效果,便于調試
  *@param out_json_result: 識别結果輸出,json格式。
  *@param buffer_len: 輸出緩沖區大小
  *@param padding: 50
  *@param maxSideLen: 1024
  *@param boxScoreThresh: 0.6f
  *@param boxThresh: 0.3f
  *@param unClipRatio: 2.0f
  *@param doAngle: true
  *@param mostAngle: true
  *@return 成功與否
  */
int ocr_detect(const char *image_path, char *out_buffer, int *buffer_len, int padding, int maxSideLen,
                float boxScoreThresh, float boxThresh, float unClipRatio, bool doAngle, bool mostAngle);
                
/**@fn ocr_detect
  *@brief 使用預設參數,識别圖檔
  *@param image_path: 圖檔完整路徑
  *@param out_buffer: 識别結果,json格式。
  *@param buffer_len: 輸出緩沖區大小
  *@return 成功與否
  */
int ocr_detect2(const char *image_path, char *out_buffer, int *buffer_len);

#ifdef __cplusplus
}
#endif

#endif //ONLINE_BASE_OCRLITEONNX_OCR_H
           

ocr.cpp

/** @file ocr.h
  * @brief 
  * @author teng.qing
  * @date 8/13/21
  */
#include "ocr.h"
#include "OcrLite.h"
#include "omp.h"
#include "json.hpp"
#include <iostream>
#include <sys/stat.h>

using json = nlohmann::json;

static OcrLite *g_ocrLite = nullptr;

inline bool isFileExists(const char *name) {
    struct stat buffer{};
    return (stat(name, &buffer) == 0);
}

int ocr_init(int numThread, const char *dbNetPath, const char *anglePath, const char *crnnPath, const char *keyPath) {
    omp_set_num_threads(numThread); // 并行計算
    if (g_ocrLite == nullptr) {
        g_ocrLite = new OcrLite();
    }
    g_ocrLite->setNumThread(numThread);
    g_ocrLite->initLogger(
            true,//isOutputConsole
            false,//isOutputPartImg
            true);//isOutputResultImg
    g_ocrLite->Logger(
            "ocr_init numThread=%d, dbNetPath=%s,anglePath=%s,crnnPath=%s,keyPath=%s \n",
            numThread, dbNetPath, anglePath, crnnPath, keyPath);
    if (!isFileExists(dbNetPath) || !isFileExists(anglePath) || !isFileExists(crnnPath) || !isFileExists(keyPath)) {
        g_ocrLite->Logger("invalid file path.\n");
        return kOcrError;
    }
    g_ocrLite->initModels(dbNetPath, anglePath, crnnPath, keyPath);
    return kOcrSuccess;
}

void ocr_cleanup() {
    if (g_ocrLite != nullptr) {
        delete g_ocrLite;
        g_ocrLite = nullptr;
    }
}

int ocr_detect(const char *image_path, char *out_buffer, int *buffer_len, int padding, int maxSideLen,
               float boxScoreThresh, float boxThresh, float unClipRatio, bool doAngle, bool mostAngle) {
    if (g_ocrLite == nullptr) {
        return kOcrError;
    }
    if (!isFileExists(image_path)) {
        return kOcrError;
    }
    g_ocrLite->Logger(
            "padding(%d),maxSideLen(%d),boxScoreThresh(%f),boxThresh(%f),unClipRatio(%f),doAngle(%d),mostAngle(%d)\n",
            padding, maxSideLen, boxScoreThresh, boxThresh, unClipRatio, doAngle, mostAngle);
    OcrResult result = g_ocrLite->detect("", image_path, padding, maxSideLen,
                                         boxScoreThresh, boxThresh, unClipRatio, doAngle, mostAngle);
    json root;
    root["dbNetTime"] = result.dbNetTime;
    root["detectTime"] = result.detectTime;
    for (const auto &item : result.textBlocks) {
        json textBlock;
        for (const auto &boxPoint : item.boxPoint) {
            json point;
            point["x"] = boxPoint.x;
            point["y"] = boxPoint.y;
            textBlock["boxPoint"].push_back(point);
        }
        for (const auto &score : item.charScores) {
            textBlock["charScores"].push_back(score);
        }
        textBlock["text"] = item.text;
        textBlock["boxScore"] = item.boxScore;
        textBlock["angleIndex"] = item.angleIndex;
        textBlock["angleScore"] = item.angleScore;
        textBlock["angleTime"] = item.angleTime;
        textBlock["crnnTime"] = item.crnnTime;
        textBlock["blockTime"] = item.blockTime;
        root["textBlocks"].push_back(textBlock);
        root["texts"].push_back(item.text);
    }
    std::string tempJsonStr = root.dump();
    if (static_cast<int>(tempJsonStr.length()) > *buffer_len) {
        g_ocrLite->Logger("buff_len is too small \n");
        return kOcrError;
    }
    *buffer_len = static_cast<int>(tempJsonStr.length());
    ::memcpy(out_buffer, tempJsonStr.c_str(), tempJsonStr.length());
    return kOcrSuccess;
}

int ocr_detect2(const char *image_path, char *out_buffer, int *buffer_len) {
    return ocr_detect(image_path, out_buffer, buffer_len, kDefaultPadding, kDefaultMaxSideLen, kDefaultBoxScoreThresh,
                      kDefaultBoxThresh, kDefaultUnClipRatio, kDefaultDoAngle, kDefaultMostAngle);
}
           

ocr_wrapper.go

package ocr

// -I: 配置編譯選項
// -L: 依賴庫路徑

/*
#cgo CFLAGS: -I ../../../OcrLiteOnnx/include
#cgo LDFLAGS: -L ../../../OcrLiteOnnx/lib -lOcrLiteOnnx -lstdc++

#include <stdlib.h>
#include <string.h>
#include "ocr.h"
*/
import "C"
import (
        "runtime"
        "unsafe"
)

//const kModelDbNet = "dbnet.onnx"
//const kModelAngle = "angle_net.onnx"
//const kModelCRNN = "crnn_lite_lstm.onnx"
//const kModelKeys = "keys.txt"

const kDefaultBufferLen = 10 * 1024

var (
        buffer [kDefaultBufferLen]byte
)

func Init(dbNet, angle, crnn, keys string) int {
        threadNum := runtime.NumCPU()

        cDbNet := C.CString(dbNet) // to c char*
        cAngle := C.CString(angle) // to c char*
        cCRNN := C.CString(crnn)   // to c char*
        cKeys := C.CString(keys)   // to c char*

        ret := C.ocr_init(C.int(threadNum), cDbNet, cAngle, cCRNN, cKeys)

        C.free(unsafe.Pointer(cDbNet))
        C.free(unsafe.Pointer(cAngle))
        C.free(unsafe.Pointer(cCRNN))
        C.free(unsafe.Pointer(cKeys))
        return int(ret)
}

func Detect(imagePath string) (bool, string) {
        resultLen := C.int(kDefaultBufferLen)

        // 構造C的緩沖區
        cTempBuffer := (*C.char)(unsafe.Pointer(&buffer[0]))
        cImagePath := C.CString(imagePath)
        defer C.free(unsafe.Pointer(cImagePath))

        isSuccess := C.ocr_detect2(cImagePath, cTempBuffer, &resultLen)
        return int(isSuccess) == 1, C.GoStringN(cTempBuffer, resultLen)
}

func CleanUp() {
        C.ocr_cleanup()
}
           

環境變量設定

路徑包含庫所在目錄,或者直接把動态庫拷貝到/usr/lib中,推薦後者:

export LD_LIBRARY_PATH=../mylib
           

運作

效果如下

CGO入門和OCR文字識别(非第三方API,有源碼,效果好)實戰CGO入門和OCR文字識别(非第三方API,有源碼,效果好)實戰

參考

  • 【Free Style】CGO: Go與C互操作技術(一):Go調C基本原理
  • 【Free Style】CGO: Go與C互操作技術(三):Go調C性能分析及優化
  • Command cgo
  • C? Go? Cgo!
  • How to use C++ in Go?
  • 深入學習CGO
  • 如何把Go調用C的性能提升10倍?
  • Go語言使用cgo時的記憶體管理筆記
  • GO原本:cgo
  • CGO 和 CGO 性能之謎
  • cgo is not Go
  • 深入CGO程式設計(Gopherchina2018)
  • how-to-use-c-in-go
  • 使用gcc生成靜态庫和動态庫
  • 如何使用GCC生成動态庫和靜态庫
  • gcc編譯工具生成動态庫和靜态庫之一----介紹