C Memory Management для Go разработчиков: malloc, calloc, realloc и free
Если вы Go разработчик, привыкший к автоматическому управлению памятью, работа с языком C может показаться архаичной. Но понимание malloc/free критично для CGO и низкоуровневой оптимизации.
Основные функции
В C память управляется вручную через четыре функции:
void* malloc(size_t size); // Выделяет память
void* calloc(size_t num, size_t size); // Выделяет и обнуляет
void* realloc(void* ptr, size_t size); // Изменяет размер
void free(void* ptr); // Освобождает память
В Go это делает GC автоматически:
// Go - память управляется автоматически
data := make([]byte, 1024)
// Память освободится сама, когда data выйдет из scope
malloc: базовое выделение
#include <stdlib.h>
#include <stdio.h>
int main() {
// Выделяем память для 10 int
int* arr = (int*)malloc(10 * sizeof(int));
if (arr == NULL) {
fprintf(stderr, "Memory allocation failed\n");
return 1;
}
// Используем память
for (int i = 0; i < 10; i++) {
arr[i] = i * 2;
}
// ОБЯЗАТЕЛЬНО освобождаем
free(arr);
return 0;
}
Эквивалент в Go:
arr := make([]int, 10)
for i := range arr {
arr[i] = i * 2
}
// free не нужен - GC сделает сам
calloc: выделение с обнулением
// malloc не инициализирует память - там мусор
int* arr1 = (int*)malloc(10 * sizeof(int));
// arr1[0] может быть любым значением!
// calloc обнуляет память
int* arr2 = (int*)calloc(10, sizeof(int));
// arr2[0] гарантированно 0
В Go make всегда инициализирует нулями:
arr := make([]int, 10) // Все элементы = 0
realloc: изменение размера
int* arr = (int*)malloc(5 * sizeof(int));
// Нужно больше места
arr = (int*)realloc(arr, 10 * sizeof(int));
if (arr == NULL) {
// realloc не освободил старую память при ошибке
// но мы потеряли указатель!
}
// Правильно:
int* new_arr = (int*)realloc(arr, 10 * sizeof(int));
if (new_arr == NULL) {
free(arr); // Освобождаем старую память
return 1;
}
arr = new_arr;
В Go append делает это автоматически:
arr := make([]int, 5)
arr = append(arr, make([]int, 5)...) // Расширяется автоматически
Использование в CGO
Вот где это становится важным для Go разработчиков:
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)) // ВАЖНО!
cResult := C.create_string(cInput)
defer C.free(unsafe.Pointer(cResult)) // ВАЖНО!
return C.GoString(cResult)
}
Типичные ошибки
1. Memory Leak
void leak_example() {
int* data = (int*)malloc(100 * sizeof(int));
// Забыли free(data)
} // Память утекла навсегда
В Go GC предотвращает это, но в CGO:
func LeakExample() {
cData := C.malloc(C.size_t(100))
// Забыли C.free(unsafe.Pointer(cData))
} // Memory leak!
2. Double Free
int* data = (int*)malloc(10 * sizeof(int));
free(data);
free(data); // ОШИБКА: undefined behavior
3. Use After Free
int* data = (int*)malloc(10 * sizeof(int));
free(data);
data[0] = 42; // ОШИБКА: используем освобожденную память
4. Неправильный размер
// НЕПРАВИЛЬНО
int* arr = (int*)malloc(10); // Выделили 10 байт, а не 10 int!
// ПРАВИЛЬНО
int* arr = (int*)malloc(10 * sizeof(int));
Паттерны для CGO
Обертка с автоматической очисткой
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)
}
}
Передача Go slice в C
func ProcessData(data []byte) error {
// Выделяем C память
cData := C.malloc(C.size_t(len(data)))
if cData == nil {
return fmt.Errorf("allocation failed")
}
defer C.free(cData)
// Копируем Go данные в C память
C.memcpy(cData, unsafe.Pointer(&data[0]), C.size_t(len(data)))
// Вызываем C функцию
C.process_buffer((*C.char)(cData), C.int(len(data)))
return nil
}
Производительность
Сравнение malloc vs Go allocator:
// Benchmark C malloc через 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)
}
}
Результаты:
BenchmarkCMalloc-8 5000000 280 ns/op
BenchmarkGoAlloc-8 20000000 85 ns/op
Go allocator быстрее для мелких аллокаций благодаря per-goroutine кешам.
Отладка утечек памяти
Valgrind для C кода
valgrind --leak-check=full ./program
AddressSanitizer
gcc -fsanitize=address -g program.c -o program
./program
В CGO
// Включаем race detector и memory sanitizer
// go build -race -msan
Когда использовать malloc в Go проектах
- Интеграция с C библиотеками: неизбежно при CGO
- Долгоживущие буферы: если GC создает проблемы
- Взаимодействие с системными API: требующими C-style память
- Оптимизация: в редких случаях для контроля аллокаций
Практический пример: 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]
}
Заключение
Для Go разработчика malloc/free — это:
- Необходимость при работе с CGO
- Источник потенциальных багов (утечки, use-after-free)
- Иногда инструмент оптимизации
Ключевые правила:
- Каждый malloc требует free
- Используйте defer для гарантированной очистки
- Рассмотрите runtime.SetFinalizer для автоматизации
- Предпочитайте Go аллокатор, где возможно
Понимание C memory management делает вас лучшим Go разработчиком, особенно при работе с системным кодом и CGO.
Дополнительные ресурсы: