Packages are Go's way of organizing code. Understanding packages and imports is essential for building maintainable Go programs.
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
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:
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).
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.mod file to see if the module is already listed as a dependency$GOPATH/pkg/mod/ or $HOME/go/pkg/mod/)go.mod fileLocal packages: For paths like "myproject/utils", Go resolves them relative to your current module. The import path is constructed from:
go.mod): module myprojectutils"myproject/utils"Import path structure
Import paths follow a hierarchical structure that reflects their location and organization:
module-path/package-path
For example:
github.com/gin-gonic/gin breaks down as:
github.com/gin-gonic/gingin (the last component)golang.org/x/crypto/bcrypt breaks down as:
golang.org/x/cryptobcrypt (subpackage within the crypto module)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:
github.com, gitlab.com)gin-gonic, uber)gin, zap)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:
go.mod) + the directory path relative to the module rootModule path vs package name
It's important to distinguish between the import path and the package name:
import statement (e.g., "github.com/gin-gonic/gin")package gin)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:
import statements in your source filesgo.mod and go.sum filesImport path best practices
go.mod, not in import pathsCommon 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
)
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:
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:
package main
import . "fmt"
func Println() { // ERROR: Println redeclared
// Your own Println function conflicts with fmt.Println
}
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)
}
Maintenance challenges: When reading code, you can't immediately tell which package an identifier belongs to. This makes refactoring and debugging more difficult.
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.
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
func Add(), type Person, var Count, const MaxSize, type Config struct { Name string }Unexported (private) identifiers
func add(), type person, var count, const maxSize, type config struct { name string }What can be exported or unexported
All identifiers in Go follow the export rules:
Add() is exported, add() is unexportedPerson is exported, person is unexportedCount is exported, count is unexportedMaxSize is exported, maxSize is unexportedStruct 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
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.
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)
}
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