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 타입 |
|---|---|---|
int | int | C.int |
int64 | int64_t | C.int64_t |
float64 | double | C.double |
byte | unsigned char | C.uchar |
string (변환 필요) | char* | C.CString() |
[]byte (변환 필요) | char* | C.CBytes() |
unsafe.Pointer | void* | 직접 캐스팅 |
외부 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 |
//export | Go 함수를 C에서 호출 가능하게 노출 |
#cgo LDFLAGS | C 라이브러리 링크 플래그 |
#cgo CFLAGS | C 컴파일러 플래그 |
| CGo 오버헤드 | ~100-200ns/호출 (배치 처리로 최소화) |
- CGo 활성화:
CGO_ENABLED=1(기본값), 비활성화:CGO_ENABLED=0 - C에서 할당한 메모리는 반드시 C에서 해제 (
C.free()) - Go 포인터를 C 구조체에 저장 금지 (GC 충돌)
- 크로스 컴파일 시 CGo 비활성화 권장