Cómo organizar código en paquetes, qué se puede usar desde otro archivo o desde
main, y cómo importar tanto la librería estándar como código propio. En Go esto no es opcional ni configurable: la visibilidad depende únicamente de la primera letra del nombre, y el mecanismo de import está atado directamente a la estructura de carpetas del proyecto.
1. La unidad básica: el paquete, no el archivo
En Go, la visibilidad se define a nivel de paquete, no a nivel de archivo. Todos los archivos .go que declaran package algo en la misma carpeta pertenecen al mismo paquete y comparten todo entre sí sin necesidad de import, aunque estén en archivos distintos.
// archivo: usuarios.go
package gestion
type Usuario struct {
Nombre string
}
// archivo: validacion.go — misma carpeta, mismo paquete
package gestion
func validar(u Usuario) bool { // usa Usuario sin importar nada, porque comparte paquete con usuarios.go
return u.Nombre != ""
}
- Todos los archivos de una misma carpeta deben declarar el mismo
package(salvo el caso especial de archivos_test.go, que pueden usarpackage algo_testpara tests de caja negra — no es necesario profundizar en esto ahora). package maines especial: identifica un paquete ejecutable, y debe tener una funciónmain(). Cualquier otro nombre de paquete es una librería — no se puede ejecutar directo congo run, solo se importa desde otro lugar.
2. La regla de exportación: mayúscula inicial
No hay palabras clave public/private/protected. La regla es puramente sintáctica y se aplica igual a funciones, structs, campos de struct, métodos, constantes, variables y tipos:
package gestion
func Crear() {} // exportado: visible desde otros paquetes
func crear() {} // privado: solo visible dentro de "gestion"
type Usuario struct { // exportado: el tipo se puede usar desde afuera
Nombre string // exportado: el campo se puede leer/escribir desde afuera
clave string // privado: solo accesible dentro de "gestion", ni siquiera se ve el campo desde otro paquete
}
const MaxIntentos = 3 // exportado
const maxIntentos = 3 // privado
var Version = "1.0" // exportado
- La regla es por identificador, no por archivo ni por bloque: dentro del mismo
package gestion, se puede tener una función exportada y diez privadas mezcladas en cualquier archivo. - Importante con structs: la exportación del tipo y la exportación de cada campo son decisiones independientes. Un struct exportado puede tener campos privados, y en ese caso solo se pueden modificar desde funciones/métodos definidos dentro del mismo paquete — es el mecanismo real de encapsulamiento en Go, no simulado con getters/setters obligatorios.
type Cuenta struct {
Titular string // accesible desde cualquier paquete
saldo float64 // solo accesible desde dentro de "gestion"
}
Desde otro paquete: c.Titular funciona, c.saldo da error de compilación (saldo is not exported).
3. Crear un paquete propio y usarlo desde main
La estructura de carpetas determina el import path. Ejemplo de un proyecto con un paquete propio:
miapp/
├── go.mod
├── main.go
└── gestion/
└── usuarios.go
// go.mod module miapp go 1.22
// gestion/usuarios.go
package gestion
type Usuario struct {
Nombre string
Edad int
}
func NuevoUsuario(nombre string, edad int) *Usuario {
return &Usuario{Nombre: nombre, Edad: edad}
}
func (u *Usuario) Saludo() string {
return "Hola, " + u.Nombre
}
// main.go
package main
import (
"fmt"
"miapp/gestion"
)
func main() {
u := gestion.NuevoUsuario("Ana", 30)
fmt.Println(u.Saludo())
}
- El import path (
miapp/gestion) se construye comonombre-del-módulo+ruta-de-carpetas-desde-la-raíz. El nombre del módulo es el que aparece en elgo.mod(module miapp), no el nombre de la carpeta del disco (aunque suelen coincidir). - El nombre que se usa para invocar (
gestion.NuevoUsuario) es el nombre declarado conpackage gestiondentro del archivo.go, que por convención coincide con el nombre de la carpeta, pero técnicamente podría ser distinto (no se recomienda hacerlo, genera confusión). - No hace falta declarar explícitamente qué se exporta desde un paquete — automáticamente se exporta todo lo que tenga mayúscula inicial. No hay un archivo
index.jsni un__init__.pyque declare la interfaz pública.
4. Import: sintaxis y variantes
import "fmt" // un solo paquete
import ( // varios paquetes, forma preferida
"fmt"
"os"
"miapp/gestion"
)
import g "miapp/gestion" // alias: se usa como g.NuevoUsuario(...)
import . "miapp/gestion" // dot import: se usa NuevoUsuario(...) directo, sin prefijo
import _ "miapp/gestion" // import en blanco: solo ejecuta el init() del paquete, sin poder usar sus identificadores
- La forma con paréntesis es la convención estándar apenas hay más de un import;
go fmtademás ordena los imports alfabéticamente dentro de cada grupo. - Alias (
import g "miapp/gestion") es útil cuando dos paquetes importados tienen el mismo nombre final (por ejemplo, dos librerías distintas que ambas se llamanjsonen su último segmento de path) o cuando el nombre es muy largo y se usa mucho. - Dot import (
import . "...") casi no se usa en código real — hace que no quede claro de dónde viene cada identificador al leer el código. Aparece en algunos contextos de testing con librerías como. "github.com/onsi/ginkgo", pero fuera de eso se considera mala práctica. - Import en blanco (
import _ "...") se usa cuando el paquete tiene efectos secundarios necesarios en suinit()pero no se necesita llamar directamente a ninguna de sus funciones — el caso clásico es registrar un driver de base de datos:import _ "github.com/lib/pq". - Import no usado es error de compilación siempre, salvo con el guion bajo — es la razón de que exista esa variante.
Paquetes de la librería estándar vs paquetes propios/externos
import (
"fmt" // stdlib: sin dominio, un solo nombre o ruta corta
"net/http" // stdlib: rutas más largas para sub-paquetes
"miapp/gestion" // paquete propio, dentro del mismo módulo
"github.com/lib/pq" // paquete externo, identificado por su ubicación real (repositorio)
)
- Los paquetes externos se descargan con
go get github.com/lib/pq, que actualizago.modygo.sumautomáticamente. - No existe un registro central como npm o PyPI — el import path de un paquete externo es literalmente la URL donde se aloja su código fuente (típicamente GitHub).
5. Organizar un paquete en varios archivos
No hay límite de un tipo por archivo ni obligación de que el nombre del archivo coincida con nada. La convención es agrupar por responsabilidad:
gestion/ ├── usuarios.go // struct Usuario y funciones relacionadas ├── validacion.go // funciones de validación, privadas casi todas └── gestion_test.go // tests del paquete
Todo lo que esté en estos tres archivos comparte el mismo espacio de nombres del paquete gestion — una función en validacion.go puede usar un tipo definido en usuarios.go sin ningún import, porque no son “módulos” distintos, son el mismo paquete repartido en archivos por prolijidad.
6. init() — código que corre al importar
package gestion
var configuracion map[string]string
func init() {
configuracion = map[string]string{"entorno": "produccion"}
}
init()se ejecuta automáticamente cuando el paquete se importa, antes demain(), sin que nadie lo llame explícitamente.- Un paquete puede tener varios
init()(incluso repartidos en distintos archivos), y se ejecutan en el orden en que Go compila los archivos (alfabético por nombre de archivo dentro del paquete). - Se usa para inicializar estado global, registrar drivers/plugins (ver el import en blanco de la sección 4), o validar invariantes al arrancar. Se usa con moderación: lógica de inicialización escondida en un
init()que nadie ve al leermain()puede ser difícil de rastrear — si se puede resolver con una función explícita llamada desdemain, suele ser preferible.
7. Paquete internal — exportado pero con límite
Go tiene una convención especial reconocida por el compilador: cualquier paquete dentro de una carpeta llamada internal solo puede ser importado por código que esté dentro del mismo módulo, en el árbol de carpetas que contiene a internal como ancestro.
miapp/
├── go.mod
├── main.go
├── internal/
│ └── basedatos/
│ └── conexion.go // exportado (mayúscula), pero solo usable dentro de miapp
└── gestion/
└── usuarios.go
// internal/basedatos/conexion.go
package basedatos
func Conectar() error { // exportado, pero...
return nil
}
// gestion/usuarios.go — dentro del mismo módulo, SÍ puede importar internal import "miapp/internal/basedatos"
Si otro proyecto externo intentara import "miapp/internal/basedatos", el compilador lo rechaza directamente, aunque Conectar esté en mayúscula. Es la forma de decir “esto es parte pública del lenguaje de exportación (mayúscula), pero es implementación interna del proyecto, no una API para terceros”.
8. Usar structs exportados con campos mixtos: el patrón constructor
Cuando un struct exportado tiene campos privados (para forzar invariantes), la forma estándar de permitir su creación desde otro paquete es una función NewX:
package gestion
type Cuenta struct {
Titular string
saldo float64 // privado: no se puede poner en negativo desde afuera
}
func NuevaCuenta(titular string, saldoInicial float64) (*Cuenta, error) {
if saldoInicial < 0 {
return nil, fmt.Errorf("el saldo inicial no puede ser negativo")
}
return &Cuenta{Titular: titular, saldo: saldoInicial}, nil
}
func (c *Cuenta) Saldo() float64 { // "getter" explícito, expone el valor sin exponer el campo
return c.saldo
}
func (c *Cuenta) Depositar(monto float64) error {
if monto <= 0 {
return fmt.Errorf("el monto debe ser positivo")
}
c.saldo += monto
return nil
}
Desde main:
cuenta, err := gestion.NuevaCuenta("Ana", 100)
if err != nil {
log.Fatal(err)
}
cuenta.Depositar(50)
fmt.Println(cuenta.Saldo()) // 150
cuenta.saldo = -1000 // error de compilación: saldo is not exported
Este patrón —struct exportado, campos sensibles privados, constructor y métodos que validan— es la forma idiomática de encapsulamiento en Go, y reemplaza directamente a los getters/setters obligatorios de otros lenguajes: se usan solo donde realmente aportan algo (validación, cómputo), no como ceremonia automática.
9. Ejemplo integrador: proyecto con dos paquetes propios
miapp/
├── go.mod
├── main.go
├── gestion/
│ └── usuarios.go
└── internal/
└── validacion/
└── validacion.go
// internal/validacion/validacion.go
package validacion
import "strings"
func EmailValido(email string) bool {
return strings.Contains(email, "@")
}
// gestion/usuarios.go
package gestion
import (
"fmt"
"miapp/internal/validacion"
)
type Usuario struct {
Nombre string
Email string
}
func NuevoUsuario(nombre, email string) (*Usuario, error) {
if !validacion.EmailValido(email) {
return nil, fmt.Errorf("email inválido: %s", email)
}
return &Usuario{Nombre: nombre, Email: email}, nil
}
// main.go
package main
import (
"fmt"
"log"
"miapp/gestion"
)
func main() {
u, err := gestion.NuevoUsuario("Ana", "ana@ejemplo.com")
if err != nil {
log.Fatal(err)
}
fmt.Println(u.Nombre, u.Email)
}
Este ejemplo muestra la cadena completa: main importa gestion, gestion importa internal/validacion, y main no puede importar internal/validacion directamente si estuviera en otro módulo — pero como todo vive bajo el mismo module miapp, sí puede hacerlo, y de hecho también podría importarlo desde main.go sin pasar por gestion si tuviera sentido.
Referencia rápida
| Necesito… | Cómo |
|---|---|
| Exportar una función, tipo, constante o variable | Nombre con mayúscula inicial |
| Mantenerlo privado al paquete | Nombre con minúscula inicial |
| Exportar un struct pero no todos sus campos | Mayúscula en el tipo, minúscula en los campos sensibles |
| Compartir código entre archivos sin import | Mismo package en la misma carpeta |
| Usar un paquete propio desde otro | import "modulo/carpeta", según el module del go.mod |
| Importar con otro nombre | import alias "ruta" |
Importar solo por el efecto de init() | import _ "ruta" |
| Código que corre al importar el paquete | función init() |
| Paquete usable solo dentro del propio proyecto | carpeta internal/ en cualquier nivel |
| Crear structs con campos privados desde afuera | función constructora NewX(...) |