Go 語(yǔ)言 快速入門

2023-03-22 14:57 更新

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


2.1 快速入門

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

2.1.1 最簡(jiǎn) CGO 程序

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

// hello.go
package main

import "C"

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

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

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

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

// hello.go
package main

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

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

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

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

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

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

前面我們使用了標(biāo)準(zhǔn)庫(kù)中已有的函數(shù)?,F(xiàn)在我們先自定義一個(gè)叫 SayHello 的 C 函數(shù)來(lái)實(shí)現(xiàn)打印,然后從 Go 語(yǔ)言環(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 語(yǔ)言源文件中(后綴名必須是 .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 文件中了,我們自然可以將對(duì)應(yīng)的 C 文件編譯打包為靜態(tài)庫(kù)或動(dòng)態(tài)庫(kù)文件供使用。如果是以靜態(tài)庫(kù)或動(dòng)態(tài)庫(kù)方式引用 SayHello 函數(shù)的話,需要將對(duì)應(yīng)的 C 源文件移出當(dāng)前目錄(CGO 構(gòu)建程序會(huì)自動(dòng)構(gòu)建當(dāng)前目錄下的 C 源文件,從而導(dǎo)致 C 函數(shù)名沖突)。關(guān)于靜態(tài)庫(kù)等細(xì)節(jié)將在稍后章節(jié)講解。

2.1.4 C 代碼的模塊化

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

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

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

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

// hello.c

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

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

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

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

// hello.cpp

#include <iostream>

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

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

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

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

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

其實(shí) CGO 不僅僅用于 Go 語(yǔ)言中調(diào)用 C 語(yǔ)言函數(shù),還可以用于導(dǎo)出 Go 語(yǔ)言函數(shù)給 C 語(yǔ)言函數(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 語(yǔ)言重新實(shí)現(xiàn) C 語(yǔ)言接口的 SayHello 函數(shù):

// hello.go
package main

import "C"

import "fmt"

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

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

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

package main

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

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

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

2.1.6 面向 C 接口的 Go 編程

在開(kāi)始的例子中,我們的全部 CGO 代碼都在一個(gè) Go 文件中。然后,通過(guò)面向 C 接口編程的技術(shù)將 SayHello 分別拆分到不同的 C 文件,而 main 依然是 Go 文件。再然后,是用 Go 函數(shù)重新實(shí)現(xiàn)了 C 語(yǔ)言接口的 SayHello 函數(shù)。但是對(duì)于目前的例子來(lái)說(shuō)只有一個(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 語(yǔ)言代碼的比例已經(jīng)很少了,但是我們依然可以進(jìn)一步以 Go 語(yǔ)言的思維來(lái)提煉我們的 CGO 代碼。通過(guò)分析可以發(fā)現(xiàn) SayHello 函數(shù)的參數(shù)如果可以直接使用 Go 字符串是最直接的。在 Go1.10 中 CGO 新增加了一個(gè) _GoString_ 預(yù)定義的 C 語(yǔ)言類型,用來(lái)表示 Go 語(yǔ)言字符串。下面是改進(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)
}

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

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



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

掃描二維碼

下載編程獅App

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

編程獅公眾號(hào)