CGO
Karena Go memerlukan GC (Garbage Collection), untuk beberapa skenario yang membutuhkan performa lebih tinggi, Go mungkin kurang cocok. C sebagai bahasa pemrograman sistem tradisional memiliki performa yang sangat baik, dan cgo dapat menghubungkan keduanya, memungkinkan saling panggilan. Go dapat memanggil C untuk menangani tugas yang sensitif terhadap performa, sementara Go menangani logika tingkat atas. Cgo juga mendukung C memanggil Go, meskipun skenario ini jarang terjadi dan tidak terlalu disarankan.
TIP
Kode dalam artikel ini didemonstrasikan di lingkungan Windows 10, menggunakan gitbash untuk command line. Pengguna Windows disarankan untuk menginstal mingw terlebih dahulu.
Untuk cgo, terdapat pengenalan singkat dari官方: C? Go? Cgo! - The Go Programming Language. Jika ingin penjelasan lebih detail, dapat dilihat di paket standar cmd/cgo/doc.go, atau langsung melihat dokumentasi cgo command - cmd/cgo - Go Packages. Kedua konten tersebut sama.
Pemanggilan Kode
Lihat contoh berikut
package main
//#include <stdio.h>
import "C"
func main() {
C.puts(C.CString("hello, cgo!"))
}Untuk menggunakan fitur cgo, cukup dengan statement import import "C" untuk mengaktifkannya. Perlu diperhatikan bahwa C harus huruf besar, dan nama import tidak dapat diubah. Selain itu, perlu memastikan variabel environment CGO_ENABLED diset ke 1. Secara default, variabel environment ini sudah aktif.
$ go env | grep CGO
$ go env -w CGO_ENABLED=1Selain itu, perlu memastikan memiliki toolchain build C/C++ di lokal, yaitu gcc. Di platform Windows adalah mingw, agar program dapat dikompilasi dengan normal. Jalankan command berikut untuk kompilasi. Dengan mengaktifkan cgo, waktu kompilasi akan lebih lama dibandingkan Go murni.
$ go build -o ./ main.go
$ ./main.exe
hello, cgo!Hal lain yang perlu diperhatikan adalah setelah mengaktifkan cgo, tidak akan mendukung cross-compile.
Menyisipkan Kode C di Go
Cgo mendukung penulisan kode C langsung di source file Go, lalu langsung dipanggil. Lihat contoh berikut, contoh ini membuat fungsi bernama printSum, lalu dipanggil di fungsi main Go.
package main
/*
#include <stdio.h>
void printSum(int a, int b) {
printf("c:%d+%d=%d",a,b,a+b);
}
*/
import "C"
func main() {
C.printSum(C.int(1), C.int(2))
}Output
c:1+2=3Ini cocok untuk skenario sederhana. Jika kode C sangat banyak dan tercampur dengan kode Go akan sangat mengurangi keterbacaan, maka tidak cocok dilakukan seperti ini.
Penanganan Error
Dalam bahasa Go, penanganan error dikembalikan sebagai nilai return, tetapi bahasa C tidak mengizinkan multi-return. Untuk ini dapat menggunakan errno di C, yang menunjukkan terjadi error selama pemanggilan fungsi. Cgo telah melakukan kompatibilitas untuk ini, saat memanggil fungsi C dapat menangani error seperti di Go. Untuk menggunakan errno, pertama import errno.h, lihat contoh berikut
package main
/*
#include <stdio.h>
#include <stdint.h>
#include <errno.h>
int32_t sum_positive(int32_t a, int32_t b) {
if (a <= 0 || b <= 0) {
errno = EINVAL;
return 0;
}
return a + b;
}
*/
import "C"
import (
"fmt"
"reflect"
)
func main() {
sum, err := C.sum_positive(C.int32_t(0), C.int32_t(1))
if err != nil {
fmt.Println(reflect.TypeOf(err))
fmt.Println(err)
return
}
fmt.Println(sum)
}Output
syscall.Errno
The device does not recognize the command.Dapat dilihat tipe errornya adalah syscall.Errno. errno.h juga mendefinisikan banyak kode error lainnya, dapat dipelajari sendiri.
Mengimport File C di Go
Dengan mengimport file C, dapat menyelesaikan masalah di atas dengan baik. Pertama buat file header sum.h, kontennya sebagai berikut
int sum(int a, int b);Kemudian buat sum.c, tulis fungsi spesifik
#include "sum.h"
int sum(int a, int b) {
return a + b;
}Lalu import file header di main.go
package main
//#include "sum.h"
import "C"
import "fmt"
func main() {
res := C.sum(C.int(1), C.int(2))
fmt.Printf("cgo sum: %d\n", res)
}Sekarang untuk kompilasi, harus menentukan folder saat ini, jika tidak akan找不到 file C
$ go build -o sum.exe . && ./sum.exe
cgo sum: 3Dalam kode, res adalah variabel di Go, C.sum adalah fungsi di bahasa C. Nilai returnnya adalah int di C bukan int di Go. Alasan dapat berhasil dipanggil adalah karena cgo melakukan konversi tipe dari sana.
C Memanggil Go
C memanggil Go yang dimaksud adalah C memanggil Go di cgo, bukan program C native memanggil Go. Rantai panggilannya adalah go-cgo-c->cgo->go. Go memanggil C untuk memanfaatkan ekosistem dan performa C, hampir tidak ada kebutuhan program C native memanggil Go. Jika ada, juga disarankan menggunakan komunikasi jaringan sebagai gantinya.
Cgo mendukung ekspor fungsi Go untuk dipanggil C. Jika ingin mengekspor fungsi Go, perlu menambahkan komentar //export func_name di atas signature fungsi, dan parameter serta nilai returnnya harus tipe yang didukung cgo. Contoh sebagai berikut
//export sum
func sum(a, b C.int32_t) C.int32_t {
return a + b
}Ubah file sum.c sebelumnya menjadi konten berikut
#include <stdint.h>
#include <stdio.h>
#include "sum.h"
#include "_cgo_export.h"
extern int32_t sum(int32_t a, int32_t b);
void do_sum() {
int32_t a = 10;
int32_t b = 10;
int32_t c = sum(a, b);
printf("%d", c);
}Sekaligus ubah file header sum.h
void do_sum();Lalu ekspor fungsi di Go
package main
/*
#include <stdio.h>
#include <stdint.h>
#include "sum.h"
*/
import "C"
func main() {
C.do_sum()
}
//export sum
func sum(a, b C.int32_t) C.int32_t {
return a + b
}Sekarang fungsi sum yang digunakan di C sebenarnya disediakan oleh Go. Output hasil sebagai berikut
20Kunci utamanya adalah _cgo_export.h yang diimport di file sum.c, file ini berisi semua tipe yang diekspor Go. Jika tidak mengimport maka tidak dapat menggunakan fungsi yang diekspor Go. Hal lain yang perlu diperhatikan adalah _cgo_export.h tidak dapat diimport di file Go, karena prasyarat file header ini dihasilkan adalah semua source file Go harus dapat dikompilasi. Oleh karena itu penulisan seperti ini salah
package main
/*
#include <stdint.h>
#include <stdio.h>
#include "_cgo_export.h"
void do_sum() {
int32_t a = 10;
int32_t b = 10;
int32_t c = sum(a, b);
printf("%d", c);
}
*/
import "C"
func main() {
C.do_sum()
}
//export sum
func sum(a, b C.int32_t) C.int32_t {
return a + b
}Compiler akan memberitahu file header tidak ada
fatal error: _cgo_export.h: No such file or directory
#include "_cgo_export.h"
^~~~~~~~~~~~~~~
compilation terminated.Jika fungsi Go memiliki banyak nilai return, maka C akan mengembalikan struct saat dipanggil.
Sekadar informasi, kita dapat meneruskan pointer Go ke C melalui parameter fungsi C. Selama pemanggilan fungsi C, cgo akan berusaha menjamin keamanan memori. Namun nilai return fungsi Go yang diekspor tidak boleh berupa pointer, karena dalam kasus ini cgo tidak dapat menentukan apakah pointer tersebut direferensikan, dan juga sulit untuk mem-fix memori. Jika memori yang dikembalikan direferensikan, lalu di Go memori tersebut di-GC atau terjadi offset, akan menyebabkan pointer out of bounds, seperti berikut.
//export newCharPtr
func newCharPtr() *C.char {
return new(C.char)
}Penulisan di atas secara default tidak diizinkan untuk dikompilasi. Jika ingin menonaktifkan pemeriksaan ini, dapat diset seperti berikut.
GODEBUG=cgocheck=0Ada dua level pemeriksaan, dapat diset 1, 2. Semakin tinggi level, semakin besar overhead runtime yang disebabkan. Dapat前往 cgo command - passing_pointer untuk了解 detail.
Konversi Tipe
Cgo melakukan pemetaan tipe antara C dan Go, memudahkan pemanggilan saat runtime. Untuk tipe di C, setelah mengimport import "C" di Go, sebagian besar dapat langsung diakses melalui
C.typenameCara ini, misalnya
C.int(1)
C.char('a')Namun tipe C dapat terdiri dari banyak keyword, misalnya
unsigned charKasus seperti ini tidak dapat diakses langsung, tetapi dapat menggunakan keyword typedef di C untuk memberikan alias pada tipe, fungsinya setara dengan alias tipe di Go. Sebagai berikut
typedef unsigned char byte;Dengan demikian, dapat mengakses tipe unsigned char melalui C.byte. Contoh sebagai berikut
package main
/*
#include <stdio.h>
typedef unsigned char byte;
void printByte(byte b) {
printf("%c\n",b);
}
*/
import "C"
func main() {
C.printByte(C.byte('a'))
C.printByte(C.byte('b'))
C.printByte(C.byte('c'))
}Output
a
b
cSebagian besar kasus, cgo sudah memberikan alias untuk tipe yang umum digunakan (seperti tipe dasar), juga dapat mendefinisikan sendiri sesuai metode di atas, tidak akan konflik.
char
char di C sesuai dengan tipe int8 di Go, unsigned char sesuai dengan uint8 di Go yaitu tipe byte.
package main
/*
#include <stdio.h>
#include<complex.h>
char ch;
char get() {
return ch;
}
void set(char c) {
ch = c;
}
*/
import "C"
import (
"fmt"
"reflect"
)
func main() {
C.set(C.char('c'))
res := C.get()
fmt.Printf("type: %s, val: %v", reflect.TypeOf(res), res)
}Output
type: main._Ctype_char, val: 99Jika parameter set diganti dengan C.char(math.MaxInt8 + 1), maka kompilasi akan gagal, dan menampilkan error berikut
cannot convert math.MaxInt8 + 1 (untyped int constant 128) to type _Ctype_charString
Cgo menyediakan beberapa pseudo-fungsi untuk meneruskan string dan slice byte antara C dan Go. Fungsi-fungsi ini sebenarnya tidak ada, tidak dapat menemukan definisinya, sama seperti import "C", paket C juga tidak ada, hanya untuk memudahkan developer. Setelah kompilasi, mereka akan dikonversi menjadi operasi lain.
// 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) []byteString di Go pada dasarnya adalah struct, di dalamnya memegang referensi array底层. Saat diteruskan ke fungsi C, perlu menggunakan C.CString() untuk membuat "string" di C menggunakan malloc, mengalokasikan ruang memori, lalu mengembalikan pointer C. Karena C tidak memiliki tipe string, biasanya menggunakan char* untuk merepresentasikan string, yaitu pointer ke array char. Ingat untuk menggunakan free untuk melepaskan memori setelah selesai.
package main
/*
#include <stdio.h>
#include <stdlib.h>
void printfGoString(char* s) {
puts(s);
}
*/
import "C"
import "unsafe"
func main() {
cstring := C.CString("this is a go string")
C.printfGoString(cstring)
C.free(unsafe.Pointer(cstring))
}Bisa juga tipe array char, keduanya sebenarnya sama, sama-sama pointer ke elemen head.
void printfGoString(char s[]) {
puts(s);
}Bisa juga meneruskan slice byte. Karena C.CBytes() mengembalikan unsafe.Pointer, perlu dikonversi ke tipe *C.char sebelum diteruskan ke fungsi C.
package main
/*
#include <stdio.h>
#include <stdlib.h>
void printfGoString(char* s) {
puts(s);
}
*/
import "C"
import "unsafe"
func main() {
cbytes := C.CBytes([]byte("this is a go string"))
C.printfGoString((*C.char)(cbytes))
C.free(unsafe.Pointer(cbytes))
}Output contoh di atas sama
this is a go stringBeberapa metode penerusan string di atas melibatkan sekali copy memori. Setelah penerusan, sebenarnya masing-masing menyimpan salinan di memori C dan Go, ini lebih aman. Meskipun demikian, kita masih dapat langsung meneruskan pointer ke fungsi C, dan juga dapat memodifikasi string di Go langsung di C. Lihat contoh berikut
package main
/*
#include <stdio.h>
#include <stdlib.h>
void printfGoString(char* s) {
puts(s);
}
*/
import "C"
import "unsafe"
func main() {
ptr := unsafe.Pointer(unsafe.SliceData([]byte("this is a go string")))
C.printfGoString((*C.char)(ptr))
}Output
this is a go stringContoh ini langsung mendapatkan pointer array底层 string melalui unsafe.SliceData, dan mengkonversinya menjadi pointer C lalu diteruskan ke fungsi C. Memori string ini dikelola oleh Go, tentu tidak perlu free lagi. Keuntungan melakukan ini adalah tidak perlu copy saat penerusan, tetapi ada risiko tertentu. Contoh berikut mendemonstrasikan memodifikasi string di Go di C
package main
/*
#include <stdio.h>
#include <stdlib.h>
void printfGoString(char* s, int len) {
puts(s);
s[8] = 'c';
puts(s);
}
*/
import "C"
import (
"fmt"
"unsafe"
)
func main() {
var buf []byte
buf = []byte("this is a go string")
ptr := unsafe.Pointer(unsafe.SliceData(buf))
C.printfGoString((*C.char)(ptr), C.int(len(buf)))
fmt.Println(string(buf))
}Output
this is a go string
this is c go string
this is c go stringInteger
Hubungan pemetaan integer antara Go dan C seperti tabel berikut. Tentang pemetaan tipe integer juga dapat melihat beberapa informasi di paket standar cmd/cgo/gcc.go.
| go | c | cgo |
|---|---|---|
| int8 | singed char | C.schar |
| uint8 | unsigned char | C.uchar |
| int16 | short | C.short |
| uint16 | unsigned short | C.ushort |
| int32 | int | C.int |
| uint32 | unsigned int | C.uint |
| int32 | long | C.long |
| uint32 | unsigned long | C.ulong |
| int64 | long long int | C.longlong |
| uint64 | unsigned long long int | C.ulonglong |
Kode contoh sebagai berikut
package main
/*
#include <stdio.h>
void printGoInt8(signed char n) {
printf("%d\n",n);
}
void printGoUInt8(unsigned char n) {
printf("%d\n",n);
}
void printGoInt16(signed short n) {
printf("%d\n",n);
}
void printGoUInt16(unsigned short n) {
printf("%d\n",n);
}
void printGoInt32(signed int n) {
printf("%d\n",n);
}
void printGoUInt32(unsigned int n) {
printf("%d\n",n);
}
void printGoInt64(signed long long int n) {
printf("%ld\n",n);
}
void printGoUInt64(unsigned long long int n) {
printf("%ld\n",n);
}
*/
import "C"
func main() {
C.printGoInt8(C.schar(1))
C.printGoInt8(C.schar(1))
C.printGoInt16(C.short(1))
C.printGoUInt16(C.ushort(1))
C.printGoInt32(C.int(1))
C.printGoUInt32(C.uint(1))
C.printGoInt64(C.longlong(1))
C.printGoUInt64(C.ulonglong(1))
}Cgo juga mendukung tipe integer <stdint.h>, tipe di sini lebih jelas ukuran memorinya, dan gaya penamaannya juga sangat mirip dengan Go.
| go | c | cgo |
|---|---|---|
| int8 | int8_t | C.int8_t |
| uint8 | uint8_t | C.uint8_t |
| int16 | int16_t | C.int16_t |
| uint16 | uint16_t | C.uint16_t |
| int32 | int32_t | C.int32_t |
| uint32 | uint32_t | C.uint32_t |
| int64 | int64_t | C.int64_t |
| uint64 | uint64_t | C.uint64_t |
Saat menggunakan cgo, disarankan menggunakan tipe integer di <stdint.h>.
Floating Point
Pemetaan tipe floating point antara Go dan C sebagai berikut
| go | c | cgo |
|---|---|---|
| float32 | float | C.float |
| float64 | double | C.double |
Contoh kode sebagai berikut
package main
/*
#include <stdio.h>
void printGoFloat32(float n) {
printf("%f\n",n);
}
void printGoFloat64(double n) {
printf("%lf\n",n);
}
*/
import "C"
func main() {
C.printGoFloat32(C.float(1.11))
C.printGoFloat64(C.double(3.14))
}Slice
Kasus slice sebenarnya mirip dengan string yang dibahas di atas, tetapi perbedaannya adalah cgo tidak menyediakan pseudo-fungsi untuk copy slice. Jika ingin C mengakses slice di Go, hanya bisa meneruskan pointer slice. Lihat contoh berikut
package main
/*
#include <stdio.h>
#include <stdint.h>
void printInt32Arr(int32_t* s, int32_t len) {
for (int32_t i = 0; i < len; i++) {
printf("%d ", s[i]);
}
}
*/
import "C"
import (
"unsafe"
)
func main() {
var arr []int32
for i := 0; i < 10; i++ {
arr = append(arr, int32(i))
}
ptr := unsafe.Pointer(unsafe.SliceData(arr))
C.printInt32Arr((*C.int32_t)(ptr), C.int(len(arr)))
}Output
0 1 2 3 4 5 6 7 8 9Di sini pointer array底层 slice diteruskan ke fungsi C. Karena memori array ini dikelola oleh Go, tidak disarankan C memegang referensi pointer dalam jangka panjang. Sebaliknya, contoh menggunakan array C sebagai array底层 slice Go sebagai berikut
package main
/*
#include <stdio.h>
#include <stdint.h>
int32_t s[] = {1, 2, 3, 4, 5, 6, 7};
*/
import "C"
import (
"fmt"
"unsafe"
)
func main() {
l := unsafe.Sizeof(C.s) / unsafe.Sizeof(C.s[0])
fmt.Println(l)
goslice := unsafe.Slice(&C.s[0], l)
for i, e := range goslice {
fmt.Println(i, e)
}
}Output
7
0 1
1 2
2 3
3 4
4 5
5 6
6 7Melalui fungsi unsafe.Slice dapat mengkonversi pointer array menjadi slice. Sesuai intuisi, array di C hanyalah pointer ke elemen head, seharusnya digunakan seperti ini
goslice := unsafe.Slice(&C.s, l)Dari output dapat dilihat, jika melakukan seperti ini, kecuali elemen pertama, sisa memori semuanya out of bounds.
0 [1 2 3 4 5 6 7]
1 [0 -1 0 0 0 3432824 0]
2 [0 0 -1 -1 0 0 -1]
3 [0 0 0 255 0 0 0]
4 [2 0 0 0 3432544 0 0]
5 [0 3432576 0 3432592 0 3432608 0]
6 [0 0 3432624 0 0 0 1422773729]Meskipun array di C hanyalah pointer head, setelah dibungkus cgo menjadi array Go, memiliki alamat sendiri. Oleh karena itu harus mengambil alamat elemen head array.
goslice := unsafe.Slice(&C.s[0], l)Struct
Melalui prefix C.struct_ ditambah nama struct, dapat mengakses struct C. Struct C tidak dapat disisipkan sebagai struct anonim di struct Go. Berikut adalah contoh sederhana struct C
package main
/*
#include <stdio.h>
#include <stdint.h>
struct person {
int32_t age;
char* name;
};
*/
import "C"
import (
"fmt"
"reflect"
)
func main() {
var p C.struct_person
p.age = C.int32_t(18)
p.name = C.CString("john")
fmt.Println(reflect.TypeOf(p))
fmt.Printf("%+v", p)
}Output
main._Ctype_struct_person
{age:18 name:0x1dd043b6e30}Jika beberapa member struct C mengandung bit-field, cgo akan mengabaikan member struct seperti ini. Misalnya mengubah person menjadi seperti berikut
struct person {
int32_t age: 1;
char* name;
};Eksekusi lagi akan error
p.age undefined (type _Ctype_struct_person has no field or method age)Aturan alignment memori field struct C dan Go tidak sama. Jika mengaktifkan cgo, sebagian besar kasus akan dipimpin oleh C.
Union
Menggunakan C.union_ ditambah nama dapat mengakses union di C. Karena Go tidak mendukung union, mereka akan ada dalam bentuk array byte di Go. Berikut adalah contoh sederhana
package main
/*
#include <stdio.h>
#include <stdint.h>
union data {
int32_t age;
char ch;
};
*/
import "C"
import (
"fmt"
"reflect"
)
func main() {
var u C.union_data
fmt.Println(reflect.TypeOf(u), u)
}Output
[4]uint8 [0 0 0 0]Melalui unsafe.Pointer dapat mengakses dan memodifikasi
func main() {
var u C.union_data
ptr := (*C.int32_t)(unsafe.Pointer(&u))
fmt.Println(*ptr)
*ptr = C.int32_t(1024)
fmt.Println(*ptr)
fmt.Println(u)
}Output
0
1024
[0 4 0 0]Enum
Melalui prefix C.enum_ ditambah nama tipe enum dapat mengakses tipe enum di C. Berikut adalah contoh sederhana
package main
/*
#include <stdio.h>
#include <stdint.h>
enum player_state {
alive,
dead,
};
*/
import "C"
import "fmt"
type State C.enum_player_state
func (s State) String() string {
switch s {
case C.alive:
return "alive"
case C.dead:
return "dead"
default:
return "unknown"
}
}
func main() {
fmt.Println(C.alive, State(C.alive))
fmt.Println(C.dead, State(C.dead))
}Output
0 alive
1 deadPointer


Membahas pointer tidak bisa lepas dari memori. Masalah terbesar dalam saling panggilan cgo adalah model memori kedua bahasa tidak sama. Memori C sepenuhnya dikelola manual oleh developer, mengalokasikan memori dengan malloc(), melepaskan dengan free(). Jika tidak dilepaskan manual, memori tidak akan pernah dilepaskan. Oleh karena itu manajemen memori C sangat stabil. Go berbeda, Go memiliki GC, dan ruang stack Goroutine dapat disesuaikan secara dinamis. Ketika ruang stack tidak cukup akan bertambah, sehingga alamat memori mungkin berubah. Seperti gambar di atas (gambar tidak terlalu ketat), pointer mungkin menjadi dangling pointer yang umum di C. Meskipun cgo dapat menghindari memory move dalam sebagian besar kasus (oleh runtime.Pinner untuk mem-fix memori),官方 Go juga tidak menyarankan C memegang referensi memori Go dalam jangka panjang. Sebaliknya, jika pointer di Go mereferensikan memori di C, ini relatif aman. Kecuali memanggil C.free() manual, memori ini tidak akan dilepaskan otomatis.
Jika ingin meneruskan pointer antara C dan Go, perlu mengkonversinya ke unsafe.Pointer terlebih dahulu, lalu mengkonversi ke tipe pointer yang sesuai. Sama seperti void* di C. Lihat dua contoh, pertama adalah contoh pointer C mereferensikan variabel Go, dan juga memodifikasi variabel.
package main
/*
#include <stdio.h>
#include <stdint.h>
void printNum(int32_t* s) {
printf("%d\n", *s);
*s = 3;
printf("%d\n", *s);
}
*/
import "C"
import (
"fmt"
"unsafe"
)
func main() {
var num int32 = 1
ptr := unsafe.Pointer(&num)
C.printNum((*C.int32_t)(ptr))
fmt.Println(num)
}Output
1
3
3Kedua adalah contoh pointer Go mereferensikan variabel C, dan memodifikasinya.
package main
/*
#include <stdio.h>
#include <stdint.h>
int32_t num = 10;
*/
import "C"
import (
"fmt"
"unsafe"
)
func main() {
fmt.Println(C.num)
ptr := unsafe.Pointer(&C.num)
iptr := (*int32)(ptr)
*iptr++
fmt.Println(C.num)
}Output
10
11Sekadar informasi, cgo tidak mendukung function pointer di C.
Library Link
Bahasa C tidak memiliki manajemen dependensi seperti Go. Untuk langsung menggunakan library yang ditulis orang lain, selain mendapatkan source code, ada cara lain yaitu static library dan dynamic library. Cgo juga mendukung ini. Berkat ini, kita dapat mengimport library yang ditulis orang lain di program Go, tanpa perlu source code.
Dynamic Library
Dynamic library tidak dapat dijalankan sendiri. Saat runtime akan dimuat bersama file executable ke memori. Berikut mendemonstrasikan membuat dynamic library sederhana, dan memanggilnya menggunakan cgo. Pertama siapkan file lib/sum.c, kontennya sebagai berikut
#include <stdint.h>
int32_t sum(int32_t a, int32_t b) {
return a + b;
}Tulis file header lib/sum.h
#include <stdint.h>
int sum(int32_t a, int32_t b);Selanjutnya gunakan gcc untuk membuat dynamic library. Pertama kompilasi untuk menghasilkan file object
$ cd lib
$ gcc -c sum.c -o sum.oLalu buat dynamic library
$ gcc -shared -o libsum.dll sum.oSetelah selesai, lalu import file header sum.h di kode Go, dan perlu memberi tahu cgo di mana mencari file library melalui macro
package main
/*
#cgo CFLAGS: -I ./lib
#cgo LDFLAGS: -L${SRCDIR}/lib -llibsum
#include "sum.h"
*/
import "C"
import "fmt"
func main() {
res := C.sum(C.int32_t(1), C.int32_t(2))
fmt.Println(res)
}CFLAGS: -Iadalah path relatif untuk mencari file header-Ladalah path pencarian library,${SRCDIR}mewakili path absolut path saat ini, karena parameternya harus path absolut-ladalah nama file library, sum adalahsum.dll.
CFFLAGS dan LDFLAGS keduanya adalah opsi kompilasi gcc. Demi keamanan, cgo menonaktifkan beberapa parameter.前往 cgo command untuk了解 detail.
Taruh dynamic library di folder yang sama dengan exe
$ ls
go.mod go.sum lib/ libsum.dll* main.exe* main.goTerakhir kompilasi program Go dan jalankan
$ go build main.go && ./main.exe
3Sampai sini dynamic library berhasil dipanggil.
Static Library
Berbeda dengan dynamic library, saat menggunakan cgo untuk mengimport static library, akan di-link dengan file object Go menjadi file executable. Masih menggunakan sum.c sebagai contoh, pertama kompilasi source file menjadi file object
$ gcc -o sum.o -c sum.cLalu paket file object menjadi static library (harus prefix lib, jika tidak akan找不到)
$ ar rcs libsum.a sum.oKonten file Go
package main
/*
#cgo CFLAGS: -I ./lib
#cgo LDFLAGS: -L${SRCDIR}/lib -llibsum
#include "sum.h"
*/
import "C"
import "fmt"
func main() {
res := C.sum(C.int32_t(1), C.int32_t(2))
fmt.Println(res)
}Kompilasi
$ go build && ./main.exe
3Sampai sini static library berhasil dipanggil.
Terakhir
Meskipun tujuan menggunakan cgo adalah untuk performa, tetapi beralih antara C dan Go juga menyebabkan kehilangan performa tertentu. Untuk tugas yang sangat sederhana, efisiensi cgo tidak sebaik Go murni. Lihat contoh
package main
/*
#include <stdint.h>
int32_t cgo_sum(int32_t a, int32_t b) {
return a + b;
}
*/
import "C"
import (
"fmt"
"time"
)
func go_sum(a, b int32) int32 {
return a + b
}
func testSum(N int, do func()) int64 {
var sum int64
for i := 0; i < N; i++ {
start := time.Now()
do()
sum += time.Now().Sub(start).Nanoseconds()
}
return sum / int64(N)
}
func main() {
N := 1000_000
nsop1 := testSum(N, func() {
C.cgo_sum(C.int32_t(1), C.int32_t(2))
})
fmt.Printf("cgo_sum: %d ns/op\n", nsop1)
nsop2 := testSum(N, func() {
go_sum(1, 2)
})
fmt.Printf("pure_go_sum: %d ns/op\n", nsop2)
}Ini adalah tes yang sangat sederhana, masing-masing menulis fungsi penjumlahan dua angka menggunakan C dan Go, lalu masing-masing menjalankan 1 juta kali, menghitung rata-rata waktu. Hasil tes sebagai berikut
cgo_sum: 49 ns/op
pure_go_sum: 2 ns/opDari hasil dapat dilihat, rata-rata waktu cgo adalah dua puluh kali lipat dari Go murni. Jika yang dieksekusi bukan penjumlahan dua angka sederhana, tetapi tugas yang memakan waktu, keunggulan cgo akan lebih besar. Selain itu, menggunakan cgo juga memiliki kekurangan berikut
- Banyak toolchain Go yang配套 tidak dapat digunakan, seperti gotest, pprof. Contoh tes di atas tidak dapat menggunakan gotest, hanya bisa tulis manual.
- Kecepatan kompilasi melambat, cross-compile bawaan juga tidak dapat digunakan
- Masalah keamanan memori
- Masalah dependensi. Jika orang lain menggunakan library Anda, sama juga harus mengaktifkan cgo.
Sebelum mempertimbangkan dengan matang, jangan mengimport cgo di proyek. Untuk beberapa tugas yang sangat kompleks, menggunakan cgo memang dapat带来 keuntungan. Tetapi jika hanya tugas sederhana, lebih baik tetap menggunakan Go.
