CGO 入门:Go 与 C 的桥梁
Go 语言虽然强大,但有时候你不得不和 C 语言打交道:
- 使用一个只有 C 版本的库(如 libjpeg、libpng)
- 调用系统底层的 API
- 集成遗留的 C 代码
- 追求极致性能的关键路径
这时候,CGO 就是你的桥梁。CGO 是 Go 提供的一种机制,允许 Go 代码调用 C 代码,反之亦然。
⚠️ 警告:Dave Cheney(Go 社区知名开发者)曾说过:"CGO 不是 Go"。使用 CGO 会带来:
- 编译速度变慢
- 交叉编译困难
- 内存管理复杂
- 性能开销
除非必要,否则尽量避免使用 CGO。
第一个 CGO 程序
package main
/*
#include <stdio.h>
void hello() {
printf("Hello from C!\n");
}
*/
import "C"
import "fmt"
func main() {
fmt.Println("Hello from Go!")
C.hello()
}
注意几个关键点:
- C 代码放在
/* */注释中 import "C"必须紧跟在 C 代码注释后面import "C"和其他 import 之间不能有空行- 通过
C.xxx调用 C 函数
编译运行:
go run main.go
# Hello from Go!
# Hello from C!
类型转换
Go 和 C 有不同的类型系统,CGO 提供了类型转换机制:
基本类型
package main
/*
#include <stdint.h>
int add(int a, int b) {
return a + b;
}
double multiply(double a, double b) {
return a * b;
}
*/
import "C"
import "fmt"
func main() {
// Go int → C int
a := C.int(10)
b := C.int(20)
result := C.add(a, b)
fmt.Println("10 + 20 =", int(result))
// Go float64 → C double
x := C.double(3.14)
y := C.double(2.0)
product := C.multiply(x, y)
fmt.Println("3.14 * 2.0 =", float64(product))
}
字符串
package main
/*
#include <stdio.h>
#include <string.h>
void print_string(const char* str) {
printf("C received: %s\n", str);
}
int string_length(const char* str) {
return strlen(str);
}
*/
import "C"
import (
"fmt"
"unsafe"
)
func main() {
// Go string → C string
goStr := "Hello, C!"
cStr := C.CString(goStr)
defer C.free(unsafe.Pointer(cStr)) // 必须手动释放!
C.print_string(cStr)
// C string → Go string
length := C.string_length(cStr)
fmt.Printf("字符串长度: %d\n", int(length))
// C.GoString 转换回 Go string
goStr2 := C.GoString(cStr)
fmt.Println("Go string:", goStr2)
}
⚠️ 重要:C.CString 会在 C 的堆上分配内存,必须用 C.free 手动释放,否则会造成内存泄漏!
数组和切片
package main
/*
#include <stdio.h>
void print_array(int* arr, int len) {
for (int i = 0; i < len; i++) {
printf("%d ", arr[i]);
}
printf("\n");
}
int sum_array(int* arr, int len) {
int sum = 0;
for (int i = 0; i < len; i++) {
sum += arr[i];
}
return sum;
}
*/
import "C"
import (
"fmt"
"unsafe"
)
func main() {
// Go 切片 → C 数组
goSlice := []int{1, 2, 3, 4, 5}
// 获取切片的底层数组指针
cArray := (*C.int)(unsafe.Pointer(&goSlice[0]))
cLen := C.int(len(goSlice))
C.print_array(cArray, cLen)
sum := C.sum_array(cArray, cLen)
fmt.Println("数组和:", int(sum))
}
使用外部 C 库
链接系统库
package main
/*
#cgo LDFLAGS: -lm
#include <math.h>
*/
import "C"
import "fmt"
func main() {
x := C.double(16.0)
result := C.sqrt(x)
fmt.Printf("sqrt(16) = %f\n", float64(result))
}
链接自定义库
假设你有一个 C 库 mylib:
// mylib.h
#ifndef MYLIB_H
#define MYLIB_H
int calculate(int a, int b);
#endif
// mylib.c
#include "mylib.h"
int calculate(int a, int b) {
return a * a + b * b;
}
编译成静态库:
gcc -c mylib.c -o mylib.o
ar rcs libmylib.a mylib.o
在 Go 中使用:
package main
/*
#cgo CFLAGS: -I${SRCDIR}
#cgo LDFLAGS: -L${SRCDIR} -lmylib
#include "mylib.h"
*/
import "C"
import "fmt"
func main() {
a := C.int(3)
b := C.int(4)
result := C.calculate(a, b)
fmt.Printf("3² + 4² = %d\n", int(result)) // 25
}
实战:调用图像处理库
让我们用 CGO 调用 C 的图像处理库:
package main
/*
#cgo LDFLAGS: -lm
#include <stdlib.h>
#include <math.h>
// 简单的图像结构
typedef struct {
unsigned char* data;
int width;
int height;
int channels;
} Image;
// 创建图像
Image* create_image(int width, int height, int channels) {
Image* img = (Image*)malloc(sizeof(Image));
img->width = width;
img->height = height;
img->channels = channels;
img->data = (unsigned char*)malloc(width * height * channels);
return img;
}
// 释放图像
void free_image(Image* img) {
if (img) {
free(img->data);
free(img);
}
}
// 灰度化
void grayscale(Image* img) {
if (img->channels != 3) return;
for (int i = 0; i < img->width * img->height; i++) {
int r = img->data[i * 3 + 0];
int g = img->data[i * 3 + 1];
int b = img->data[i * 3 + 2];
int gray = (int)(0.299 * r + 0.587 * g + 0.114 * b);
img->data[i * 3 + 0] = gray;
img->data[i * 3 + 1] = gray;
img->data[i * 3 + 2] = gray;
}
}
// 调整亮度
void adjust_brightness(Image* img, int delta) {
for (int i = 0; i < img->width * img->height * img->channels; i++) {
int val = img->data[i] + delta;
if (val < 0) val = 0;
if (val > 255) val = 255;
img->data[i] = val;
}
}
*/
import "C"
import (
"fmt"
"image"
"image/color"
"image/png"
"os"
"unsafe"
)
// GoImage 包装 C 图像
type GoImage struct {
cimg *C.Image
}
func NewGoImage(width, height int) *GoImage {
return &GoImage{
cimg: C.create_image(C.int(width), C.int(height), 3),
}
}
func (g *GoImage) Free() {
C.free_image(g.cimg)
}
func (g *GoImage) Grayscale() {
C.grayscale(g.cimg)
}
func (g *GoImage) AdjustBrightness(delta int) {
C.adjust_brightness(g.cimg, C.int(delta))
}
func (g *GoImage) SetPixel(x, y int, r, g, b uint8) {
idx := (y*int(g.cimg.width) + x) * int(g.cimg.channels)
data := (*[1 << 30]byte)(unsafe.Pointer(g.cimg.data))
data[idx+0] = r
data[idx+1] = g
data[idx+2] = b
}
func (g *GoImage) ToImage() image.Image {
width := int(g.cimg.width)
height := int(g.cimg.height)
img := image.NewRGBA(image.Rect(0, 0, width, height))
data := (*[1 << 30]byte)(unsafe.Pointer(g.cimg.data))
for y := 0; y < height; y++ {
for x := 0; x < width; x++ {
idx := (y*width + x) * 3
r := data[idx+0]
g := data[idx+1]
b := data[idx+2]
img.Set(x, y, color.RGBA{r, g, b, 255})
}
}
return img
}
func main() {
// 创建 100x100 的图像
img := NewGoImage(100, 100)
defer img.Free()
// 填充渐变色
for y := 0; y < 100; y++ {
for x := 0; x < 100; x++ {
r := uint8(x * 255 / 100)
g := uint8(y * 255 / 100)
b := uint8(128)
img.SetPixel(x, y, r, g, b)
}
}
// 应用滤镜
img.Grayscale()
img.AdjustBrightness(20)
// 保存为 PNG
f, err := os.Create("output.png")
if err != nil {
fmt.Println("Error:", err)
return
}
defer f.Close()
png.Encode(f, img.ToImage())
fmt.Println("图像已保存到 output.png")
}
错误处理
C 函数可能返回错误,需要妥善处理:
package main
/*
#include <errno.h>
#include <string.h>
#include <stdio.h>
int divide(int a, int b, int* result) {
if (b == 0) {
errno = EINVAL;
return -1;
}
*result = a / b;
return 0;
}
*/
import "C"
import (
"fmt"
"syscall"
)
func main() {
var result C.int
// 正常情况
ret := C.divide(10, 2, &result)
if ret == 0 {
fmt.Printf("10 / 2 = %d\n", int(result))
}
// 错误情况
ret = C.divide(10, 0, &result)
if ret != 0 {
// 获取 errno
err := syscall.Errno(C.errno)
fmt.Printf("错误: %v\n", err)
}
}
性能考虑
CGO 调用有开销(大约 100-200 纳秒),因为需要:
- 切换栈(Go 使用分段栈,C 使用连续栈)
- 转换参数
- 保存/恢复寄存器
// 不好:频繁的小调用
for i := 0; i < 1000000; i++ {
C.small_function(C.int(i))
}
// 好:批量处理
C.batch_process((*C.int)(unsafe.Pointer(&data[0])), C.int(len(data)))
最佳实践
- 尽量减少 CGO 调用次数:批量处理数据
- 避免在热路径使用:性能敏感代码用纯 Go
- 封装 C 接口:提供 Go 友好的 API
- 注意内存管理:C 分配的内存必须手动释放
- 处理错误:不要忽略 C 函数的错误码
- 文档化:说明为什么需要 CGO
总结
CGO 是 Go 与 C 世界的桥梁,它让你能够:
- 调用现有的 C 库
- 访问系统底层 API
- 集成遗留代码
但代价是:
- 编译复杂度增加
- 性能开销
- 内存管理复杂
- 交叉编译困难
使用原则:能用纯 Go 解决的,就不要用 CGO。只有当你确实需要调用 C 库或追求极致性能时,才考虑使用 CGO。
记住 Dave Cheney 的话:"CGO 不是 Go"。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。