本文最后更新于:February 25, 2025 pm

本文作者:[wangwenhai] # 概要:Golang+CGO技术的应用。

在Go语言开发中,有时候需要借助C语言已有的库来实现一些功能,或者使用一些底层的系统调用,这时Cgo就派上了用场。Cgo是Go语言提供的一种机制,它允许Go代码调用C代码,从而将Go语言的高效并发和C语言的底层性能优势结合起来。本文将详细介绍Cgo的基本语法、类型转换以及实际应用。

Cgo技术的应用

一、引言

在Go语言开发中,有时候需要借助C语言已有的库来实现一些功能,或者使用一些底层的系统调用,这时Cgo就派上了用场。Cgo是Go语言提供的一种机制,它允许Go代码调用C代码,从而将Go语言的高效并发和C语言的底层性能优势结合起来。本文将详细介绍Cgo的基本语法、类型转换以及实际应用。

二、Cgo基本语法

2.1 简单示例

下面是一个简单的Cgo示例,展示了如何在Go代码中调用C函数:

package main

/*
#include <stdio.h>

void printHello() {
    printf("Hello from C!\n");
}
*/
import "C"

func main() {
    C.printHello()
}

在这个示例中,我们在Go代码文件中使用注释块 /* ... */ 包含了C代码,其中定义了一个简单的函数 printHello。然后通过 import "C" 引入Cgo支持,就可以在Go代码中直接调用这个C函数。

2.2 注意事项

  • 注释块位置:包含C代码的注释块必须紧跟在 package 声明之后,并且在 import "C" 之前。
  • 编译环境:Cgo依赖于C编译器,因此需要确保系统中已经安装了合适的C编译器(如GCC)。

三、Cgo中的类型转换

3.1 基本数据类型转换

C和Go有各自的基本数据类型,在使用Cgo时需要进行相应的转换。以下是一些常见的基本数据类型转换示例:

C 类型 Go 类型
int C.int 转换为 intgoInt := int(cInt)
int 转换为 C.intcInt := C.int(goInt)
float C.float 转换为 float32goFloat := float32(cFloat)
float32 转换为 C.floatcFloat := C.float(goFloat)
double C.double 转换为 float64goDouble := float64(cDouble)
float64 转换为 C.doublecDouble := C.double(goDouble)
char C.char 转换为 bytegoByte := byte(cChar)
byte 转换为 C.charcChar := C.char(goByte)

3.2 字符串类型转换

C语言使用以 '\0' 结尾的字符数组来表示字符串,而Go使用 string 类型。在Cgo中进行字符串转换时,需要注意内存管理。

3.2.1 Go字符串转C字符串

package main

/*
#include <stdio.h>
#include <stdlib.h>

void printCString(const char* str) {
    printf("C received: %s\n", str);
}
*/
import "C"
import (
    "unsafe"
)

func main() {
    goStr := "Hello from Go!"
    cStr := C.CString(goStr)
    defer C.free(unsafe.Pointer(cStr))
    C.printCString(cStr)
}

在这个示例中,使用 C.CString 函数将Go字符串转换为C字符串。需要注意的是,C.CString 会分配新的内存,因此在使用完后需要调用 C.free 释放内存,避免内存泄漏。

3.2.2 C字符串转Go字符串

package main

/*
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

char* getCString() {
    char* str = (char*)malloc(12 * sizeof(char));
    strcpy(str, "Hello from C");
    return str;
}
*/
import "C"
import (
    "unsafe"
)

func main() {
    cStr := C.getCString()
    defer C.free(unsafe.Pointer(cStr))
    goStr := C.GoString(cStr)
    println(goStr)
}

这里使用 C.GoString 函数将C字符串转换为Go字符串。同样,由于C字符串是动态分配的内存,使用完后需要释放。

3.3 指针类型转换

在Cgo中,指针类型的转换需要特别小心,因为涉及到内存管理和指针操作。

3.3.1 Go指针转C指针

package main

/*
#include <stdio.h>

void printIntPointer(int* ptr) {
    printf("Value at pointer: %d\n", *ptr);
}
*/
import "C"
import (
    "unsafe"
)

func main() {
    goInt := 42
    cPtr := (*C.int)(unsafe.Pointer(&goInt))
    C.printIntPointer(cPtr)
}

在这个示例中,使用 unsafe.Pointer 作为中间类型,将Go整数的指针转换为C整数的指针。

3.3.2 C指针转Go指针

package main

/*
#include <stdio.h>
#include <stdlib.h>

int* getIntPointer() {
    int* ptr = (int*)malloc(sizeof(int));
    *ptr = 100;
    return ptr;
}
*/
import "C"
import (
    "unsafe"
)

func main() {
    cPtr := C.getIntPointer()
    defer C.free(unsafe.Pointer(cPtr))
    goPtr := (*int)(unsafe.Pointer(cPtr))
    println(*goPtr)
}

这里将C整数指针转换为Go整数指针,同样要注意内存释放。

四、Cgo的实际应用场景

4.1 调用系统库

在某些情况下,Go语言可能没有直接提供某些系统功能的支持,这时可以通过Cgo调用C语言的系统库来实现。例如,在Linux系统中调用 getuid 函数获取当前用户的UID:

package main

/*
#include <unistd.h>
*/
import "C"
import "fmt"

func main() {
    uid := C.getuid()
    fmt.Printf("Current user ID: %d\n", int(uid))
}

4.2 复用已有的C库

如果已经有一些成熟的C库,不想重新用Go语言实现,可以使用Cgo在Go项目中复用这些库。例如,使用C语言的OpenSSL库进行加密操作:

package main

/*
#include <openssl/md5.h>
#include <stdio.h>
#include <string.h>

void md5(const char* input, unsigned char* output) {
    MD5_CTX md5Context;
    MD5_Init(&md5Context);
    MD5_Update(&md5Context, input, strlen(input));
    MD5_Final(output, &md5Context);
}
*/
import "C"
import (
    "fmt"
    "unsafe"
)

func main() {
    input := "Hello, World!"
    cInput := C.CString(input)
    defer C.free(unsafe.Pointer(cInput))

    var output [16]byte
    C.md5(cInput, (*C.uchar)(&output[0]))

    for i := 0; i < 16; i++ {
        fmt.Printf("%02x", output[i])
    }
    fmt.Println()
}

实战:3轴加速度传感器IIC协议读取数据

畅维通达EN系列网关是一款面向工业、农业、交通运输、科研等领域,集数据采集、通讯、边缘计算于一体的通用设备。搭载了LIS3DHTR三轴加速度传感器,通过IIC协议进行数据读取。
三轴的预设场景:

  • 电梯运行异常检测
  • 贵重物品异常挪动
  • 车载异常速度检测

文件列表

  • lis3dhtr.h

    #ifndef LIS3DHTR_H
    #define LIS3DHTR_H
    
    #include <stdint.h>
    
    // 翻译加速度值
    double _translateAccValue(uint8_t data);
    
    // 四舍五入到指定精度
    double roundToPrecision(double value, int precisionFactor);
    
    // 读取加速度数据
    void readAcceleration(double *xData, double *yData, double *zData);
    
    #endif
  • lis3dhtr.c

    #include <stdio.h>
    #include <stdlib.h>
    #include <fcntl.h>
    #include <unistd.h>
    #include <linux/i2c-dev.h>
    #include <sys/ioctl.h>
    #include "lis3dhtr.h"
    
    #define LIS3DHTR_ADDR 24
    #define DEVICE_ID_REG 0x0F
    #define CTRL_REG0 0x1E
    #define OUT_X_MSB_REG 0x29
    #define OUT_X_LSB_REG 0x28
    #define OUT_Y_MSB_REG 0x2B
    #define OUT_Y_LSB_REG 0x2A
    #define OUT_Z_MSB_REG 0x2D
    #define OUT_Z_LSB_REG 0x2C
    #define CTRL_REG1_REG 0x20
    #define CTRL_REG4_REG 0x23
    
    // 翻译加速度值
    double _translateAccValue(uint8_t data) {
        double resAcc = 0;
        if (data & 0x80) { // sign, < 0
            resAcc = ((0x7F - (data & 0x7F) + 1) / 64.0) * (-1);
        } else { // > 0
            resAcc = (double)data / 64.0;
        }
        return resAcc;
    }
    
    // 四舍五入到指定精度
    double roundToPrecision(double value, int precisionFactor) {
        return (double)((int)(value * precisionFactor + 0.5)) / precisionFactor;
    }
    
    // 读取加速度数据
    void readAcceleration(double *xData, double *yData, double *zData) {
        int file;
        char *filename = "/dev/i2c-1";
        if ((file = open(filename, O_RDWR)) < 0) {
            perror("Failed to open the i2c bus");
            return;
        }
    
        if (ioctl(file, I2C_SLAVE, LIS3DHTR_ADDR) < 0) {
            perror("Failed to acquire bus access and/or talk to slave");
            close(file);
            return;
        }
    
        // 0. test
        uint8_t rawData;
        if (read(file, &rawData, 1) != 1) {
            perror("Failed to read from the i2c bus");
            close(file);
            return;
        }
    
        // 1. set Data rate and low-power mode
        uint8_t ctrlData = 0x9F; // Low-power mode (5.376 kHz)
        if (write(file, &ctrlData, 1) != 1) {
            perror("Failed to write to the i2c bus");
            close(file);
            return;
        }
    
        // 2. set Full-scale and high-resolution disabled
        ctrlData = 0x00;
        if (write(file, &ctrlData, 1) != 1) {
            perror("Failed to write to the i2c bus");
            close(file);
            return;
        }
    
        // 3. read acceleration data
        uint8_t buffer[5];
        if (read(file, buffer, 5) != 5) {
            perror("Failed to read acceleration data");
            close(file);
            return;
        }
    
        close(file);
    
        const int precisionFactor = 10000;
        *xData = roundToPrecision(_translateAccValue(buffer[0]), precisionFactor);
        *yData = roundToPrecision(_translateAccValue(buffer[2]), precisionFactor);
        *zData = roundToPrecision(_translateAccValue(buffer[4]), precisionFactor);
    }
  • lis3dhtr.go

    package lis3dhtr
    
    /*
    #include "lis3dhtr.h"
    */
    import "C"
    import "unsafe"
    
    // AccelerationData 存储加速度数据
    type AccelerationData struct {
        X float64
        Y float64
        Z float64
    }
    
    // ReadAcceleration 读取加速度数据
    func ReadAcceleration() AccelerationData {
        var x, y, z C.double
        C.readAcceleration(&x, &y, &z)
    
        return AccelerationData{
            X: float64(x),
            Y: float64(y),
            Z: float64(z),
        }
    }
  • main.go

    package main
    
    import (
    	"fmt"
    )
    
    func main() {
    	data := ReadAcceleration()
    	fmt.Printf("X: %.4f, Y: %.4f, Z: %.4f\n", data.X, data.Y, data.Z)
    }