Para integrar código de C en tus proyectos de Go, utilizas un mecanismo especial llamado cgo activado mediante la sentencia pseudo-paquete import "C". No es un paquete real de Go, sino una directiva para el compilador. Funciona mediante un proceso de “trampolín”: cuando llamas a una función de C, el runtime de Go debe realizar un cambio de contexto, guardando los registros actuales y cambiando de la pila de la goroutine (que es segmentable y crece dinámicamente) a una pila de sistema estándar. Este diseño es necesario porque el programador de Go (scheduler) no tiene forma de entender cómo el código de C gestiona la memoria o las señales del sistema.
Solo debes recurrir a cgo cuando es estrictamente necesario, principalmente para enlazar con librerías de C maduras y de uso estándar donde reimplementar la lógica en Go sería un esfuerzo prohibitivo o ineficiente, como libssl, sqlite3 o drivers de hardware muy específicos. Si puedes encontrar una implementación nativa en Go (pure Go), úsala siempre.
Si usas cgo de forma indiscriminada, romperás varias ventajas competitivas de Go: la velocidad de compilación se disparará porque ahora necesitas un compilador de C (gcc o clang) en el pipeline; perderás la capacidad de realizar cross-compilation sencilla (necesitarás un toolchain de C para el target específico); y tus binarios dejarán de ser estáticos y puros, ya que dependerán de la libc del sistema operativo anfitrión. Además, prepárate para un impacto severo en el rendimiento: mientras que una función Go pura tiene un costo de llamada de apenas unos pocos nanosegundos, cada cruce de frontera con cgo añade un overhead de entre 60 y 200 nanosegundos debido al cambio de contexto y la gestión de la pila.
package main
/*
#include <stdio.h>
#include <stdlib.h>
// Función C que simula una operación pesada en una librería externa
void procesar_datos_externos(const char* nombre) {
printf("[C] Procesando datos para: %s\n", nombre);
}
*/
import "C"
import (
"fmt"
"unsafe"
)
func main() {
nombreGo := "Gopher_Pro"
// Convertimos el string de Go a un *C.char (C-style string)
// Esto reserva memoria en el heap de C, fuera del control del GC de Go.
cNombre := C.CString(nombreGo)
// Es vital liberar la memoria asignada por C.CString manualmente.
// De lo contrario, tendremos un leak de memoria que el GC de Go no puede limpiar.
defer C.free(unsafe.Pointer(cNombre))
fmt.Println("[Go] Iniciando llamada a C...")
// Llamada a la función de C pasando el puntero
C.procesar_datos_externos(cNombre)
// Para volver a Go, convertimos el puntero de C a un string nativo de Go.
// C.GoString crea una copia de los datos en el heap de Go.
resultadoC := C.GoString(cNombre)
fmt.Printf("[Go] Retorno de C: %s\n", resultadoC)
}
Desglose del ejemplo
En el código anterior, la magia ocurre en la zona del preamble (el comentario justo antes del import "C"), donde definimos la función procesar_datos_externos. Al llamar a C.CString(nombreGo), no solo estamos cambiando el tipo de dato; estamos realizando una llamada de memoria a malloc en el espacio de C. Por eso, la variable cNombre no es un string de Go, sino un puntero *C.char.
Para evitar que el programa consuma toda la RAM de la máquina, usamos defer C.free(unsafe.Pointer(cNombre)). Aquí es donde entra unsafe.Pointer: es el puente que nos permite decirle al compilador “trata este puntero de C como un puntero genérico para que pueda pasarlo a la función free de la librería estándar”.
Al final, C.GoString(cNombre) realiza una operación de copia. Esto es fundamental: Go necesita que los datos de la cadena vivan en su propio mundo, bajo su propio sistema de gestión de memoria y Garbage Collector, para poder trabajar con ellos de forma segura.
El error frecuente
El error más común y peligroso al usar cgo es olvidar liberar la memoria de los tipos que realizan una asignación manual, como C.CString.
// MAL: Esto causa un memory leak persistente
func errorComun() {
for i := 0; i < 1000000; i++ {
// Cada iteración reserva memoria en el heap de C
// que el Garbage Collector de Go NUNCA va a recolectar.
cStr := C.CString("datos_pesados")
C.procesar_datos_externos(cStr)
// Olvidar C.free(unsafe.Pointer(cStr)) aquí matará tu proceso por falta de RAM.
}
}
Dado que la memoria de C no es gestionada por el runtime de Go, un bucle que llame repetidamente a una función con C.CString sin su correspondiente C.free agotará la memoria del sistema operativo sin que el Garbage Collector de Go muestre señales de actividad o latencia.
N° 180