3.6 Packages and Imports

Packages are Go's way of organizing code. Understanding packages and imports is essential for building maintainable Go programs.

Package Organization

A package is a collection of Go files in the same directory that share the same package name:

myproject/
  main.go          // package main
  utils/
    math.go        // package utils
    string.go      // package utils
  models/
    user.go        // package models

Package naming

Import Paths

An import path is a string that uniquely identifies a package and tells Go where to find it. Understanding how import paths work is crucial for working with Go's module system and package management.

What is an import path?

An import path is the string you use in your import statement to bring a package into your code. It serves as both an identifier and a location specifier. The import path tells Go's toolchain where to find the package code, whether it's in the standard library, a third-party module, or your own project.

How Go resolves import paths

When you write an import statement, Go follows a specific resolution process to locate the package:

  1. Standard library packages: For paths like "fmt", "os", or "strings", Go looks in the standard library that comes with your Go installation. These are located in $GOROOT/src/ (though you typically don't need to know this).

  2. Module-based packages: For paths like "github.com/gin-gonic/gin", Go uses the module system introduced in Go 1.11. The resolution process works as follows:

    • Go checks your go.mod file to see if the module is already listed as a dependency
    • If not listed, Go looks in the module cache ($GOPATH/pkg/mod/ or $HOME/go/pkg/mod/)
    • If not found locally, Go downloads the module from the source (e.g., GitHub) and caches it
    • The import path must match the module path declared in the package's go.mod file
  3. Local packages: For paths like "myproject/utils", Go resolves them relative to your current module. The import path is constructed from:

    • Your module's path (from go.mod): module myproject
    • Plus the subdirectory: utils
    • Result: "myproject/utils"

Import path structure

Import paths follow a hierarchical structure that reflects their location and organization:

module-path/package-path

For example:

Standard library imports

Standard library packages use simple, short paths. These are built into Go and don't require any external dependencies:

import (
    "fmt"      // Formatting and printing
    "os"       // Operating system interface
    "strings"  // String manipulation
    "time"     // Time and date operations
    "net/http" // HTTP client and server
)

Standard library packages are always available and don't need to be downloaded. They're part of every Go installation.

Third-party imports

Third-party packages use import paths that typically match their source code repository location. This convention makes it easy to find the source code and understand where a package comes from:

import (
    "github.com/gin-gonic/gin"           // Web framework from GitHub
    "golang.org/x/crypto/bcrypt"         // Extended crypto library
    "google.golang.org/grpc"             // gRPC library
    "go.uber.org/zap"                    // Logging library
)

The import path structure often reflects:

Local imports

Local packages within your project use import paths based on your module's path (declared in go.mod):

// go.mod
module myproject

// main.go
import (
    "myproject/utils"    // Local package in utils/ directory
    "myproject/models"   // Local package in models/ directory
    "myproject/api/v1"   // Local package in api/v1/ directory
)

The import path for local packages is:

Module path vs package name

It's important to distinguish between the import path and the package name:

The package name is what you use to access exported identifiers. The import path is how you tell Go where to find the package. They can be different, though by convention they often match the last component of the import path.

Example

// The import path
import "github.com/gin-gonic/gin"

// The package name (from gin's source: package gin)
// So you use: gin.New(), gin.Context, etc.

How Go finds packages

When you build or run a Go program, the compiler needs to locate all imported packages. Here's the process:

  1. Parse import statements: Go reads all import statements in your source files
  2. Check module dependencies: For module-based projects, Go checks go.mod and go.sum files
  3. Resolve module paths: Go maps import paths to module paths
  4. Locate package code: Go finds the package source code in:
    • The module cache (for downloaded dependencies)
    • The local file system (for your project's packages)
    • The standard library (for standard packages)
  5. Compile dependencies: Go compiles all imported packages as needed

Import path best practices

  1. Use meaningful paths: Import paths should clearly indicate the package's purpose and origin
  2. Follow conventions: Use repository-based paths for third-party packages
  3. Keep paths stable: Once published, import paths should not change (this is part of Go's compatibility promise)
  4. Use versioning: For modules, use semantic versioning in go.mod, not in import paths
  5. Avoid deep nesting: Keep import paths reasonably short and readable

Common import path patterns

import (
    // Standard library (short paths)
    "fmt"
    "net/http"

    // Third-party (repository-based)
    "github.com/user/repo"
    "golang.org/x/tools/go/packages"

    // Local (module-based)
    "myproject/internal/utils"
    "myproject/pkg/models"

    // Subpackages
    "github.com/gorilla/mux"
    "github.com/gorilla/sessions"  // Different package, same module
)

Import Aliases

Go provides several ways to customize how you reference imported packages. These aliasing mechanisms serve different purposes and have specific use cases.

Default package name

When you import a package without an alias, Go automatically uses the package name (declared in the package's source files) to access its exported identifiers:

import "fmt"

func main() {
    fmt.Println("Hello")  // Uses package name "fmt"
}

The package name is determined by the package declaration in the imported package's source files, not by the import path. For example, "github.com/gin-gonic/gin" has the package name gin, so you use gin.New() to access its functions.

Custom alias

You can assign a custom name to an imported package using the syntax alias "importpath". This is useful for:

  1. Resolving naming conflicts: When two packages would have the same name
  2. Improving readability: When the package name is verbose or unclear
  3. Migration scenarios: When refactoring or switching between package versions
import (
    "context"                           // Standard library
    gcontext "github.com/gorilla/context"  // Third-party with alias
)

func handleRequest(ctx context.Context) {
    // Use standard library context
    ctx = context.WithValue(ctx, "key", "value")

    // Use Gorilla context with alias
    gcontext.Set(req, "session", "abc123")
}

When you use a custom alias, you must use that alias (not the original package name) to access the package's exported identifiers:

import f "fmt"

func main() {
    f.Println("Hello")  // Must use "f", not "fmt"
    // fmt.Println("Hello")  // ERROR: undefined: fmt
}

Dot import (. alias)

The dot import is a special alias that imports a package's exported identifiers directly into the current package's namespace. This means you can use the package's exported functions, types, and variables without a package prefix.

How dot imports work

When you use import . "package", all exported identifiers from that package become available in your current file's namespace as if they were defined locally. This eliminates the need for a package prefix when accessing the package's contents.

// Without dot import
import "fmt"

func main() {
    fmt.Println("Hello")  // Need package prefix
    fmt.Printf("Value: %d\n", 42)
}

// With dot import
import . "fmt"

func main() {
    Println("Hello")      // No prefix needed
    Printf("Value: %d\n", 42)  // No prefix needed
}

When dot imports are useful

Dot imports have a specific, limited use case: test files. In test files (files ending with _test.go), dot imports can make test code more readable by reducing verbosity:

// math_test.go
package math

import (
    . "math"  // Dot import of the package being tested
    "testing"
)

func TestAdd(t *testing.T) {
    // Can use math functions directly without "math." prefix
    result := Add(2, 3)
    if result != 5 {
        t.Errorf("Expected 5, got %d", result)
    }
}

Why dot imports are generally discouraged

While dot imports are syntactically valid, they're considered non-idiomatic in production code for several important reasons:

  1. Naming conflicts: Dot imports can cause identifier collisions. If the imported package exports an identifier with the same name as something in your current package (or another dot-imported package), you'll get a compile error:
package main

import . "fmt"

func Println() {  // ERROR: Println redeclared
    // Your own Println function conflicts with fmt.Println
}
  1. Reduced clarity: Without package prefixes, it becomes unclear where identifiers come from. This makes code harder to read and understand, especially for developers unfamiliar with the codebase:
import . "fmt"
import . "os"

func main() {
    // Where does Println come from? fmt or os?
    // Is Exit from os or somewhere else?
    Println("Hello")
    Exit(0)
}
  1. Maintenance challenges: When reading code, you can't immediately tell which package an identifier belongs to. This makes refactoring and debugging more difficult.

  2. Tool limitations: Some Go tools and linters have difficulty analyzing code with dot imports, as they can't easily determine identifier origins.

Best practices for dot imports

Blank import (_ alias)

The blank import is used when you want to import a package solely for its side effects (like initialization code) without using any of its exported identifiers:

import _ "database/sql/driver"

// The package is imported and its init() functions run,
// but you can't access any of its exported identifiers

This is commonly used for:

Complete alias examples

package main

import (
    // Default: use package name
    "fmt"

    // Custom alias
    f "fmt"

    // Dot import (discouraged in production)
    . "fmt"

    // Blank import (for side effects)
    _ "database/sql"

    // Resolving conflicts
    "context"
    gcontext "github.com/gorilla/context"
)

func main() {
    // Using default package name
    fmt.Println("Default")

    // Using custom alias
    f.Println("Alias")

    // Using dot import (no prefix)
    Println("Dot import")

    // Using aliased package to avoid conflict
    ctx := context.Background()
    gcontext.Set(req, "key", "value")
}

Idiomatic Go Note While Go allows dot imports (. "package"), the idiomatic approach is to use the package name prefix. Dot imports can cause naming conflicts and reduce code clarity, making it harder to see where functions come from. The only exception is in test files where dot imports can make test code more readable. Always prefer explicit package prefixes in production code for better maintainability and clarity.

Exported vs Unexported

Go uses a simple naming convention to control visibility: identifiers that start with an uppercase letter are exported (public), while those starting with a lowercase letter are unexported (private). This design provides a clean way to create public APIs while keeping implementation details hidden.

What does exported mean?

An exported identifier is one that can be accessed from outside the package where it's defined. When you import a package, you can only access its exported identifiers. This creates a clear boundary between a package's public API and its internal implementation.

What does unexported mean?

An unexported identifier is only accessible within the same package. It's private to that package and cannot be accessed from other packages, even if you know it exists. This allows packages to hide implementation details and maintain encapsulation.

The naming rule

The rule is simple but strict: the first character of the identifier name determines visibility. This applies to all identifiers: functions, types, variables, constants, methods, and struct fields.

Exported (public) identifiers

Unexported (private) identifiers

What can be exported or unexported

All identifiers in Go follow the export rules:

  1. Functions: Add() is exported, add() is unexported
  2. Types: Person is exported, person is unexported
  3. Variables: Count is exported, count is unexported
  4. Constants: MaxSize is exported, maxSize is unexported
  5. Methods: Methods on exported types can be exported or unexported based on their name
  6. Struct fields: Fields in exported structs can be exported or unexported

Struct field visibility

Struct fields follow the same rules. Even if a struct type is exported, its individual fields can be exported or unexported independently.

package user

// Exported type
type User struct {
    Name     string  // Exported field
    Email    string  // Exported field
    password string  // Unexported field (private)
    id       int     // Unexported field (private)
}

// Exported method
func (u *User) SetPassword(pwd string) {
    u.password = pwd  // Can access unexported field within same package
}

From another package, you can access Name and Email, but not password or id. However, methods in the same package can access all fields, exported or not.

Complete example

// Package: calculator
package calculator

// Exported function (public API)
func Add(a, b int) int {
    return a + b
}

// Unexported function (internal helper)
func multiply(a, b int) int {
    return a * b
}

// Exported variable
var Pi = 3.14159

// Unexported variable
var pi = 3.14159

// Exported type
type Calculator struct {
    // Exported field
    Version string
    // Unexported field
    cache map[string]int
}

// Exported method
func (c *Calculator) Calculate(op string, a, b int) int {
    // Can call unexported helper
    return multiply(a, b)
}

Using the package

package main

import (
    "fmt"
    "myproject/calculator"
)

func main() {
    // ✅ OK: Add is exported
    result := calculator.Add(5, 3)
    fmt.Println(result)

    // ❌ ERROR: multiply is unexported
    // result = calculator.multiply(5, 3)  // Compile error!

    // ✅ OK: Pi is exported
    fmt.Println(calculator.Pi)

    // ❌ ERROR: pi is unexported
    // fmt.Println(calculator.pi)  // Compile error!

    // ✅ OK: Calculator type is exported
    calc := calculator.Calculator{Version: "1.0"}

    // ✅ OK: Version field is exported
    fmt.Println(calc.Version)

    // ❌ ERROR: cache field is unexported
    // calc.cache = make(map[string]int)  // Compile error!
}

This design provides several benefits:

Go's export mechanism is designed to be simple and explicit. Unlike languages that use keywords like public or private, Go uses naming conventions that are:

Package-level visibility

Visibility is determined at the package level, not the file level. All files in the same package can access all identifiers (exported or unexported) from other files in that package. This means you can organize code across multiple files while maintaining the same visibility rules.

Best practices

  1. Export only what's needed: Keep your public API minimal—export only what other packages need to use
  2. Use unexported for implementation: Hide internal helpers, constants, and types that are implementation details
  3. Be consistent: If you export a type, typically export the functions that create or work with it
  4. Document exported identifiers: Since they're part of your public API, they should be well-documented
  5. Use unexported types for encapsulation: You can return unexported types through exported interfaces

Common patterns

package cache

// Exported interface (public API)
type Cache interface {
    Get(key string) (string, bool)
    Set(key, value string)
}

// Unexported type (implementation detail)
type memoryCache struct {
    data map[string]string
}

// Exported function returns unexported type through interface
func NewCache() Cache {
    return &memoryCache{data: make(map[string]string)}
}

This pattern allows you to export an interface while keeping the concrete implementation private, providing flexibility to change the implementation without affecting users of your package.

Creating Your Own Packages

1. Create package directory

mkdir -p myproject/utils

2. Create package file

// myproject/utils/math.go
package utils

func Add(a, b int) int {
    return a + b
}

func Subtract(a, b int) int {
    return a - b
}

3. Use the package

// myproject/main.go
package main

import (
    "fmt"
    "myproject/utils"
)

func main() {
    sum := utils.Add(10, 5)
    diff := utils.Subtract(10, 5)
    fmt.Println(sum, diff)
}

Complete package example

// Package: stringutils
package stringutils

import "strings"

// Reverse reverses a string
func Reverse(s string) string {
    runes := []rune(s)
    for i, j := 0, len(runes)-1; i < j; i, j = i+1, j-1 {
        runes[i], runes[j] = runes[j], runes[i]
    }
    return string(runes)
}

// ToUpper converts string to uppercase
func ToUpper(s string) string {
    return strings.ToUpper(s)
}

// Contains checks if string contains substring
func Contains(s, substr string) bool {
    return strings.Contains(s, substr)
}

Standard Library Overview

Go's standard library is one of its greatest strengths. It's comprehensive, well-designed, and provides most of what you need to build production applications without requiring external dependencies. The standard library follows Go's design principles: simplicity, clarity, and efficiency.

What is the standard library?

The standard library is a collection of packages that come with every Go installation. These packages are maintained by the Go team, are thoroughly tested, and are guaranteed to be available in any Go environment. Unlike many languages where you need to install additional libraries for common tasks, Go's standard library includes packages for networking, file I/O, text processing, cryptography, and much more.

The standard library matters because it provides:

Using the standard library has several advantages:

Key packages and their purposes

fmt: Formatting and printing

The fmt package provides functions for formatted I/O, similar to C's printf and scanf. It's one of the most commonly used packages for printing output and reading input.

import "fmt"
fmt.Println("Hello")                    // Simple printing
fmt.Printf("Value: %d\n", 42)          // Formatted printing
fmt.Sprintf("Formatted: %s", "text")    // Format to string

Use fmt for: console output, string formatting, reading input, and debugging output.

strings: String manipulation

The strings package provides functions for working with UTF-8 encoded strings. It includes operations for searching, replacing, splitting, joining, and transforming strings.

import "strings"
strings.Contains("hello", "ell")       // Check if substring exists
strings.ToUpper("hello")               // Convert to uppercase
strings.Split("a,b,c", ",")            // Split into slice
strings.Join([]string{"a", "b"}, "-")  // Join slice into string
strings.TrimSpace("  text  ")          // Remove whitespace

Use strings for: text processing, parsing, validation, and string transformations.

os: Operating system interface

The os package provides a platform-independent interface to operating system functionality. It includes file operations, environment variables, process management, and system calls.

import "os"
os.Getenv("PATH")                      // Get environment variable
os.Setenv("KEY", "value")              // Set environment variable
os.Exit(1)                             // Exit program with code
os.Getwd()                             // Get current working directory
file, _ := os.Open("file.txt")         // Open file

Use os for: file operations, environment variables, process control, and system information.

io/fs: File system operations

The io/fs package (introduced in Go 1.16) provides a filesystem interface and file system operations. It's designed to work with both real filesystems and virtual filesystems.

import "io/fs"
data, _ := fs.ReadFile(fsys, "file.txt")  // Read file
entries, _ := fs.ReadDir(fsys, ".")        // Read directory

Use io/fs for: file system abstraction, working with embedded files, and filesystem testing.

net/http: HTTP client and server

The net/http package provides HTTP client and server implementations. It's powerful enough to build production web servers and clients without additional frameworks.

import "net/http"
resp, _ := http.Get("https://example.com")  // HTTP GET request
http.HandleFunc("/", handler)               // Register route handler
http.ListenAndServe(":8080", nil)           // Start HTTP server

Use net/http for: building web servers, making HTTP requests, REST APIs, and web applications.

encoding/json: JSON encoding/decoding

The encoding/json package provides functions for encoding and decoding JSON data. It's essential for working with APIs and data serialization.

import "encoding/json"
data, _ := json.Marshal(obj)              // Convert to JSON
json.Unmarshal(jsonData, &result)         // Parse JSON
encoder := json.NewEncoder(writer)        // Stream encoding
decoder := json.NewDecoder(reader)        // Stream decoding

Use encoding/json for: API communication, configuration files, data persistence, and inter-service communication.

time: Time operations

The time package provides functionality for measuring and displaying time. It handles time zones, durations, timers, and tickers.

import "time"
time.Now()                                // Current time
time.Sleep(1 * time.Second)              // Sleep for duration
ticker := time.NewTicker(1 * time.Second) // Periodic timer
timer := time.NewTimer(5 * time.Second)   // One-shot timer

Use time for: timestamps, scheduling, timeouts, rate limiting, and time-based operations.

sync: Synchronization primitives

The sync package provides basic synchronization primitives like mutexes, wait groups, and once operations. These are essential for safe concurrent programming.

import "sync"
var mu sync.Mutex                         // Mutual exclusion lock
var wg sync.WaitGroup                     // Wait for goroutines
var once sync.Once                        // Execute once
var pool sync.Pool                        // Object pool

Use sync for: protecting shared resources, coordinating goroutines, and implementing thread-safe operations.

Other important standard library packages

Complete example

package main

import (
    "encoding/json"
    "fmt"
    "strings"
    "time"
)

type Person struct {
    Name string `json:"name"`
    Age  int    `json:"age"`
}

func main() {
    // Strings
    text := "Hello, World"
    fmt.Println(strings.ToUpper(text))
    fmt.Println(strings.Contains(text, "World"))

    // JSON
    p := Person{Name: "Alice", Age: 30}
    jsonData, _ := json.Marshal(p)
    fmt.Println(string(jsonData))

    // Time
    fmt.Println("Current time:", time.Now())
}

Key points about packages