Understanding Go Packages and Modules
Packages are the building blocks of Go applications. This guide covers everything you need to know about working with packages and modules in Go.
Package Basics
Package Declaration
// File: user.go
package users
// Package-level variables
var (
defaultRole = "user"
MaxUsers = 1000 // Exported
)
// Exported type
type User struct {
ID string
Name string
role string // Unexported
}
// Exported function
func NewUser(name string) *User {
return &User{
Name: name,
role: defaultRole,
}
}
Package Organization
myapp/
├── go.mod
├── go.sum
├── main.go
├── users/
│ ├── user.go
│ ├── validation.go
│ └── database.go
└── config/
├── config.go
└── defaults.go
Package Visibility
Exported vs Unexported
package math
// Exported (public) - starts with uppercase
func Calculate(x int) int {
return multiply(x, 2)
}
// Unexported (private) - starts with lowercase
func multiply(x, y int) int {
return x * y
}
// Exported type with both exported and unexported fields
type Rectangle struct {
Width float64 // Exported
Height float64 // Exported
area float64 // Unexported
}
Using Packages
package main
import (
"fmt"
"myapp/users"
"myapp/config"
)
func main() {
user := users.NewUser("Alice")
fmt.Printf("Created user: %v\n", user)
// Can't access unexported members
// user.role = "admin" // Compilation error
}
Package Documentation
Package Comments
// Package users provides user management functionality.
// It handles user creation, validation, and persistence.
package users
// User represents a system user with basic information.
type User struct {
ID string
Name string
}
// NewUser creates a new user with the given name.
// It returns nil if the name is invalid.
func NewUser(name string) *User {
if name == "" {
return nil
}
return &User{Name: name}
}
Example Tests
package users_test
func Example() {
user := users.NewUser("Alice")
fmt.Printf("User: %v\n", user)
// Output: User: {ID: Name:Alice}
}
func ExampleNewUser() {
user := users.NewUser("Bob")
fmt.Printf("Name: %s\n", user.Name)
// Output: Name: Bob
}
Package Initialization
Init Function
package database
import "log"
var db *Database
func init() {
// Initialize package
var err error
db, err = Connect()
if err != nil {
log.Fatal(err)
}
}
func GetDB() *Database {
return db
}
Multiple Init Functions
package config
// First init function
func init() {
loadDefaults()
}
// Second init function
func init() {
loadEnvironment()
}
// Third init function
func init() {
validateConfig()
}
Package Dependencies
Import Statements
package main
import (
// Standard library
"fmt"
"log"
// Third-party packages
"github.com/gin-gonic/gin"
// Local packages
"myapp/users"
"myapp/config"
)
Import Aliases
import (
// Alias import
mmath "math"
// Dot import (avoid in production code)
. "fmt"
// Blank import
_ "github.com/lib/pq"
)
Package Management
Go Modules
# Initialize a new module
go mod init myapp
# Add dependencies
go get github.com/gin-gonic/gin
# Update dependencies
go get -u
# Tidy dependencies
go mod tidy
Version Selection
// go.mod
module myapp
go 1.21
require (
github.com/gin-gonic/gin v1.9.1
github.com/lib/pq v1.10.9
)
Best Practices
1. Package Organization
myapp/
├── cmd/ # Command-line applications
│ └── server/
│ └── main.go
├── internal/ # Private application code
│ ├── auth/
│ └── database/
├── pkg/ # Public library code
│ ├── models/
│ └── utils/
└── api/ # API definitions
└── proto/
2. Package Naming
// Good package names
package users
package database
package config
// Avoid
package utils // Too generic
package common // Too generic
package helper // Too generic
3. Interface Definition
// Good: Define interfaces where they are used
package client
type Service interface {
Get(id string) (*Response, error)
List() ([]*Response, error)
}
// Implementation can be in a different package
package server
type service struct{}
func (s *service) Get(id string) (*client.Response, error) {
// Implementation
}
Common Patterns
1. Options Pattern
package server
type Option func(*Server)
func WithPort(port int) Option {
return func(s *Server) {
s.port = port
}
}
func New(options ...Option) *Server {
s := &Server{
port: 8080, // Default
}
for _, opt := range options {
opt(s)
}
return s
}
2. Builder Pattern
package query
type Builder struct {
table string
where string
limit int
}
func NewBuilder() *Builder {
return &Builder{}
}
func (b *Builder) Table(name string) *Builder {
b.table = name
return b
}
func (b *Builder) Build() string {
// Build query
return fmt.Sprintf("SELECT * FROM %s", b.table)
}
3. Factory Pattern
package storage
type Store interface {
Save(data []byte) error
Load() ([]byte, error)
}
func NewStore(kind string) Store {
switch kind {
case "file":
return &FileStore{}
case "memory":
return &MemoryStore{}
default:
return &NullStore{}
}
}
Testing Packages
Table Tests
package users
import "testing"
func TestValidateUser(t *testing.T) {
tests := []struct {
name string
user User
wantErr bool
}{
{
name: "valid user",
user: User{Name: "Alice"},
wantErr: false,
},
{
name: "invalid user",
user: User{Name: ""},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.user.Validate()
if (err != nil) != tt.wantErr {
t.Errorf("Validate() error = %v, wantErr %v",
err, tt.wantErr)
}
})
}
}
Mocks and Interfaces
package users
type Repository interface {
Save(user *User) error
Find(id string) (*User, error)
}
// Mock implementation for testing
type MockRepository struct {
users map[string]*User
}
func NewMockRepository() *MockRepository {
return &MockRepository{
users: make(map[string]*User),
}
}
func (m *MockRepository) Save(user *User) error {
m.users[user.ID] = user
return nil
}
Next Steps
- Learn about interfaces
- Explore testing
- Study project layout
- Practice with dependency management