본문으로 건너뛰기

CGo — C 코드와 Go 통합

CGo는 Go 프로그램에서 C 코드를 직접 호출하고, C 프로그램에서 Go 코드를 호출할 수 있게 해주는 메커니즘입니다. 기존 C 라이브러리 재사용, 시스템 콜 직접 접근, 성능이 중요한 저수준 연산에 활용됩니다.


CGo 기본 개념

import "C" 가상 패키지

// main.go
package main

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

// C 함수 직접 정의 가능
int add(int a, int b) {
return a + b;
}

void print_message(const char* msg) {
printf("C에서 출력: %s\n", msg);
}
*/
import "C"

import (
"fmt"
"unsafe"
)

func main() {
// C 함수 호출
result := C.add(10, 20)
fmt.Printf("C add(10, 20) = %d\n", int(result))

// Go 문자열 → C 문자열 변환
msg := C.CString("안녕하세요, CGo!")
defer C.free(unsafe.Pointer(msg)) // 반드시 해제!
C.print_message(msg)

// C 표준 라이브러리 사용
cStr := C.CString("hello world")
defer C.free(unsafe.Pointer(cStr))
length := C.strlen(cStr)
fmt.Printf("문자열 길이: %d\n", int(length))
}
# CGo 빌드 (C 컴파일러 필요: gcc 또는 clang)
go build .
go run .

# CGo 비활성화 (순수 Go 빌드)
CGO_ENABLED=0 go build .

핵심 규칙:

  • import "C" 바로 위 주석이 C 코드 (프리앰블)
  • import "C"와 프리앰블 사이에 빈 줄 없음 (있으면 컴파일 오류)
  • C 문자열은 C.CString() → Go 사용 → C.free() 순서

타입 변환 매핑

package main

/*
#include <stdint.h>

typedef struct {
int x;
int y;
} Point;

Point make_point(int x, int y) {
Point p = {x, y};
return p;
}

double distance(Point a, Point b) {
int dx = a.x - b.x;
int dy = a.y - b.y;
return sqrt((double)(dx*dx + dy*dy));
}
*/
import "C"

import (
"fmt"
"math"
_ "unsafe"
)

func main() {
// 기본 타입 변환
var goInt int = 42
cInt := C.int(goInt) // Go int → C int
backToGo := int(cInt) // C int → Go int
fmt.Printf("변환: %d → %d → %d\n", goInt, cInt, backToGo)

// C 구조체 사용
p1 := C.make_point(0, 0)
p2 := C.make_point(3, 4)

// C 구조체 필드 접근
fmt.Printf("p1: (%d, %d)\n", int(p1.x), int(p1.y))
fmt.Printf("p2: (%d, %d)\n", int(p2.x), int(p2.y))

// C 함수 호출
dist := C.distance(p1, p2)
fmt.Printf("거리: %.2f (기대값: %.2f)\n", float64(dist), math.Sqrt(25))
}

타입 대응표

Go 타입C 타입CGo 타입
intintC.int
int64int64_tC.int64_t
float64doubleC.double
byteunsigned charC.uchar
string (변환 필요)char*C.CString()
[]byte (변환 필요)char*C.CBytes()
unsafe.Pointervoid*직접 캐스팅

외부 C 라이브러리 연동

공유 라이브러리 링크

// sqlite_example.go
package main

/*
#cgo LDFLAGS: -lsqlite3
#cgo CFLAGS: -I/usr/include

#include <sqlite3.h>
#include <stdlib.h>

// 래퍼 함수로 복잡한 콜백 처리
static int exec_simple(sqlite3* db, const char* sql) {
char* errmsg = NULL;
int rc = sqlite3_exec(db, sql, NULL, NULL, &errmsg);
if (errmsg) sqlite3_free(errmsg);
return rc;
}
*/
import "C"

import (
"fmt"
"log"
"unsafe"
)

func openDB(path string) *C.sqlite3 {
cPath := C.CString(path)
defer C.free(unsafe.Pointer(cPath))

var db *C.sqlite3
rc := C.sqlite3_open(cPath, &db)
if rc != C.SQLITE_OK {
log.Fatalf("DB 열기 실패: %d", int(rc))
}
return db
}

func execSQL(db *C.sqlite3, sql string) error {
cSQL := C.CString(sql)
defer C.free(unsafe.Pointer(cSQL))

rc := C.exec_simple(db, cSQL)
if rc != C.SQLITE_OK {
return fmt.Errorf("SQL 실행 실패: %d", int(rc))
}
return nil
}

func main() {
db := openDB(":memory:")
defer C.sqlite3_close(db)

if err := execSQL(db, "CREATE TABLE test (id INTEGER, name TEXT)"); err != nil {
log.Fatal(err)
}
if err := execSQL(db, "INSERT INTO test VALUES (1, 'Go')"); err != nil {
log.Fatal(err)
}
fmt.Println("SQLite 작업 성공!")
}

pkg-config 활용

// openssl_example.go
package main

/*
#cgo pkg-config: libssl libcrypto
#include <openssl/md5.h>
#include <string.h>

void compute_md5(const char* input, unsigned char* output) {
MD5_CTX ctx;
MD5_Init(&ctx);
MD5_Update(&ctx, input, strlen(input));
MD5_Final(output, &ctx);
}
*/
import "C"

import (
"fmt"
"unsafe"
)

func MD5Sum(input string) [16]byte {
cInput := C.CString(input)
defer C.free(unsafe.Pointer(cInput))

var output [16]C.uchar
C.compute_md5(cInput, &output[0])

var result [16]byte
for i, b := range output {
result[i] = byte(b)
}
return result
}

func main() {
hash := MD5Sum("hello world")
fmt.Printf("MD5: %x\n", hash)
// 출력: MD5: 5eb63bbbe01eeed093cb22bb8f5acdc3
}

Go 함수를 C에서 호출 — export

// export_example.go
package main

/*
#include <stdio.h>

// Go 함수를 C에서 호출하는 선언
extern int GoAdd(int a, int b);
extern void GoLog(char* msg);

void c_caller() {
int result = GoAdd(5, 3);
printf("GoAdd(5, 3) = %d\n", result);
GoLog("C에서 Go 함수 호출 성공!");
}
*/
import "C"

import "fmt"

// //export 주석으로 C에 노출
//
//export GoAdd
func GoAdd(a, b C.int) C.int {
return C.int(int(a) + int(b))
}

//export GoLog
func GoLog(msg *C.char) {
fmt.Printf("Go Log: %s\n", C.GoString(msg))
}

func main() {
C.c_caller()
}

주의사항:

  • //export FuncName 주석에서 //export 사이에 공백 없음
  • export된 함수는 C 타입 매개변수와 반환값 사용
  • C.GoString()으로 C 문자열을 Go 문자열로 변환

메모리 관리 패턴

package main

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

// C에서 메모리 할당하는 함수
char* create_buffer(int size) {
char* buf = (char*)malloc(size);
if (buf) memset(buf, 0, size);
return buf;
}

// C에서 할당한 메모리를 C에서 해제
void destroy_buffer(char* buf) {
free(buf);
}
*/
import "C"

import (
"fmt"
"unsafe"
)

// Go 래퍼 타입으로 리소스 관리
type CBuffer struct {
ptr *C.char
size int
}

func NewCBuffer(size int) *CBuffer {
ptr := C.create_buffer(C.int(size))
if ptr == nil {
return nil
}
return &CBuffer{ptr: ptr, size: size}
}

func (b *CBuffer) Write(data string) {
cStr := C.CString(data)
defer C.free(unsafe.Pointer(cStr))
C.strncpy(b.ptr, cStr, C.size_t(b.size-1))
}

func (b *CBuffer) Read() string {
return C.GoString(b.ptr)
}

func (b *CBuffer) Free() {
if b.ptr != nil {
C.destroy_buffer(b.ptr)
b.ptr = nil
}
}

func main() {
buf := NewCBuffer(256)
if buf == nil {
fmt.Println("버퍼 할당 실패")
return
}
defer buf.Free() // 반드시 해제

buf.Write("Hello from CGo buffer!")
fmt.Printf("버퍼 내용: %s\n", buf.Read())
}

메모리 안전 규칙

/*
CGo 메모리 규칙 요약:

1. C.CString() → 반드시 C.free() 호출
2. C.CBytes() → 반드시 C.free() 호출
3. C에서 malloc() → C에서 free() (Go의 GC 관할 아님)
4. Go의 []byte → C 포인터 전달 시 핀(pin) 필요
5. Go 포인터를 C에 저장 금지 (GC가 이동시킬 수 있음)
*/

// 올바른 슬라이스 전달
package main

/*
#include <string.h>

int sum_bytes(char* data, int len) {
int sum = 0;
for (int i = 0; i < len; i++) {
sum += (unsigned char)data[i];
}
return sum;
}
*/
import "C"

import "fmt"

func sumBytes(data []byte) int {
if len(data) == 0 {
return 0
}
// 슬라이스의 첫 번째 요소 포인터 전달 (안전)
result := C.sum_bytes((*C.char)(C.CBytes(data)), C.int(len(data)))
return int(result)
}

func main() {
data := []byte{1, 2, 3, 4, 5}
fmt.Printf("바이트 합계: %d\n", sumBytes(data)) // 15
}

빌드 태그와 조건부 컴파일

// gpu_accelerated.go
//go:build cgo && linux && amd64

package compute

/*
#cgo LDFLAGS: -L/usr/local/cuda/lib64 -lcuda
#cgo CFLAGS: -I/usr/local/cuda/include

#include "gpu_ops.h"
*/
import "C"

func MatMulGPU(a, b []float32, rows, cols int) []float32 {
// GPU 가속 행렬 곱셈
// ...
return nil
}
// gpu_fallback.go
//go:build !cgo || !linux || !amd64

package compute

// CGo 미지원 환경에서의 순수 Go 폴백
func MatMulGPU(a, b []float32, rows, cols int) []float32 {
result := make([]float32, rows*cols)
// CPU 구현...
return result
}
# Makefile — 조건부 CGo 빌드
.PHONY: build-cgo build-pure

build-cgo:
CGO_ENABLED=1 go build -tags cgo ./...

build-pure:
CGO_ENABLED=0 go build ./...

# 크로스 컴파일 (CGo는 크로스 컴파일이 어려움)
# CGO_ENABLED=0일 때만 크로스 컴파일 가능
cross-compile:
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o bin/app-linux .
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -o bin/app.exe .

성능과 오버헤드

CGo 함수 호출에는 순수 Go 호출 대비 ~100ns의 오버헤드가 있습니다.

// benchmark_test.go
package main

import (
"testing"
)

/*
#include <math.h>

double c_sqrt(double x) {
return sqrt(x);
}
*/
import "C"
import "math"

func BenchmarkGoSqrt(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = math.Sqrt(float64(i))
}
}

func BenchmarkCGoSqrt(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = C.c_sqrt(C.double(float64(i)))
}
}

// 결과 예시:
// BenchmarkGoSqrt 1000000000 0.5 ns/op
// BenchmarkCGoSqrt 10000000 120 ns/op ← CGo 오버헤드 ~100-200ns

배치 처리로 오버헤드 최소화

/*
// 나쁜 예: 루프마다 CGo 호출
for _, val := range largeSlice {
C.process_one(C.double(val)) // 매번 ~100ns 오버헤드
}

// 좋은 예: 한 번에 C에 전달
void process_batch(double* data, int len, double* output) {
for (int i = 0; i < len; i++) {
output[i] = sqrt(data[i]);
}
}
*/
import "C"
import "unsafe"

func processBatch(data []float64) []float64 {
output := make([]float64, len(data))
if len(data) == 0 {
return output
}

// 슬라이스 전체를 한 번에 C에 전달 → CGo 오버헤드 1회만 발생
C.process_batch(
(*C.double)(unsafe.Pointer(&data[0])),
C.int(len(data)),
(*C.double)(unsafe.Pointer(&output[0])),
)
return output
}

CGo를 쓰지 말아야 할 때

CGo 사용 결정 기준:

✅ CGo 사용 권장
- 성숙한 C 라이브러리 (OpenSSL, SQLite, FFmpeg 등) 재사용
- 하드웨어 직접 접근 (GPU, 특수 장치 드라이버)
- 레거시 C 코드베이스와의 통합 필수
- 시스템 콜 래퍼 (cgo로만 가능한 OS 기능)

❌ CGo 피해야 할 경우
- 순수 Go로 구현 가능한 경우
- 크로스 컴파일이 필요한 경우 (CGo는 호스트 C 컴파일러 필요)
- 컨테이너/scratch 이미지 배포 (C 런타임 불필요)
- 성능이 중요한 핫패스 (100ns+ 오버헤드)
- Go 도구체인만으로 빌드해야 하는 경우 (CI/CD 단순화)

실전 예제 — 이미지 처리 라이브러리 래핑

// image/vips.go
package image

/*
#cgo pkg-config: vips
#include <vips/vips.h>
#include <stdlib.h>

// 초기화 상태 추적
static int initialized = 0;

int init_vips(const char* program) {
if (initialized) return 0;
int r = vips_init(program);
if (r == 0) initialized = 1;
return r;
}

// 이미지 리사이즈 래퍼
VipsImage* resize_image(const char* filename, int width, int height) {
VipsImage* in;
if (vips_jpegload(filename, &in, NULL)) return NULL;

VipsImage* out;
double scale = (double)width / vips_image_get_width(in);
if (vips_resize(in, &out, scale, NULL)) {
g_object_unref(in);
return NULL;
}
g_object_unref(in);
return out;
}
*/
import "C"

import (
"fmt"
"unsafe"
)

func init() {
name := C.CString("go-vips")
defer C.free(unsafe.Pointer(name))

if rc := C.init_vips(name); rc != 0 {
panic("vips 초기화 실패")
}
}

type Image struct {
vips *C.VipsImage
}

func LoadAndResize(path string, width, height int) (*Image, error) {
cPath := C.CString(path)
defer C.free(unsafe.Pointer(cPath))

vimg := C.resize_image(cPath, C.int(width), C.int(height))
if vimg == nil {
return nil, fmt.Errorf("이미지 리사이즈 실패: %s", path)
}

return &Image{vips: vimg}, nil
}

func (img *Image) Close() {
if img.vips != nil {
C.g_object_unref(C.gpointer(unsafe.Pointer(img.vips)))
img.vips = nil
}
}

핵심 정리

항목내용
import "C"C 코드 인터페이스 가상 패키지
프리앰블import "C" 바로 위 /* ... */ 주석
C.CString()Go string → C string (반드시 C.free() 필요)
C.GoString()C string → Go string
//exportGo 함수를 C에서 호출 가능하게 노출
#cgo LDFLAGSC 라이브러리 링크 플래그
#cgo CFLAGSC 컴파일러 플래그
CGo 오버헤드~100-200ns/호출 (배치 처리로 최소화)
  • CGo 활성화: CGO_ENABLED=1 (기본값), 비활성화: CGO_ENABLED=0
  • C에서 할당한 메모리는 반드시 C에서 해제 (C.free())
  • Go 포인터를 C 구조체에 저장 금지 (GC 충돌)
  • 크로스 컴파일 시 CGo 비활성화 권장