CGO 入门:Go 与 C 的桥梁

学习使用 CGO 在 Go 中调用 C 代码,理解 Go 与 C 的互操作机制

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()
}

注意几个关键点:

  1. C 代码放在 /* */ 注释中
  2. import "C" 必须紧跟在 C 代码注释后面
  3. import "C" 和其他 import 之间不能有空行
  4. 通过 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)))

最佳实践

  1. 尽量减少 CGO 调用次数:批量处理数据
  2. 避免在热路径使用:性能敏感代码用纯 Go
  3. 封装 C 接口:提供 Go 友好的 API
  4. 注意内存管理:C 分配的内存必须手动释放
  5. 处理错误:不要忽略 C 函数的错误码
  6. 文档化:说明为什么需要 CGO

总结

CGO 是 Go 与 C 世界的桥梁,它让你能够:

  • 调用现有的 C 库
  • 访问系统底层 API
  • 集成遗留代码

但代价是:

  • 编译复杂度增加
  • 性能开销
  • 内存管理复杂
  • 交叉编译困难

使用原则:能用纯 Go 解决的,就不要用 CGO。只有当你确实需要调用 C 库或追求极致性能时,才考虑使用 CGO。

记住 Dave Cheney 的话:"CGO 不是 Go"。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页