Go Packages: Code Organization

Go packages are the fundamental unit of code organization. Every Go source file belongs to exactly one package, and packages provide namespacing, encapsulation, and reusability. Understanding how to...

Key Insights

  • Go packages are defined by directories, not files—all .go files in a directory must declare the same package name, and this is how Go naturally encourages modular code organization.
  • Exported identifiers (capitalized) form your package’s public API while unexported (lowercase) identifiers remain private, making visibility control a fundamental aspect of package design rather than an afterthought.
  • Organize packages by feature or domain rather than technical layer when possible—grouping related functionality together reduces coupling and makes code easier to navigate and maintain.

Introduction to Go Packages

Go packages are the fundamental unit of code organization. Every Go source file belongs to exactly one package, and packages provide namespacing, encapsulation, and reusability. Understanding how to structure packages effectively is critical for building maintainable Go applications.

The relationship between packages, directories, and modules is straightforward: a package is a collection of .go files in the same directory that share the same package declaration. A module is a collection of packages with a go.mod file at the root. This physical organization mirrors the logical organization of your code.

Go’s visibility rules are elegantly simple: identifiers starting with an uppercase letter are exported (public), while those starting with a lowercase letter are unexported (private to the package). This applies to functions, types, constants, variables, and even struct fields.

// file: mathutil/mathutil.go
package mathutil

// Add is exported and can be used by other packages
func Add(a, b int) int {
    return a + b
}

// multiply is unexported and only available within mathutil
func multiply(a, b int) int {
    return a * b
}
// file: main.go
package main

import "myproject/mathutil"

func main() {
    result := mathutil.Add(5, 3)      // OK
    // result := mathutil.multiply(5, 3) // Error: undefined
}

Package Naming and Structure Conventions

Package names should be short, lowercase, and singular. Avoid underscores, hyphens, or mixed caps. The package name should describe what the package provides, not what it contains. Good names: http, json, user, auth. Bad names: utils, helpers, common, myproject_handlers.

// Good
package user
package auth
package postgres

// Bad
package user_management  // no underscores
package Users           // not capitalized
package helpers         // too generic

Keep your directory structure relatively flat. Deep nesting makes imports verbose and navigation difficult. Most projects should have a structure like this:

myproject/
├── go.mod
├── main.go
├── user/
│   ├── user.go
│   └── repository.go
├── auth/
│   ├── auth.go
│   └── token.go
└── internal/
    └── database/
        └── postgres.go

The main package is special—it’s the entry point for executable programs. The internal directory is also special: packages within internal can only be imported by code in the parent tree. This is Go’s way of enforcing truly private packages.

Creating and Importing Packages

A package can span multiple files in the same directory. All files must declare the same package name. This allows you to split large packages into logical files without affecting imports.

// file: user/user.go
package user

type User struct {
    ID       int
    Username string
    Email    string
}

func New(username, email string) *User {
    return &User{
        Username: username,
        Email:    email,
    }
}
// file: user/repository.go
package user

import "database/sql"

type Repository struct {
    db *sql.DB
}

func NewRepository(db *sql.DB) *Repository {
    return &Repository{db: db}
}

func (r *Repository) Save(u *User) error {
    // implementation
    return nil
}

Import paths are relative to your module root defined in go.mod. For a module named github.com/username/myproject, you’d import local packages like this:

package main

import (
    "fmt"
    "github.com/username/myproject/user"
    "github.com/username/myproject/auth"
)

func main() {
    u := user.New("john", "john@example.com")
    fmt.Println(u.Username)
}

Circular dependencies are not allowed in Go. If package A imports package B, then package B cannot import package A. This forces you to think carefully about dependency direction and often leads to better architecture. If you encounter circular dependencies, extract shared types into a third package or reconsider your design.

Package Visibility and Encapsulation

Designing a clean package API means carefully choosing what to export. Only expose what consumers actually need. Keep implementation details private.

// file: auth/auth.go
package auth

import (
    "crypto/sha256"
    "encoding/hex"
    "errors"
)

// Service is the public interface for authentication
type Service struct {
    secret string
    cache  *tokenCache
}

// tokenCache is private implementation detail
type tokenCache struct {
    tokens map[string]string
}

func NewService(secret string) *Service {
    return &Service{
        secret: secret,
        cache:  &tokenCache{tokens: make(map[string]string)},
    }
}

// GenerateToken is part of the public API
func (s *Service) GenerateToken(userID string) (string, error) {
    if userID == "" {
        return "", errors.New("userID required")
    }
    return s.hashToken(userID), nil
}

// hashToken is a private helper
func (s *Service) hashToken(input string) string {
    h := sha256.New()
    h.Write([]byte(input + s.secret))
    return hex.EncodeToString(h.Sum(nil))
}

For truly private packages that should never be imported outside your module, use the internal directory:

myproject/
├── internal/
│   └── database/
│       └── connection.go  // only importable by myproject
└── user/
    └── user.go

Common Package Organization Patterns

There are two main approaches to organizing packages: by technical layer or by feature/domain.

Layered architecture groups code by technical function:

myproject/
├── handlers/     # HTTP handlers
├── services/     # Business logic
├── repositories/ # Data access
└── models/       # Data structures

This works for small projects but becomes unwieldy as you grow. You end up with packages like handlers containing dozens of unrelated handlers.

Domain-driven organization groups by feature:

myproject/
├── user/
│   ├── handler.go
│   ├── service.go
│   ├── repository.go
│   └── user.go
├── order/
│   ├── handler.go
│   ├── service.go
│   ├── repository.go
│   └── order.go
└── product/
    ├── handler.go
    ├── service.go
    └── product.go

This approach scales better. Each package is cohesive and self-contained. When you need to work on user functionality, everything is in one place.

Here’s a practical example of a feature-based HTTP API structure:

// file: user/handler.go
package user

import (
    "encoding/json"
    "net/http"
)

type Handler struct {
    service *Service
}

func NewHandler(service *Service) *Handler {
    return &Handler{service: service}
}

func (h *Handler) Create(w http.ResponseWriter, r *http.Request) {
    var req CreateRequest
    if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
        http.Error(w, err.Error(), http.StatusBadRequest)
        return
    }
    
    user, err := h.service.Create(req.Username, req.Email)
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }
    
    json.NewEncoder(w).Encode(user)
}

Package Initialization and Dependencies

The init() function runs automatically when a package is imported. Use it sparingly—only for truly necessary initialization like registering database drivers or validators.

package postgres

import (
    "database/sql"
    _ "github.com/lib/pq" // driver registers itself in init()
)

var defaultConfig = Config{
    MaxConnections: 25,
    Timeout:       30,
}

func init() {
    // Avoid complex logic here
    // No side effects that could fail
}

Prefer explicit initialization through constructors and dependency injection:

// file: user/service.go
package user

type Service struct {
    repo *Repository
}

// NewService uses dependency injection
func NewService(repo *Repository) *Service {
    return &Service{repo: repo}
}

func (s *Service) Create(username, email string) (*User, error) {
    user := &User{Username: username, Email: email}
    return user, s.repo.Save(user)
}

This makes dependencies explicit, testable, and avoids global state.

Best Practices and Anti-patterns

Keep packages focused. A package should have a single, clear purpose. If you struggle to name a package or describe what it does in one sentence, it’s probably doing too much.

Avoid generic names. Packages named utils, helpers, or common become dumping grounds for unrelated code. Be specific: stringutil, httputil, testhelper.

Don’t create packages prematurely. Start with a simple structure. Split packages only when you have a clear reason—usually when a logical boundary emerges or a package grows beyond 1000 lines.

Minimize package dependencies. Each import creates coupling. If package A imports packages B, C, D, and E, changes in any of those packages might break A. Keep your dependency graph shallow.

Here’s a refactoring example showing improvement:

// Before: Everything in one package
package app

type User struct { /* ... */ }
type Order struct { /* ... */ }
type Product struct { /* ... */ }
func CreateUser() { /* ... */ }
func CreateOrder() { /* ... */ }
func CreateProduct() { /* ... */ }

// After: Organized by domain
package user
type User struct { /* ... */ }
func Create() { /* ... */ }

package order
type Order struct { /* ... */ }
func Create() { /* ... */ }

package product
type Product struct { /* ... */ }
func Create() { /* ... */ }

The refactored version has clear boundaries, better encapsulation, and each package can evolve independently. This is the essence of good package organization in Go.

Liked this? There's more.

Every week: one practical technique, explained simply, with code you can use immediately.