Go 語言 快速入門

2023-03-22 14:57 更新

原文鏈接:https://chai2010.cn/advanced-go-programming-book/ch2-cgo/ch2-01-hello-cgo.html


2.1 快速入門

本節(jié)我們將通過一系列由淺入深的小例子來快速掌握 CGO 的基本用法。

2.1.1 最簡 CGO 程序

真實(shí)的 CGO 程序一般都比較復(fù)雜。不過我們可以由淺入深,一個(gè)最簡的 CGO 程序該是什么樣的呢?要構(gòu)造一個(gè)最簡 CGO 程序,首先要忽視一些復(fù)雜的 CGO 特性,同時(shí)要展示 CGO 程序和純 Go 程序的差別來。下面是我們構(gòu)建的最簡 CGO 程序:

// hello.go
package main

import "C"

func main() {
    println("hello cgo")
}

代碼通過 import "C" 語句啟用 CGO 特性,主函數(shù)只是通過 Go 內(nèi)置的 println 函數(shù)輸出字符串,其中并沒有任何和 CGO 相關(guān)的代碼。雖然沒有調(diào)用 CGO 的相關(guān)函數(shù),但是 go build 命令會(huì)在編譯和鏈接階段啟動(dòng) gcc 編譯器,這已經(jīng)是一個(gè)完整的 CGO 程序了。

2.1.2 基于 C 標(biāo)準(zhǔn)庫函數(shù)輸出字符串

第一章那個(gè) CGO 程序還不夠簡單,我們現(xiàn)在來看看更簡單的版本:

// hello.go
package main

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

func main() {
    C.puts(C.CString("Hello, World\n"))
}

我們不僅僅通過 import "C" 語句啟用 CGO 特性,同時(shí)包含 C 語言的 <stdio.h> 頭文件。然后通過 CGO 包的 C.CString 函數(shù)將 Go 語言字符串轉(zhuǎn)為 C 語言字符串,最后調(diào)用 CGO 包的 C.puts 函數(shù)向標(biāo)準(zhǔn)輸出窗口打印轉(zhuǎn)換后的 C 字符串。

相比 “Hello, World 的革命” 一節(jié)中的 CGO 程序最大的不同是:我們沒有在程序退出前釋放 C.CString 創(chuàng)建的 C 語言字符串;還有我們改用 puts 函數(shù)直接向標(biāo)準(zhǔn)輸出打印,之前是采用 fputs 向標(biāo)準(zhǔn)輸出打印。

沒有釋放使用 C.CString 創(chuàng)建的 C 語言字符串會(huì)導(dǎo)致內(nèi)存泄漏。但是對于這個(gè)小程序來說,這樣是沒有問題的,因?yàn)槌绦蛲顺龊蟛僮飨到y(tǒng)會(huì)自動(dòng)回收程序的所有資源。

2.1.3 使用自己的 C 函數(shù)

前面我們使用了標(biāo)準(zhǔn)庫中已有的函數(shù)?,F(xiàn)在我們先自定義一個(gè)叫 SayHello 的 C 函數(shù)來實(shí)現(xiàn)打印,然后從 Go 語言環(huán)境中調(diào)用這個(gè) SayHello 函數(shù):

// hello.go
package main

/*
#include <stdio.h>

static void SayHello(const char* s) {
    puts(s);
}
*/
import "C"

func main() {
    C.SayHello(C.CString("Hello, World\n"))
}

除了 SayHello 函數(shù)是我們自己實(shí)現(xiàn)的之外,其它的部分和前面的例子基本相似。

我們也可以將 SayHello 函數(shù)放到當(dāng)前目錄下的一個(gè) C 語言源文件中(后綴名必須是 .c)。因?yàn)槭蔷帉懺讵?dú)立的 C 文件中,為了允許外部引用,所以需要去掉函數(shù)的 static 修飾符。

// hello.c

#include <stdio.h>

void SayHello(const char* s) {
    puts(s);
}

然后在 CGO 部分先聲明 SayHello 函數(shù),其它部分不變:

// hello.go
package main

//void SayHello(const char* s);
import "C"

func main() {
    C.SayHello(C.CString("Hello, World\n"))
}

注意,如果之前運(yùn)行的命令是 go run hello.go 或 go build hello.go 的話,此處須使用 go run "your/package" 或 go build "your/package" 才可以。若本就在包路徑下的話,也可以直接運(yùn)行 go run . 或 go build。

既然 SayHello 函數(shù)已經(jīng)放到獨(dú)立的 C 文件中了,我們自然可以將對應(yīng)的 C 文件編譯打包為靜態(tài)庫或動(dòng)態(tài)庫文件供使用。如果是以靜態(tài)庫或動(dòng)態(tài)庫方式引用 SayHello 函數(shù)的話,需要將對應(yīng)的 C 源文件移出當(dāng)前目錄(CGO 構(gòu)建程序會(huì)自動(dòng)構(gòu)建當(dāng)前目錄下的 C 源文件,從而導(dǎo)致 C 函數(shù)名沖突)。關(guān)于靜態(tài)庫等細(xì)節(jié)將在稍后章節(jié)講解。

2.1.4 C 代碼的模塊化

在編程過程中,抽象和模塊化是將復(fù)雜問題簡化的通用手段。當(dāng)代碼語句變多時(shí),我們可以將相似的代碼封裝到一個(gè)個(gè)函數(shù)中;當(dāng)程序中的函數(shù)變多時(shí),我們將函數(shù)拆分到不同的文件或模塊中。而模塊化編程的核心是面向程序接口編程(這里的接口并不是 Go 語言的 interface,而是 API 的概念)。

在前面的例子中,我們可以抽象一個(gè)名為 hello 的模塊,模塊的全部接口函數(shù)都在 hello.h 頭文件定義:

// hello.h
void SayHello(const char* s);

其中只有一個(gè) SayHello 函數(shù)的聲明。但是作為 hello 模塊的用戶來說,就可以放心地使用 SayHello 函數(shù),而無需關(guān)心函數(shù)的具體實(shí)現(xiàn)。而作為 SayHello 函數(shù)的實(shí)現(xiàn)者來說,函數(shù)的實(shí)現(xiàn)只要滿足頭文件中函數(shù)的聲明的規(guī)范即可。下面是 SayHello 函數(shù)的 C 語言實(shí)現(xiàn),對應(yīng) hello.c 文件:

// hello.c

#include "hello.h"
#include <stdio.h>

void SayHello(const char* s) {
    puts(s);
}

在 hello.c 文件的開頭,實(shí)現(xiàn)者通過 #include "hello.h" 語句包含 SayHello 函數(shù)的聲明,這樣可以保證函數(shù)的實(shí)現(xiàn)滿足模塊對外公開的接口。

接口文件 hello.h 是 hello 模塊的實(shí)現(xiàn)者和使用者共同的約定,但是該約定并沒有要求必須使用 C 語言來實(shí)現(xiàn) SayHello 函數(shù)。我們也可以用 C++ 語言來重新實(shí)現(xiàn)這個(gè) C 語言函數(shù):

// hello.cpp

#include <iostream>

extern "C" {
    #include "hello.h"
}

void SayHello(const char* s) {
    std::cout << s;
}

在 C++ 版本的 SayHello 函數(shù)實(shí)現(xiàn)中,我們通過 C++ 特有的 std::cout 輸出流輸出字符串。不過為了保證 C++ 語言實(shí)現(xiàn)的 SayHello 函數(shù)滿足 C 語言頭文件 hello.h 定義的函數(shù)規(guī)范,我們需要通過 extern "C" 語句指示該函數(shù)的鏈接符號(hào)遵循 C 語言的規(guī)則。

在采用面向 C 語言 API 接口編程之后,我們徹底解放了模塊實(shí)現(xiàn)者的語言枷鎖:實(shí)現(xiàn)者可以用任何編程語言實(shí)現(xiàn)模塊,只要最終滿足公開的 API 約定即可。我們可以用 C 語言實(shí)現(xiàn) SayHello 函數(shù),也可以使用更復(fù)雜的 C++ 語言來實(shí)現(xiàn) SayHello 函數(shù),當(dāng)然我們也可以用匯編語言甚至 Go 語言來重新實(shí)現(xiàn) SayHello 函數(shù)。

2.1.5 用 Go 重新實(shí)現(xiàn) C 函數(shù)

其實(shí) CGO 不僅僅用于 Go 語言中調(diào)用 C 語言函數(shù),還可以用于導(dǎo)出 Go 語言函數(shù)給 C 語言函數(shù)調(diào)用。在前面的例子中,我們已經(jīng)抽象一個(gè)名為 hello 的模塊,模塊的全部接口函數(shù)都在 hello.h 頭文件定義:

// hello.h
void SayHello(/*const*/ char* s);

現(xiàn)在我們創(chuàng)建一個(gè) hello.go 文件,用 Go 語言重新實(shí)現(xiàn) C 語言接口的 SayHello 函數(shù):

// hello.go
package main

import "C"

import "fmt"

//export SayHello
func SayHello(s *C.char) {
    fmt.Print(C.GoString(s))
}

我們通過 CGO 的 //export SayHello 指令將 Go 語言實(shí)現(xiàn)的函數(shù) SayHello 導(dǎo)出為 C 語言函數(shù)。為了適配 CGO 導(dǎo)出的 C 語言函數(shù),我們禁止了在函數(shù)的聲明語句中的 const 修飾符。需要注意的是,這里其實(shí)有兩個(gè)版本的 SayHello 函數(shù):一個(gè) Go 語言環(huán)境的;另一個(gè)是 C 語言環(huán)境的。cgo 生成的 C 語言版本 SayHello 函數(shù)最終會(huì)通過橋接代碼調(diào)用 Go 語言版本的 SayHello 函數(shù)。

通過面向 C 語言接口的編程技術(shù),我們不僅僅解放了函數(shù)的實(shí)現(xiàn)者,同時(shí)也簡化的函數(shù)的使用者?,F(xiàn)在我們可以將 SayHello 當(dāng)作一個(gè)標(biāo)準(zhǔn)庫的函數(shù)使用(和 puts 函數(shù)的使用方式類似):

package main

//#include <hello.h>
import "C"

func main() {
    C.SayHello(C.CString("Hello, World\n"))
}

一切似乎都回到了開始的 CGO 代碼,但是代碼內(nèi)涵更豐富了。

2.1.6 面向 C 接口的 Go 編程

在開始的例子中,我們的全部 CGO 代碼都在一個(gè) Go 文件中。然后,通過面向 C 接口編程的技術(shù)將 SayHello 分別拆分到不同的 C 文件,而 main 依然是 Go 文件。再然后,是用 Go 函數(shù)重新實(shí)現(xiàn)了 C 語言接口的 SayHello 函數(shù)。但是對于目前的例子來說只有一個(gè)函數(shù),要拆分到三個(gè)不同的文件確實(shí)有些繁瑣了。

正所謂合久必分、分久必合,我們現(xiàn)在嘗試將例子中的幾個(gè)文件重新合并到一個(gè) Go 文件。下面是合并后的成果:

package main

//void SayHello(char* s);
import "C"

import (
    "fmt"
)

func main() {
    C.SayHello(C.CString("Hello, World\n"))
}

//export SayHello
func SayHello(s *C.char) {
    fmt.Print(C.GoString(s))
}

現(xiàn)在版本的 CGO 代碼中 C 語言代碼的比例已經(jīng)很少了,但是我們依然可以進(jìn)一步以 Go 語言的思維來提煉我們的 CGO 代碼。通過分析可以發(fā)現(xiàn) SayHello 函數(shù)的參數(shù)如果可以直接使用 Go 字符串是最直接的。在 Go1.10 中 CGO 新增加了一個(gè) _GoString_ 預(yù)定義的 C 語言類型,用來表示 Go 語言字符串。下面是改進(jìn)后的代碼:

// +build go1.10

package main

//void SayHello(_GoString_ s);
import "C"

import (
    "fmt"
)

func main() {
    C.SayHello("Hello, World\n")
}

//export SayHello
func SayHello(s string) {
    fmt.Print(s)
}

雖然看起來全部是 Go 語言代碼,但是執(zhí)行的時(shí)候是先從 Go 語言的 main 函數(shù),到 CGO 自動(dòng)生成的 C 語言版本 SayHello 橋接函數(shù),最后又回到了 Go 語言環(huán)境的 SayHello 函數(shù)。這個(gè)代碼包含了 CGO 編程的精華,讀者需要深入理解。

思考題: main 函數(shù)和 SayHello 函數(shù)是否在同一個(gè) Goroutine 里執(zhí)行?



以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)