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 usar package algo_test para tests de caja negra — no es necesario profundizar en esto ahora).
  • package main es especial: identifica un paquete ejecutable, y debe tener una función main(). Cualquier otro nombre de paquete es una librería — no se puede ejecutar directo con go 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 como nombre-del-módulo + ruta-de-carpetas-desde-la-raíz. El nombre del módulo es el que aparece en el go.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 con package gestion dentro 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.js ni un __init__.py que 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 fmt ademá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 llaman json en 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 su init() 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 actualiza go.mod y go.sum automá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 de main(), 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 leer main() puede ser difícil de rastrear — si se puede resolver con una función explícita llamada desde main, 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 variableNombre con mayúscula inicial
Mantenerlo privado al paqueteNombre con minúscula inicial
Exportar un struct pero no todos sus camposMayúscula en el tipo, minúscula en los campos sensibles
Compartir código entre archivos sin importMismo package en la misma carpeta
Usar un paquete propio desde otroimport "modulo/carpeta", según el module del go.mod
Importar con otro nombreimport alias "ruta"
Importar solo por el efecto de init()import _ "ruta"
Código que corre al importar el paquetefunción init()
Paquete usable solo dentro del propio proyectocarpeta internal/ en cualquier nivel
Crear structs con campos privados desde afuerafunción constructora NewX(...)