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.
