CGO
Vì Go cần GC nên đối với một số tình huống yêu cầu hiệu suất cao hơn Go có thể không phù hợp để xử lý C với tư cách là ngôn ngữ lập trình hệ thống truyền thống có hiệu suất rất xuất sắc và cgo có thể liên kết cả hai với nhau cho phép gọi lẫn nhau để Go gọi C chuyển các tác vụ nhạy cảm về hiệu suất cho C hoàn thành cgo cũng hỗ trợ C gọi Go nhưng tình huống này khá hiếm và cũng không khuyến khích làm như vậy.
TIP
Mã trong bài viết được demo trên môi trường win10 dòng lệnh sử dụng gitbash người dùng Windows nên cài đặt mingw trước.
Về cgo có một giới thiệu đơn giản từ chính thức C? Go? Cgo! - The Go Programming Language nếu muốn giới thiệu chi tiết hơn có thể lấy thông tin chi tiết trong cmd/cgo/doc.go của thư viện chuẩn hoặc cũng có thể xem trực tiếp tài liệu cgo command - cmd/cgo - Go Packages nội dung của cả hai hoàn toàn giống nhau.
Gọi mã
Xem ví dụ dưới đây
package main
//#include <stdio.h>
import "C"
func main() {
C.puts(C.CString("hello, cgo!"))
}Muốn sử dụng tính năng cgo thông qua câu lệnh import import "C" là có thể bật cần lưu ý C phải là chữ hoa và tên import không thể ghi đè đồng thời cần đảm bảo biến môi trường CGO_ENABLED có được đặt thành 1 không mặc định biến môi trường này được bật.
$ go env | grep CGO
$ go env -w CGO_ENABLED=1Ngoài ra cần đảm bảo máy local có công cụ build C/C++ tức là gcc trên nền tảng Windows chính là mingw như vậy mới đảm bảo chương trình có thể biên dịch thành công. Thực hiện lệnh sau để biên dịch sau khi bật cgo thời gian biên dịch sẽ lâu hơn so với Go thuần.
$ go build -o ./ main.go
$ ./main.exe
hello, cgo!Một điểm cần lưu ý khác là sau khi bật cgo sẽ không hỗ trợ cross-compile.
Nhúng mã C vào Go
cgo hỗ trợ trực tiếp viết mã C vào file nguồn Go sau đó gọi trực tiếp xem ví dụ dưới đây ví dụ viết một hàm tên là printSum sau đó gọi trong hàm main của 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))
}Kết quả xuất
c:1+2=3Điều này phù hợp với các tình huống đơn giản nếu mã C rất nhiều trộn lẫn với mã Go sẽ làm giảm khả năng đọc thì không phù hợp để làm như vậy.
Xử lý lỗi
Trong Go xử lý lỗi được trả về dưới dạng giá trị trả về nhưng C không cho phép nhiều giá trị trả về vì vậy có thể sử dụng errno trong C biểu thị lỗi xảy ra trong quá trình gọi hàm cgo đã tương thích với điều này khi gọi hàm C có thể xử lý lỗi giống như Go. Để sử dụng errno trước tiên cần import errno.h xem ví dụ dưới đây
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)
}Kết quả xuất
syscall.Errno
The device does not recognize the command.Có thể thấy loại lỗi của nó là syscall.Errno errno.h còn định nghĩa nhiều lỗi khác bạn có thể tự tìm hiểu.
Import file C vào Go
Thông qua việc import file C có thể giải quyết vấn đề trên trước tiên tạo một file header sum.h nội dung như sau
int sum(int a, int b);Sau đó tạo sum.c viết hàm cụ thể
#include "sum.h"
int sum(int a, int b) {
return a + b;
}Sau đó import file header trong 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)
}Bây giờ khi biên dịch phải chỉ định thư mục hiện tại nếu không sẽ không tìm thấy file C như sau
$ go build -o sum.exe . && ./sum.exe
cgo sum: 3Trong mã res là một biến trong Go C.sum là hàm trong C giá trị trả về của nó là int trong C chứ không phải int trong Go lý do có thể gọi thành công là vì cgo đã thực hiện chuyển đổi loại từ giữa chúng.
C gọi Go
C gọi Go đề cập đến việc C gọi Go trong cgo chứ không phải chương trình C gốc gọi Go chúng là một chuỗi gọi go-cgo-c->cgo->go. Go gọi C là để tận dụng hệ sinh thái và hiệu suất của C hầu như không có nhu cầu chương trình C gốc gọi Go nếu có cũng nên thay thế bằng giao tiếp mạng.
cgo hỗ trợ xuất hàm Go để C gọi nếu muốn xuất hàm Go cần thêm chú thích //export func_name phía trên chữ ký hàm và các tham số và giá trị trả về của nó phải là loại được cgo hỗ trợ ví dụ như sau
//export sum
func sum(a, b C.int32_t) C.int32_t {
return a + b
}Viết lại file sum.c vừa rồi thành nội dung sau
#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);
}Đồng thời sửa file header sum.h
void do_sum();Sau đó xuất hàm trong 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
}Bây giờ hàm sum được sử dụng trong C thực tế là do Go cung cấp kết quả xuất như sau
20Điểm mấu chốt nằm ở _cgo_export.h được import trong file sum.c nó chứa tất cả các loại được Go xuất nếu không import sẽ không thể sử dụng hàm được Go xuất. Một điểm lưu ý khác là _cgo_export.h không thể được import trong file Go vì tiền đề để tạo file header này là tất cả các file nguồn Go phải có thể biên dịch thành công. Do đó cách viết sau đây là sai
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
}Trình biên dịch sẽ thông báo file header không tồn tại
fatal error: _cgo_export.h: No such file or directory
#include "_cgo_export.h"
^~~~~~~~~~~~~~~
compilation terminated.Nếu hàm Go có nhiều giá trị trả về thì khi C gọi sẽ trả về một struct.
Nhân tiện chúng ta có thể truyền con trỏ Go cho C thông qua tham số hàm C trong quá trình gọi hàm C cgo sẽ cố gắng đảm bảo an toàn bộ nhớ nhưng giá trị trả về của hàm Go xuất không được mang con trỏ vì trong trường hợp này cgo không thể判断 liệu nó có được tham chiếu hay không và cũng không cố định được bộ nhớ nếu bộ nhớ trả về được tham chiếu rồi sau đó trong Go bộ nhớ này bị GC thu hồi hoặc bị lệch sẽ dẫn đến con trỏ vượt quá giới hạn như sau.
//export newCharPtr
func newCharPtr() *C.char {
return new(C.char)
}Cách viết trên mặc định không cho phép biên dịch thành công nếu muốn tắt kiểm tra này có thể đặt như sau.
GODEBUG=cgocheck=0Nó có hai cấp độ kiểm tra có thể đặt thành 1 2 cấp độ càng cao chi phí runtime do kiểm tra gây ra càng lớn bạn có thể đến cgo command - passing_pointer để biết chi tiết.
Chuyển đổi loại
cgo đã tạo một ánh xạ loại giữa C và Go để thuận tiện cho việc gọi trong runtime. Đối với loại trong C sau khi import import "C" trong Go trong hầu hết các trường hợp có thể truy cập trực tiếp thông qua
C.typenameví dụ như
C.int(1)
C.char('a')Nhưng loại trong C có thể được tạo thành từ nhiều từ khóa ví dụ như
unsigned charTrong trường hợp này không thể truy cập trực tiếp được nhưng có thể sử dụng từ khóa typedef trong C để đặt bí danh cho loại chức năng tương đương với bí danh loại trong Go. Như sau
typedef unsigned char byte;Như vậy có thể truy cập loại unsigned char thông qua C.byte. Ví dụ như sau
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'))
}Kết quả xuất
a
b
cTrong hầu hết các trường hợp cgo đã đặt bí danh cho các loại thông dụng (loại cơ bản v.v.) bạn cũng có thể tự định nghĩa theo phương pháp trên sẽ không xung đột.
char
char trong C tương ứng với loại int8 trong Go unsigned char tương ứng với uint8 trong Go tức là loại 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)
}Kết quả xuất
type: main._Ctype_char, val: 99Nếu thay đổi tham số của set thành C.char(math.MaxInt8 + 1) thì biên dịch sẽ thất bại và thông báo lỗi sau
cannot convert math.MaxInt8 + 1 (untyped int constant 128) to type _Ctype_charChuỗi
cgo cung cấp một số hàm giả để truyền chuỗi và slice byte giữa C và Go những hàm này thực tế không tồn tại bạn cũng không thể tìm thấy định nghĩa của chúng giống như import "C" gói C này cũng không tồn tại chỉ để thuận tiện cho nhà phát triển sử dụng sau khi biên dịch chúng sẽ được chuyển đổi thành các thao tác khá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) []byteBản chất chuỗi trong Go là một struct bên trong giữ một tham chiếu đến mảng cơ sở khi truyền cho hàm C cần sử dụng C.CString() để tạo một "chuỗi" trong C sử dụng malloc để phân bổ không gian bộ nhớ sau đó trả về một con trỏ C vì C không có loại chuỗi thường sẽ sử dụng char* để biểu thị chuỗi tức là con trỏ của một mảng ký tự nhớ giải phóng bộ nhớ sau khi sử dụng xong bằng free.
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))
}Cũng có thể là loại mảng char thực tế cả hai đều giống nhau đều là con trỏ trỏ đến phần tử đầu.
void printfGoString(char s[]) {
puts(s);
}Cũng có thể truyền slice byte vì C.CBytes() sẽ trả về một unsafe.Pointer trước khi truyền cho hàm C cần chuyển đổi nó thành loại *C.char.
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))
}Kết quả xuất của các ví dụ trên đều giống nhau
this is a go stringCác phương thức truyền chuỗi trên liên quan đến một lần sao chép bộ nhớ sau khi truyền thực tế là mỗi bên giữ một bản sao trong bộ nhớ C và bộ nhớ Go làm như vậy sẽ an toàn hơn. Tuy nhiên chúng ta vẫn có thể truyền trực tiếp con trỏ cho hàm C cũng có thể sửa đổi chuỗi trong Go trực tiếp trong C xem ví dụ dưới đây
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))
}Kết quả xuất
this is a go stringVí dụ trực tiếp lấy con trỏ của mảng cơ sở của chuỗi thông qua unsafe.SliceData và chuyển đổi nó thành con trỏ C sau đó truyền cho hàm C bộ nhớ của chuỗi này được Go quản lý tự nhiên không cần free nữa lợi ích của việc làm như vậy là không cần sao chép trong quá trình truyền nhưng có một số rủi ro. Ví dụ dưới đây demo việc sửa đổi chuỗi trong Go trong 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))
}Kết quả xuất
this is a go string
this is c go string
this is c go stringSố nguyên
Mối quan hệ ánh xạ giữa số nguyên trong Go và C như bảng dưới đây về ánh xạ loại số nguyên cũng có thể xem một số thông tin liên quan trong cmd/cgo/gcc.go của thư viện chuẩn.
| 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 |
Mã ví dụ như sau
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 đồng thời hỗ trợ loại số nguyên của <stdint.h> loại ở đây có kích thước bộ nhớ rõ ràng hơn và phong cách đặt tên của nó cũng rất giống với 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 |
Khi sử dụng cgo nên sử dụng loại số nguyên trong <stdint.h>.
Số thực
Ánh xạ loại số thực giữa Go và C như sau
| go | c | cgo |
|---|---|---|
| float32 | float | C.float |
| float64 | double | C.double |
Ví dụ mã như sau
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
Trường hợp slice thực tế giống với chuỗi đã đề cập ở trên nhưng điểm khác biệt là cgo không cung cấp hàm giả để sao chép slice muốn để C truy cập slice trong Go chỉ có thể truyền con trỏ của slice sang. Xem ví dụ dưới đây
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)))
}Kết quả xuất
0 1 2 3 4 5 6 7 8 9Ở đây truyền con trỏ của mảng cơ sở của slice cho hàm C vì bộ nhớ của mảng này được Go quản lý không khuyến khích C giữ tham chiếu con trỏ trong thời gian dài. Ngược lại ví dụ sử dụng mảng của C làm mảng cơ sở của slice trong Go như sau
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)
}
}Kết quả xuất
7
0 1
1 2
2 3
3 4
4 5
5 6
6 7Thông qua hàm unsafe.Slice có thể chuyển đổi con trỏ mảng thành slice theo trực giác mảng trong C chỉ là một con trỏ trỏ đến phần tử đầu theo lý thuyết nên sử dụng như sau
goslice := unsafe.Slice(&C.s, l)Thông qua kết quả xuất có thể thấy nếu làm như vậy ngoại trừ phần tử đầu tiên tất cả bộ nhớ còn lại đều vượt quá giới hạn.
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]Cho dù mảng trong C chỉ là một con trỏ đầu sau khi được cgo bọc sẽ trở thành mảng Go có địa chỉ riêng vì vậy nên lấy địa chỉ phần tử đầu của mảng.
goslice := unsafe.Slice(&C.s[0], l)Struct
Thông qua tiền tố C.struct_ cộng với tên struct có thể truy cập struct trong C struct trong C không thể được nhúng ẩn danh vào struct trong Go. Dưới đây là một ví dụ đơn giản về struct trong 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)
}Kết quả xuất
main._Ctype_struct_person
{age:18 name:0x1dd043b6e30}Nếu một số thành viên của struct trong C chứa bit-field cgo sẽ bỏ qua các thành viên struct loại này ví dụ sửa person thành như sau
struct person {
int32_t age: 1;
char* name;
};Thực hiện lại sẽ báo lỗi
p.age undefined (type _Ctype_struct_person has no field or method age)Quy tắc căn chỉnh bộ nhớ của các trường struct trong C và Go không giống nhau nếu bật cgo trong hầu hết các trường hợp sẽ do C主导.
Union
Sử dụng C.union_ cộng với tên có thể truy cập union trong C vì Go không hỗ trợ union chúng sẽ tồn tại dưới dạng mảng byte trong Go. Dưới đây là một ví dụ đơn giản
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)
}Kết quả xuất
[4]uint8 [0 0 0 0]Thông qua unsafe.Pointer có thể truy cập và sửa đổi
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)
}Kết quả xuất
0
1024
[0 4 0 0]Enum
Thông qua tiền tố C.enum_ cộng với tên enum có thể truy cập loại enum trong C. Dưới đây là một ví dụ đơn giản
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))
}Kết quả xuất
0 alive
1 deadCon trỏ


Nói đến con trỏ không thể không nói đến bộ nhớ vấn đề lớn nhất khi gọi lẫn nhau giữa cgo là mô hình bộ nhớ của hai ngôn ngữ không giống nhau bộ nhớ của C hoàn toàn do nhà phát triển quản lý thủ công sử dụng malloc() để phân bổ bộ nhớ free() để giải phóng bộ nhớ nếu không giải phóng thủ công nó sẽ không tự giải phóng vì vậy quản lý bộ nhớ của C rất ổn định. Còn Go thì khác nó có GC và không gian stack của Goroutine sẽ được điều chỉnh động khi không gian stack không đủ sẽ tăng lên như vậy địa chỉ bộ nhớ có thể đã thay đổi giống như hình trên (hình vẽ không chính xác lắm) con trỏ có thể trở thành con trỏ treo phổ biến trong C. Cho dù cgo có thể tránh di chuyển bộ nhớ trong hầu hết các trường hợp (do runtime.Pinner để cố định bộ nhớ) nhưng Go chính thức cũng không khuyến khích C tham chiếu bộ nhớ của Go trong thời gian dài. Nhưng ngược lại nếu con trỏ trong Go tham chiếu bộ nhớ trong C thì khá an toàn trừ khi gọi C.free() thủ công nếu không bộ nhớ này sẽ không được giải phóng tự động.
Nếu muốn truyền con trỏ giữa C và Go cần chuyển nó thành unsafe.Pointer trước sau đó chuyển đổi thành loại con trỏ tương ứng giống như void* trong C. Xem hai ví dụ ví dụ đầu tiên là ví dụ về con trỏ C tham chiếu biến Go và còn sửa đổi biến.
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)
}Kết quả xuất
1
3
3Ví dụ thứ hai là con trỏ Go tham chiếu biến C và sửa đổi nó.
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)
}Kết quả xuất
10
11Nhân tiện cgo không hỗ trợ con trỏ hàm trong C.
Thư viện liên kết
C không có quản lý phụ thuộc như Go muốn sử dụng thư viện do người khác viết ngoài việc lấy mã nguồn trực tiếp còn có một cách khác là thư viện liên kết tĩnh và thư viện liên kết động cgo cũng hỗ trợ những thứ này nhờ đó chúng ta có thể import thư viện do người khác viết vào chương trình Go mà không cần mã nguồn.
Thư viện liên kết động
Thư viện liên kết động không thể chạy độc lập nó sẽ được load vào bộ nhớ cùng với file thực thi trong runtime dưới đây demo tạo một thư viện liên kết động đơn giản và sử dụng cgo để gọi. Trước tiên chuẩn bị một file lib/sum.c nội dung như sau
#include <stdint.h>
int32_t sum(int32_t a, int32_t b) {
return a + b;
}Viết file header lib/sum.h
#include <stdint.h>
int sum(int32_t a, int32_t b);Tiếp theo sử dụng gcc để tạo thư viện liên kết động trước tiên biên dịch tạo file đối tượng
$ cd lib
$ gcc -c sum.c -o sum.oSau đó tạo thư viện liên kết động
$ gcc -shared -o libsum.dll sum.oSau khi tạo xong import file header sum.h trong mã Go và còn phải thông qua macro告诉 cgo tìm file thư viện ở đâu
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: -Ilà đường dẫn tương đối để tìm file header-Llà đường dẫn tìm thư viện${SRCDIR}đại diện cho đường dẫn tuyệt đối của đường dẫn hiện tại vì tham số của nó phải là đường dẫn tuyệt đối-llà tên của file thư viện sum chính làsum.dll.
CFFLAGS và LDFLAGS đều là tùy chọn biên dịch của gcc vì lý do an toàn cgo đã vô hiệu hóa một số tham số đến cgo command để biết chi tiết.
Đặt thư viện động vào cùng thư mục với exe
$ ls
go.mod go.sum lib/ libsum.dll* main.exe* main.goCuối cùng biên dịch chương trình Go và thực thi
$ go build main.go && ./main.exe
3Đến đây gọi thư viện liên kết động thành công.
Thư viện liên kết tĩnh
Khác với thư viện liên kết động khi sử dụng cgo để import thư viện liên kết tĩnh nó sẽ được liên kết với file đối tượng Go cuối cùng thành một file thực thi. Vẫn lấy sum.c làm ví dụ trước tiên biên dịch file nguồn thành file đối tượng
$ gcc -o sum.o -c sum.cSau đó đóng gói file đối tượng thành thư viện liên kết tĩnh (phải bắt đầu bằng tiền tố lib nếu không sẽ không tìm thấy)
$ ar rcs libsum.a sum.oNội dung 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)
}Biên dịch
$ go build && ./main.exe
3Đến đây gọi thư viện liên kết tĩnh thành công.
Cuối cùng
Mặc dù điểm xuất phát của việc sử dụng cgo là vì hiệu suất nhưng việc chuyển đổi giữa C và Go cũng sẽ gây ra tổn thất hiệu suất không nhỏ đối với một số tác vụ rất đơn giản hiệu quả của cgo không bằng Go thuần. Xem một ví dụ
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)
}Đây là một bài kiểm tra rất đơn giản lần lượt viết hàm tính tổng hai số bằng C và Go sau đó chạy 1 triệu lần mỗi hàm tính thời gian trung bình kết quả kiểm tra như sau
cgo_sum: 49 ns/op
pure_go_sum: 2 ns/opTừ kết quả có thể thấy thời gian trung bình của cgo gấp hơn 20 lần so với Go thuần nếu không thực hiện phép cộng hai số đơn thuần mà là một tác vụ tốn thời gian hơn lợi thế của cgo sẽ lớn hơn một chút. Ngoài ra việc sử dụng cgo còn có những nhược điểm sau
- Nhiều công cụ đi kèm của Go sẽ không thể sử dụng được ví dụ như gotest pprof ví dụ kiểm tra trên không thể sử dụng gotest chỉ có thể tự viết tay.
- Tốc độ biên dịch chậm hơn cross-compile tích hợp sẵn cũng không thể sử dụng được
- Vấn đề an toàn bộ nhớ
- Vấn đề phụ thuộc nếu người khác sử dụng thư viện của bạn cũng phải bật cgo.
Trước khi suy nghĩ kỹ đừng import cgo vào dự án đối với một số tác vụ rất phức tạp sử dụng cgo thực sự có thể mang lại lợi ích nhưng nếu chỉ là một số tác vụ đơn giản thì vẫn nên dùng Go.
