Los tipos numéricos de arquitectura en Go, específicamente int y uint, son tipos de datos cuya precisión y rango se determinan en tiempo de compilación según la palabra nativa del procesador de destino. A diferencia de los tipos de tamaño fijo, un int puede ocupar 32 bits en sistemas embebidos o legacy y 64 bits en infraestructuras modernas de servidor, manteniendo siempre la eficiencia de procesamiento al alinearse con el tamaño del registro de la CPU.
Go implementa este comportamiento para maximizar la eficiencia en el acceso a memoria y la aritmética de punteros. Al utilizar el tamaño natural del procesador, se evitan ciclos adicionales de CPU que ocurrirían al emular tipos de 64 bits en arquitecturas de 32 bits. Este enfoque resuelve el compromiso entre rendimiento y portabilidad, permitiendo que el compilador optimice el uso de registros mientras mantiene un sistema de tipos estrictamente definido.
En la especificación de Go, int y uint son tipos distintos de sus contrapartes de tamaño fijo (int32, int64, etc.). Aunque en un sistema de 64 bits el underlying type de int coincida en tamaño con int64, la regla de assignability prohíbe el intercambio directo sin una conversión explícita. Esta rigidez previene errores de truncamiento accidentales al mover código entre diferentes plataformas. Por otro lado, existe el tipo uintptr, un entero sin signo diseñado para almacenar los bits de una dirección de memoria. Su importancia radica en la capacidad de realizar aritmética de direcciones, algo prohibido con punteros tipados (*T).
package main
import (
"fmt"
"reflect"
"unsafe"
)
func main() {
var i int = 42
var i64 int64 = 42
// Comparación de tamaños según arquitectura
fmt.Printf("Tamaño int: %d bytes\n", unsafe.Sizeof(i)) // → 8 en sistemas x64
fmt.Printf("Tamaño int64: %d bytes\n", unsafe.Sizeof(i64)) // → 8 siempre
// Error de compilación: i = i64 (mismatched types)
i = int(i64) // Conversión explícita obligatoria
// Verificación de tipos en tiempo de ejecución
fmt.Println(reflect.TypeOf(i) == reflect.TypeOf(i64)) // → false
// Uso de uintptr para aritmética básica de memoria
arr := [2]int{10, 20}
basePtr := unsafe.Pointer(&arr[0])
// Convertimos a uintptr para saltar el tamaño de un int en bytes
nextElementAddr := uintptr(basePtr) + unsafe.Sizeof(arr[0])
ptr2 := unsafe.Pointer(nextElementAddr)
fmt.Println(*(*int)(ptr2)) // → 20
}
GoLa distinción entre int y int64 garantiza que el desarrollador sea consciente de las limitaciones de la plataforma, forzando una decisión deliberada cuando se opera con datos que podrían desbordar un registro de 32 bits.
El riesgo de invisibilidad de uintptr ante el Garbage Collector
Un comportamiento crítico y a menudo malinterpretado es la relación entre uintptr y el Garbage Collector (GC) de Go. A diferencia de un puntero estándar o un unsafe.Pointer, una variable de tipo uintptr es simplemente un número entero para el runtime; no posee semántica de referencia. Esto significa que el GC no rastrea las direcciones almacenadas en un uintptr.
Si un objeto en el heap solo es referenciado por una variable uintptr, el GC considerará que el objeto es inalcanzable y procederá a liberar su memoria o moverla durante un proceso de compactación (stack scanning). Un caso de error común ocurre al almacenar la dirección de un objeto en un uintptr, ejecutar una operación que dispare la recolección de basura y luego intentar convertir ese uintptr de nuevo a un puntero. En ese intervalo, la memoria pudo haber sido reasignada, convirtiendo el uintptr en una dirección inválida o “colgante”. Por ello, las conversiones entre unsafe.Pointer y uintptr deben ocurrir de forma atómica en la misma expresión donde se realiza la aritmética de punteros para evitar que el objeto sea recolectado prematuramente.
- Módulo: Sistema de Tipos
- Artículo número: #19