cgo en Go: llamar a código C desde Go, importar librerías C y cuándo evitarlo

cgo permite llamar a código C desde Go mediante la directiva especial import "C". El bloque de comentario inmediatamente anterior a esa directiva se trata como código C y se compila junto al programa. Es la forma estándar de usar librerías C desde Go cuando no existe un binding nativo.

Llamada básica a funciones C

package main

// #include <stdio.h>
// #include <string.h>
//
// int sumar(int a, int b) {
//     return a + b;
// }
//
// int longitud_cadena(const char* s) {
//     return strlen(s);
// }
import "C"

import "fmt"

func main() {
    resultado := C.sumar(3, 4)
    fmt.Println("Suma:", int(resultado)) // 7

    cStr := C.CString("hola mundo")
    defer C.free(unsafe.Pointer(cStr))
    longitud := C.longitud_cadena(cStr)
    fmt.Println("Longitud:", int(longitud)) // 10
}

Conversión de strings con C.CString y C.GoString

import (
    "C"
    "unsafe"
    "fmt"
)

func llamarConString(s string) string {
    // Go ? C: aloca memoria en el heap de C, hay que liberarla
    cStr := C.CString(s)
    defer C.free(unsafe.Pointer(cStr))

    // Llamar a función C que devuelve un string
    resultado := C.some_c_function(cStr)

    // C ? Go: copia el contenido al heap de Go
    return C.GoString(resultado)
}

// Convertir []byte directamente
func byteSliceAC(b []byte) *C.char {
    return (*C.char)(unsafe.Pointer(&b[0]))
}

Pasar structs entre Go y C

// // typedef struct {
// //     int x;
// //     int y;
// // } Punto;
// //
// // Punto mover(Punto p, int dx, int dy) {
// //     Punto resultado = {p.x + dx, p.y + dy};
// //     return resultado;
// // }
import "C"

tipo PuntoGo struct {
    X, Y int
}

func moverPunto(p PuntoGo, dx, dy int) PuntoGo {
    cp := C.Punto{x: C.int(p.X), y: C.int(p.Y)}
    resultado := C.mover(cp, C.int(dx), C.int(dy))
    return PuntoGo{X: int(resultado.x), Y: int(resultado.y)}
}

Enlazar librerías externas con LDFLAGS

Para enlazar librerías C del sistema usa las directivas de cgo en el preámbulo:

// #cgo LDFLAGS: -lz -lm
// #cgo CFLAGS: -O2 -I/usr/local/include
// #cgo linux LDFLAGS: -ldl
// #cgo darwin LDFLAGS: -framework CoreFoundation
//
// #include <zlib.h>
import "C"

func comprimirNivel(datos []byte, nivel int) ([]byte, error) {
    // Usar la función compress() de zlib
    destLen := C.uLong(len(datos) * 2)
    dest := make([]byte, destLen)
    ret := C.compress2(
        (*C.Bytef)(&dest[0]),
        &destLen,
        (*C.Bytef)(&datos[0]),
        C.uLong(len(datos)),
        C.int(nivel),
    )
    if ret != C.Z_OK {
        return nil, fmt.Errorf("error de compresión: %d", ret)
    }
    return dest[:destLen], nil
}

Exportar funciones Go a C

Con el comentario //export puedes hacer que una función Go sea visible desde C:

package main

import "C"
import "fmt"

//export GoHola
func GoHola(nombre *C.char) *C.char {
    n := C.GoString(nombre)
    resultado := "Hola desde Go, " + n
    return C.CString(resultado) // el caller es responsable de liberar
}

//export GoSumar
func GoSumar(a, b C.int) C.int {
    return a + b
}

El overhead real de cgo y cuándo evitarlo

Cada llamada a través de cgo tiene un coste fijo de aproximadamente 60-100 ns, frente a los 1-3 ns de una llamada Go normal. Este overhead se debe a que cgo tiene que cambiar entre la pila de Go y la pila de C, y sincronizar el garbage collector.

// Benchmark: cgo overhead
// BenchmarkGoFunc-8        1000000000    0.8 ns/op
// BenchmarkCgoFunc-8          20000000   75.0 ns/op

// Minimiza el número de llamadas cgo agrupando trabajo:
// MAL: llamar a C por cada elemento
for _, v := range valores {
    C.procesar(C.int(v)) // 75ns x N llamadas
}

// BIEN: pasar el slice completo con una sola llamada
ptr := (*C.int)(unsafe.Pointer(&valores[0]))
C.procesar_batch(ptr, C.int(len(valores))) // una sola llamada

Evita cgo cuando existe un binding Go puro que cubre tus necesidades. El coste no es solo de rendimiento: cgo desactiva la compilación cruzada simple (CGO_ENABLED=0 genera binarios estáticos sin dependencias externas), complica el debugging y hace que go test -race no detecte data races en el código C.

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP