Inspección de memoria en Go: unsafe.Sizeof y Alignof

El paquete unsafe de Go expone funciones intrínsecas que permiten la introspección del diseño de memoria de los tipos de datos, específicamente mediante unsafe.Sizeof, que devuelve el tamaño en bytes que ocupa una expresión en la memoria del sistema. A diferencia de la mayoría de las funciones en Go, las llamadas a este paquete son procesadas directamente por el compilador, lo que permite obtener metadatos sobre la representación física de los tipos sin necesidad de lógica de ejecución compleja.

Este comportamiento existe para facilitar la interoperabilidad con código de bajo nivel, como llamadas al sistema (syscalls) o integración con bibliotecas en C, donde la precisión del layout de memoria es crítica. Resuelve la opacidad que suele acompañar a los lenguajes con recolección de basura, proporcionando una “escotilla de escape” que permite a los desarrolladores optimizar el uso de caché y minimizar el consumo de memoria mediante el conocimiento exacto de cómo el compilador organiza los datos.

La tríada de inspección de memoria

El análisis del diseño de un tipo compuesto se apoya en tres pilares: Sizeof, Alignof y Offsetof. La función unsafe.Sizeof toma una expresión de cualquier tipo y devuelve su tamaño, considerando únicamente la estructura inmediata del tipo. Por otro lado, unsafe.Alignof reporta los requisitos de alineamiento de un tipo, un valor que determina en qué múltiplos de direcciones de memoria debe comenzar una variable. Finalmente, unsafe.Offsetof se utiliza exclusivamente con campos de estructuras para determinar la distancia en bytes desde el inicio del struct hasta el campo en cuestión.

Internamente, el compilador utiliza estas reglas para insertar bytes de relleno (padding) y asegurar que cada campo esté alineado según su arquitectura. Es vital comprender que Sizeof no mide el contenido dinámico de tipos por referencia; para un slice o un string, siempre devolverá el tamaño de la cabecera (header), independientemente de la cantidad de elementos que contenga el subyacente backing array.

package main

import (
	"fmt"
	"unsafe"
)

type Layout struct {
	Activo bool    // Tamaño: 1, Alineamiento: 1
	ID     int64   // Tamaño: 8, Alineamiento: 8
	Valor  float32 // Tamaño: 4, Alineamiento: 4
}

func main() {
	var l Layout

	// Sizeof reporta el tamaño total incluyendo el padding insertado
	fmt.Println(unsafe.Sizeof(l)) // → 24

	// Alignof indica el factor de alineamiento del tipo (el máximo de sus campos)
	fmt.Println(unsafe.Alignof(l)) // → 8

	// Offsetof revela dónde el compilador insertó padding
	fmt.Println(unsafe.Offsetof(l.Activo)) // → 0
	fmt.Println(unsafe.Offsetof(l.ID))     // → 8 (el compilador saltó 7 bytes de padding)
	fmt.Println(unsafe.Offsetof(l.Valor))  // → 16
}
Go

El comportamiento más contraintuitivo se observa en el campo ID del ejemplo anterior. Aunque el campo Activo solo ocupa 1 byte, el campo ID comienza en el offset 8 debido a que su alineamiento requiere una dirección divisible por 8, dejando un hueco de memoria inutilizada que incrementa el tamaño total del struct.

Evaluación en tiempo de compilación y constantes

Un comportamiento no obvio de estas funciones es que, en muchos contextos, el compilador de Go las trata como expresiones constantes. Si el argumento pasado a unsafe.Sizeof no es un valor cuya estructura dependa del runtime (lo cual es cierto para casi todos los tipos en Go), el resultado de la función puede utilizarse para definir el tamaño de un array o como valor de una constante global. Esto significa que la inspección del layout ocurre efectivamente durante la fase de compilación, permitiendo que el binario final ya contenga los valores numéricos calculados.

El tamaño estático de tipos dinámicos

Un edge case real que genera confusión es la medición de interfaces y tipos puntero. Al aplicar unsafe.Sizeof a una variable de tipo interface{}, el resultado será siempre 16 bytes (en arquitecturas de 64 bits), correspondientes a los dos punteros que forman la estructura eface (el puntero al tipo y el puntero a los datos).

De igual forma, medir un puntero siempre devolverá 8 bytes, sin importar si apunta a un int8 o a una estructura de un gigabyte. Esto subraya que el paquete unsafe solo ve la “huella” inmediata en el stack o el heap donde reside la variable, ignorando cualquier indirección posterior hacia la memoria que el puntero referencia. Si se requiere calcular el tamaño total de una estructura recursiva, unsafe.Sizeof por sí solo resulta insuficiente y requiere de una implementación manual que recorra los punteros.

  • Módulo: Sistema de Tipos
  • Artículo número: #33

Dejar un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Scroll al inicio