Packages and Modules in Golang

Packages and Modules in Golang

Packages and modules are fundamental concepts in Go that help organize and manage code. A package is a collection of Go files that are compiled together, while a module is a collection of related Go packages that are versioned and distributed together.

Packages

Every Go program is made up of packages. A package is a way to group related code together. The main package is special - it defines a standalone executable program.

Creating Packages

Create a new directory for your package and add Go files to it. All files in the same directory must belong to the same package.

mathutils.go
package mathutils

import "math"

// Add returns the sum of two numbers
func Add(a, b float64) float64 {
    return a + b
}

// Multiply returns the product of two numbers
func Multiply(a, b float64) float64 {
    return a * b
}

// Sqrt returns the square root of a number
func Sqrt(x float64) float64 {
    return math.Sqrt(x)
}

Using Packages

To use a package, import it at the top of your file.

main.go
package main

import (
    "fmt"
    "myproject/mathutils" // Import your custom package
)

func main() {
    result1 := mathutils.Add(5, 3)
    result2 := mathutils.Multiply(4, 2)
    result3 := mathutils.Sqrt(16)
    
    fmt.Printf("5 + 3 = %.0f\n", result1)
    fmt.Printf("4 * 2 = %.0f\n", result2)
    fmt.Printf("Square root of 16 = %.0f\n", result3)
}

Package Visibility

In Go, identifiers (functions, variables, types) that start with a capital letter are exported and can be accessed from other packages. Identifiers starting with a lowercase letter are private to the package.

geometry.go
package geometry

import "math"

// Point represents a 2D point - exported type
type Point struct {
    X, Y float64
}

// Area calculates the area of a circle - exported function
func Area(radius float64) float64 {
    return math.Pi * radius * radius
}

// perimeter calculates the perimeter of a circle - private function
func perimeter(radius float64) float64 {
    return 2 * math.Pi * radius
}

// Circle represents a circle - exported type
type Circle struct {
    Center Point
    Radius float64
}

// Area method for Circle
func (c Circle) Area() float64 {
    return Area(c.Radius)
}

Modules

Modules were introduced in Go 1.11 to manage dependencies and versioning. A module is a collection of Go packages stored in a file tree with a go.mod file at its root.

Creating a Module

Use go mod init to create a new module.

go mod init myproject

This creates a go.mod file:

go.mod
module myproject

go 1.21

Adding Dependencies

When you import packages that aren’t part of the standard library, Go will automatically download them.

main.go
package main

import (
    "fmt"
    "github.com/google/uuid" // External dependency
)

func main() {
    id := uuid.New()
    fmt.Println("Generated UUID:", id)
}

Run go mod tidy to download dependencies and update go.mod and go.sum files.

Module Structure

A typical Go module structure:

myproject/
├── go.mod
├── go.sum
├── main.go
├── mathutils/
│   └── mathutils.go
└── cmd/
    └── server/
        └── main.go

Module Commands

Common go mod commands:

  • go mod init <module-path> - Initialize a new module
  • go mod tidy - Add missing dependencies and remove unused ones
  • go mod download - Download modules to local cache
  • go mod verify - Verify dependencies have expected content
  • go mod why <module> - Explain why a module is needed
  • go mod graph - Print module requirement graph

Versioning

Modules use semantic versioning. You can specify versions in go.mod:

go.mod
module myproject

go 1.21

require (
    github.com/google/uuid v1.3.0
    github.com/gorilla/mux v1.8.0
)

Replacing Modules

You can replace a module with a local version for development:

go.mod
module myproject

go 1.21

require github.com/example/mylib v1.0.0

replace github.com/example/mylib => ../mylib

Package Initialization

The init function is a special function that gets called automatically when a package is imported. It’s used for package-level initialization.

database.go
package database

import (
    "database/sql"
    "fmt"
    _ "github.com/lib/pq" // Import for side effects
)

var db *sql.DB

func init() {
    var err error
    db, err = sql.Open("postgres", "user=postgres dbname=mydb sslmode=disable")
    if err != nil {
        panic(fmt.Sprintf("Failed to connect to database: %v", err))
    }
    
    fmt.Println("Database connection initialized")
}

func GetDB() *sql.DB {
    return db
}

Blank Imports

Sometimes you need to import a package just for its side effects (like registering drivers). Use a blank import:

main.go
package main

import (
    "database/sql"
    _ "github.com/lib/pq" // Blank import - only for side effects
)

func main() {
    db, err := sql.Open("postgres", "connection-string")
    // Use db...
}

Internal Packages

Use the internal directory to create packages that can only be imported by parent directories.

myproject/
├── internal/
│   ├── auth/
│   │   └── auth.go
│   └── utils/
│       └── helpers.go
├── cmd/
│   └── main.go

The internal/auth package can be imported by cmd/main.go but not by external modules.

Vendoring

Vendoring allows you to store dependencies locally. Run go mod vendor to create a vendor directory.

go mod vendor

Then build with:

go build -mod=vendor

Best Practices

  1. Use descriptive package names - The package name should reflect its purpose
  2. Keep packages focused - Each package should have a single responsibility
  3. Use internal for private APIs - Keep implementation details private
  4. Version your modules - Use semantic versioning for releases
  5. Document exported APIs - Use comments for exported functions and types
  6. Avoid circular dependencies - Design your package structure to prevent import cycles

Example: Complete Module Structure

Let’s create a complete example with proper module structure.

Terminal
mkdir calculator
cd calculator
go mod init calculator
go.mod
module calculator

go 1.21
main.go
package main

import (
    "fmt"
    "calculator/internal/operations"
)

func main() {
    result := operations.Add(10, 5)
    fmt.Printf("10 + 5 = %d\n", result)
    
    result = operations.Multiply(10, 5)
    fmt.Printf("10 * 5 = %d\n", result)
}
internal/operations/add.go
package operations

// Add returns the sum of two integers
func Add(a, b int) int {
    return a + b
}
internal/operations/multiply.go
package operations

// Multiply returns the product of two integers
func Multiply(a, b int) int {
    return a + b // Bug for demonstration
}
internal/operations/multiply_test.go
package operations

import "testing"

func TestMultiply(t *testing.T) {
    result := Multiply(3, 4)
    expected := 12
    
    if result != expected {
        t.Errorf("Multiply(3, 4) = %d; want %d", result, expected)
    }
}

Common Patterns

Factory Pattern

factory.go
package factory

import "fmt"

// Product interface
type Product interface {
    Use()
}

// ConcreteProductA
type ConcreteProductA struct{}

func (p *ConcreteProductA) Use() {
    fmt.Println("Using Product A")
}

// ConcreteProductB
type ConcreteProductB struct{}

func (p *ConcreteProductB) Use() {
    fmt.Println("Using Product B")
}

// Factory function
func CreateProduct(productType string) Product {
    switch productType {
    case "A":
        return &ConcreteProductA{}
    case "B":
        return &ConcreteProductB{}
    default:
        return nil
    }
}

Singleton Pattern

singleton.go
package singleton

import "sync"

type Singleton struct {
    value int
}

var instance *Singleton
var once sync.Once

func GetInstance() *Singleton {
    once.Do(func() {
        instance = &Singleton{value: 42}
    })
    return instance
}

func (s *Singleton) GetValue() int {
    return s.value
}

External Resources:

Related Tutorials:

Last updated on