C Memory Management for Go Developers: malloc, calloc, realloc and free
If you’re a Go developer used to automatic memory management, working with C might feel archaic. But understanding malloc/free is critical for CGO and low-level optimization.
Core Functions
In C, memory is managed manually through four functions:
void* malloc(size_t size); // Allocates memory
void* calloc(size_t num, size_t size); // Allocates and zeros
void* realloc(void* ptr, size_t size); // Resizes allocation
void free(void* ptr); // Frees memory
In Go, GC does this automatically:
// Go - memory managed automatically
data := make([]byte, 1024)
// Memory freed automatically when data goes out of scope
malloc: Basic Allocation
#include <stdlib.h>
#include <stdio.h>
int main() {
// Allocate memory for 10 ints
int* arr = (int*)malloc(10 * sizeof(int));
if (arr == NULL) {
fprintf(stderr, "Memory allocation failed\n");
return 1;
}
// Use memory
for (int i = 0; i < 10; i++) {
arr[i] = i * 2;
}
// MUST free
free(arr);
return 0;
}
Go equivalent:
arr := make([]int, 10)
for i := range arr {
arr[i] = i * 2
}
// No free needed - GC handles it
calloc: Allocation with Zeroing
// malloc doesn't initialize - contains garbage
int* arr1 = (int*)malloc(10 * sizeof(int));
// arr1[0] could be any value!
// calloc zeros memory
int* arr2 = (int*)calloc(10, sizeof(int));
// arr2[0] guaranteed to be 0
In Go, make always initializes to zero:
arr := make([]int, 10) // All elements = 0
realloc: Resizing
int* arr = (int*)malloc(5 * sizeof(int));
// Need more space
arr = (int*)realloc(arr, 10 * sizeof(int));
if (arr == NULL) {
// realloc didn't free old memory on error
// but we lost the pointer!
}
// Correct way:
int* new_arr = (int*)realloc(arr, 10 * sizeof(int));
if (new_arr == NULL) {
free(arr); // Free old memory
return 1;
}
arr = new_arr;
In Go, append does this automatically:
arr := make([]int, 5)
arr = append(arr, make([]int, 5)...) // Grows automatically
Using in CGO
This is where it matters for Go developers:
package main
/*
#include <stdlib.h>
#include <string.h>
char* create_string(const char* input) {
size_t len = strlen(input);
char* result = (char*)malloc(len + 1);
if (result != NULL) {
strcpy(result, input);
}
return result;
}
*/
import "C"
import "unsafe"
func CreateString(input string) string {
cInput := C.CString(input)
defer C.free(unsafe.Pointer(cInput)) // IMPORTANT!
cResult := C.create_string(cInput)
defer C.free(unsafe.Pointer(cResult)) // IMPORTANT!
return C.GoString(cResult)
}
Common Mistakes
1. Memory Leak
void leak_example() {
int* data = (int*)malloc(100 * sizeof(int));
// Forgot free(data)
} // Memory leaked forever
In Go, GC prevents this, but in CGO:
func LeakExample() {
cData := C.malloc(C.size_t(100))
// Forgot C.free(unsafe.Pointer(cData))
} // Memory leak!
2. Double Free
int* data = (int*)malloc(10 * sizeof(int));
free(data);
free(data); // ERROR: undefined behavior
3. Use After Free
int* data = (int*)malloc(10 * sizeof(int));
free(data);
data[0] = 42; // ERROR: using freed memory
4. Wrong Size
// WRONG
int* arr = (int*)malloc(10); // Allocated 10 bytes, not 10 ints!
// CORRECT
int* arr = (int*)malloc(10 * sizeof(int));
Patterns for CGO
Wrapper with Automatic Cleanup
type CBuffer struct {
ptr unsafe.Pointer
}
func NewCBuffer(size int) *CBuffer {
ptr := C.malloc(C.size_t(size))
if ptr == nil {
return nil
}
buf := &CBuffer{ptr: ptr}
runtime.SetFinalizer(buf, func(b *CBuffer) {
C.free(b.ptr)
})
return buf
}
func (b *CBuffer) Free() {
if b.ptr != nil {
C.free(b.ptr)
b.ptr = nil
runtime.SetFinalizer(b, nil)
}
}
Passing Go Slice to C
func ProcessData(data []byte) error {
// Allocate C memory
cData := C.malloc(C.size_t(len(data)))
if cData == nil {
return fmt.Errorf("allocation failed")
}
defer C.free(cData)
// Copy Go data to C memory
C.memcpy(cData, unsafe.Pointer(&data[0]), C.size_t(len(data)))
// Call C function
C.process_buffer((*C.char)(cData), C.int(len(data)))
return nil
}
Performance
Comparing malloc vs Go allocator:
// Benchmark C malloc via CGO
func BenchmarkCMalloc(b *testing.B) {
for i := 0; i < b.N; i++ {
ptr := C.malloc(1024)
C.free(ptr)
}
}
// Benchmark Go allocator
func BenchmarkGoAlloc(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = make([]byte, 1024)
}
}
Results:
BenchmarkCMalloc-8 5000000 280 ns/op
BenchmarkGoAlloc-8 20000000 85 ns/op
Go allocator is faster for small allocations thanks to per-goroutine caches.
Debugging Memory Leaks
Valgrind for C Code
valgrind --leak-check=full ./program
AddressSanitizer
gcc -fsanitize=address -g program.c -o program
./program
In CGO
// Enable race detector and memory sanitizer
// go build -race -msan
When to Use malloc in Go Projects
- C Library Integration: Inevitable with CGO
- Long-lived Buffers: If GC creates issues
- System API Interaction: Requiring C-style memory
- Optimization: Rare cases for allocation control
Practical Example: Image Buffer
package main
/*
#include <stdlib.h>
#include <string.h>
typedef struct {
unsigned char* data;
int width;
int height;
} Image;
Image* create_image(int width, int height) {
Image* img = (Image*)malloc(sizeof(Image));
if (img == NULL) return NULL;
img->width = width;
img->height = height;
img->data = (unsigned char*)calloc(width * height * 4, 1);
if (img->data == NULL) {
free(img);
return NULL;
}
return img;
}
void free_image(Image* img) {
if (img != NULL) {
free(img->data);
free(img);
}
}
*/
import "C"
import "unsafe"
type Image struct {
cImg *C.Image
}
func NewImage(width, height int) *Image {
cImg := C.create_image(C.int(width), C.int(height))
if cImg == nil {
return nil
}
img := &Image{cImg: cImg}
runtime.SetFinalizer(img, func(i *Image) {
i.Free()
})
return img
}
func (i *Image) Free() {
if i.cImg != nil {
C.free_image(i.cImg)
i.cImg = nil
}
}
func (i *Image) Data() []byte {
size := int(i.cImg.width * i.cImg.height * 4)
return (*[1 << 30]byte)(unsafe.Pointer(i.cImg.data))[:size:size]
}
Conclusion
For Go developers, malloc/free is:
- Necessary when working with CGO
- Source of potential bugs (leaks, use-after-free)
- Sometimes an optimization tool
Key rules:
- Every malloc requires free
- Use defer for guaranteed cleanup
- Consider runtime.SetFinalizer for automation
- Prefer Go allocator where possible
Understanding C memory management makes you a better Go developer, especially when working with system code and CGO.
Additional Resources: