Package System
Package System
Every Go source file belongs to a package. Packages are the unit of code organisation and encapsulation in Go.
Package Declaration
The first line of every .go file (after optional build constraints) declares its package:
package mypackageAll files in the same directory must use the same package name. The one exception is test files, which may use package mypackage_test for black-box testing.
Exported vs Unexported Names Go's access control is based entirely on capitalisation:
- Exported (public): identifiers starting with an uppercase letter (
MyStruct,Println,MaxRetries) - Unexported (private): identifiers starting with a lowercase letter (
myHelper,internalCache)
This rule applies to functions, types, variables, constants, struct fields, and interface methods. There are no public/private/protected keywords.
The init() Function
Each package can define one or more init() functions:
func init() {
// initialisation logic
}init functions run automatically after all variable initialisations in the package, and before main(). Their execution order within a package follows source file order, then declaration order. They cannot be called explicitly. Common uses: registering drivers, validating configuration, setting up global state.
Blank Identifier for Side Effects
Import a package purely for its init() side effects using the blank identifier:
import _ "image/png" // registers PNG decoder
import _ "github.com/lib/pq" // registers PostgreSQL driverWithout using any exported name, the blank import prevents the "imported and not used" compile error.
Package Aliases Rename a package at the import site to avoid conflicts or improve readability:
import (
mrand "math/rand"
crand "crypto/rand"
)Internal Packages
An internal directory restricts import visibility. A package at a/b/internal/c can only be imported by code rooted at a/b. This is enforced by the Go toolchain and helps maintain API boundaries within large repositories.
Circular Imports Go does not allow circular import dependencies. If package A imports B and B imports A, the build fails. Resolve cycles by extracting shared types to a third package that both can import, or by passing values through interfaces.
Package Naming Conventions
- Short, lowercase, single-word names preferred:
fmt,http,sync - Avoid underscores or mixed caps in package names
- The package name should describe what it provides, not what it contains (
bytesnotbyteutils)
Code Examples
package main
import "fmt"
// Exported: accessible from other packages
type Config struct {
Host string
Port int
timeout int // unexported field — only accessible in this package
}
// Exported constructor returning exported type
func NewConfig(host string, port int) *Config {
return &Config{
Host: host,
Port: port,
timeout: 30, // set internally; caller cannot set this directly
}
}
// Exported method
func (c *Config) Address() string {
return fmt.Sprintf("%s:%d", c.Host, c.Port)
}
// unexported helper — not visible to other packages
func validate(c *Config) bool {
return c.Host != "" && c.Port > 0 && c.Port <= 65535
}
func main() {
cfg := NewConfig("localhost", 8080)
fmt.Println("Address:", cfg.Address())
fmt.Println("Valid:", validate(cfg))
// cfg.timeout would be accessible here (same package)
fmt.Println("Timeout:", cfg.timeout)
// Exported fields can be set directly
cfg.Host = "example.com"
fmt.Println("Updated address:", cfg.Address())
}Capitalisation controls visibility. The timeout field is set by the constructor but cannot be accessed by external packages. validate() is package-private. This is Go's entire access control system.
package main
import "fmt"
// Package-level variables are initialised first
var globalDB = initDB()
func initDB() string {
fmt.Println("1. initDB() called — package var init")
return "db_connection"
}
// init() runs after package-level vars, before main()
func init() {
fmt.Println("2. first init() — could register drivers, validate config")
}
func init() {
// Multiple init() functions are allowed in one file
fmt.Println("3. second init() — runs after first init()")
if globalDB == "" {
panic("database not initialised")
}
}
func main() {
fmt.Println("4. main() — all init()s have already run")
fmt.Println("DB:", globalDB)
}Package-level variable initialisers run first, then all init() functions in source order, then main(). Multiple init() functions in the same file are permitted and run top to bottom.
package main
import (
"fmt"
"math/rand"
// Alias to disambiguate two packages with the same base name
mrand "math/rand"
// Blank import: import purely for side-effects of init()
// In a real program you would use:
// _ "image/png" — registers PNG decoder
// _ "github.com/lib/pq" — registers PostgreSQL driver
)
// Simulate a self-registering plugin pattern (like database drivers)
type Plugin interface {
Name() string
}
var registeredPlugins []Plugin
func RegisterPlugin(p Plugin) {
registeredPlugins = append(registeredPlugins, p)
fmt.Println("registered plugin:", p.Name())
}
type jsonPlugin struct{}
func (j jsonPlugin) Name() string { return "json" }
func init() {
// A plugin package would call Register in its own init()
RegisterPlugin(jsonPlugin{})
}
func main() {
// Using package alias
n := mrand.Intn(100)
_ = rand.Intn(100) // rand and mrand refer to the same package here
fmt.Println("random number:", n)
fmt.Println("plugins:", len(registeredPlugins))
for _, p := range registeredPlugins {
fmt.Println(" -", p.Name())
}
}init() in a plugin package registers the plugin before main() runs — the blank import pattern. Package aliases resolve name conflicts at the import site. The blank identifier suppresses 'imported and not used' errors.
Quick Quiz
1. How does Go determine whether a name is exported (public)?
2. When does an init() function run?
3. What is the purpose of `import _ "some/package"`?
4. What happens if package A imports package B and package B imports package A?
Was this lesson helpful?