Dwarves
Memo
Type ESC to close search bar

Package first design

Here’s another article that I want to reassure everyone to know about it. As Go pushes more type composition over inheritance, the POV on building ‘unit’ is different compare to other languages.

In Go, packages serve as the basic building blocks for creating modular, reusable, and maintainable software. Go’s philosophy encourages developers to organize their code in a package-oriented way.

Treat your packages as base units. This means that, from the outset, you should structure your project into reusable, well-encapsulated packages, each with a clear purpose.

Key concepts

  1. Encapsulation and exporting

By default, Go keeps all symbols (functions, variables, constants, types) within a package private unless they are explicitly exported. Exported symbols in Go start with an uppercase letter. This helps enforce encapsulation, exposing only what’s necessary for external users while keeping the internal details hidden.

For example:

// This function is public and can be used outside the package.
func Add(a, b int) int {
    return a + b
}

// This function is private to the package.
func subtract(a, b int) int {
    return a - b
}
  1. Separation of concerns

Packages should follow the principle of separation of concerns. Each package should serve a single purpose or set of related tasks. This makes the codebase more understandable and easier to maintain.

For instance, if you’re building a web server, you might separate concerns into different packages like:

  1. Directory structure Go’s tooling is designed to work seamlessly with a package-oriented directory structure. Each directory contains its own package, which can be imported by other parts of your project.

Here’s an example directory structure:

myproject/
  ├── go.mod
  ├── cmd/            // For command-line tools and executables
  │   └── myapp/
  │       └── main.go
  ├── pkg/            // For libraries and reusable code
  │   └── http/
  │       └── handler.go
  ├── internal/       // For non-public packages
  │   └── config/
  │       └── config.go
  └── vendor/         // Third-party dependencies (if needed)
  1. Testing in packages

Each package should also contain its own unit tests, which are placed in the same directory as the package itself, following Go’s testing framework. Test files are named with the _test.go suffix and can test both exported and internal functions of a package.

Example:

// In mathutil/add_test.go
package mathutil

import "testing"

func TestAdd(t *testing.T) {
    result := Add(2, 3)
    if result != 5 {
        t.Errorf("Expected 5, but got %d", result)
    }
}
  1. Modularity and reusability

By structuring your code into distinct packages, you create reusable building blocks. These packages can be easily shared across different projects or within teams, and since Go’s import system relies on unique paths, there’s no conflict as long as each package’s import path is unique.

How to apply

To apply package-oriented development with a focus on reusability, follow these steps. Keeping reusability in mind from the start ensures your code is modular, maintainable, and adaptable for future projects.

Please take note that all below examples are to demonstrate the approach

  1. Identify core domains and reusable utility needs

Example Structure:

myproject/
  ├── users/
  ├── orders/
  └── util/
       ├── stringutil/
       └── timeutil/
  1. Design each package to be self-contained and purpose-driven

This single responsibility design ensures that when you need similar functionality in another project, you can reuse the package without modification.

  1. Create generalized, flexible functions
  1. Use interfaces to decouple dependencies
// orders/service.go
package orders

type UserFetcher interface {
    GetUser(userID int) (User, error)
}

type OrderService struct {
    UserService UserFetcher
}

Using interfaces like this enhances reusability because each package relies on general contracts rather than specific implementations.

  1. Structure utility packages for broad use

This organization avoids the common “catch-all” utils package, promoting well-structured, reusable functions that don’t add unnecessary dependencies.

  1. Document with reusability in mind

This documentation makes it easier for others to understand and adopt your package, increasing the likelihood of reuse.

  1. Write independent unit tests for each package
  1. Refactor with reusability in mind

Periodic refactoring keeps packages easy to understand, maintain, and reuse, avoiding monolithic packages that are hard to untangle or apply to new contexts.