Table of contents
- How to Organize Code
- Packages — The Basic Unit of Code
- Uppercase and Lowercase — Go’s Visibility Rules
- go mod — Module Management
- init() — Package Initialization
- Project Structure Conventions
- internal Packages — Compiler-Enforced Boundaries
- Import Organization
How to Organize Code
All the examples so far have been crammed into a single main() function in package main. That’s fine for practice, but in real projects, code grows to thousands of lines and structure becomes necessary. In Go, the unit of code organization is the package, and the unit of dependency management is the module.
Packages — The Basic Unit of Code
In Go, every .go file must belong to a package. Files in the same directory must use the same package name, and different directories become different packages.
myproject/
├── go.mod
├── main.go // package main
├── calc/
│ ├── math.go // package calc
│ └── math_test.go // package calc
└── greeting/
└── hello.go // package greeting
Let’s create calc/math.go.
// calc/math.go
package calc
// Add adds two integers.
func Add(a, b int) int {
return a + b
}
// subtract is an internal-only function.
func subtract(a, b int) int {
return a - b
}
Using this package from main.go:
// main.go
package main
import (
"fmt"
"myproject/calc"
)
func main() {
fmt.Println(calc.Add(3, 5)) // 8
// fmt.Println(calc.subtract(3, 5)) // Compile error — lowercase, not visible
}
calc.Add is accessible but calc.subtract is not. The reason is in the next section.
Uppercase and Lowercase — Go’s Visibility Rules
Go’s access control is remarkably simple. If a name starts with an uppercase letter, it’s accessible from outside the package (exported); if lowercase, it’s package-internal only (unexported). That’s it.
package user
// User — starts with uppercase, accessible from other packages
type User struct {
Name string // Uppercase — externally accessible
Email string // Uppercase — externally accessible
age int // Lowercase — only accessible within this package
}
// NewUser — starts with uppercase, callable from outside
func NewUser(name, email string, age int) *User {
return &User{Name: name, Email: email, age: age}
}
// validate — starts with lowercase, internal use only
func validate(email string) bool {
return len(email) > 0
}
There are no public, private, or protected keywords. One rule — uppercase/lowercase — handles everything, making visibility immediately apparent when reading code. It might feel unfamiliar at first, but once you adapt, it’s hard to imagine a more concise access control system.
go mod — Module Management
A Go module is a collection of packages. The go.mod file marks the module root and manages the module name, Go version, and external dependencies.
When starting a new project:
mkdir myproject && cd myproject
go mod init github.com/username/myproject
This command creates the go.mod file.
module github.com/username/myproject
go 1.22
When you use external packages, dependencies are automatically added.
go get golang.org/x/text
Dependencies are recorded in go.mod, and checksums are stored in go.sum. You won’t need to edit go.sum directly, but it must be included in version control. It ensures that others building the same code get identical dependencies.
Here’s a summary of commonly used module commands.
go mod init MODULE_NAME # Initialize new module
go mod tidy # Clean up unused deps, add missing ones
go get PACKAGE@VERSION # Add a specific version of a package
go list -m all # List all dependencies
Running go mod tidy regularly is a good habit. After adding or removing imports in your code, running it keeps go.mod and go.sum clean.
init() — Package Initialization
Each package can define an init() function. This function runs automatically when the program starts, before main().
package config
import "fmt"
var DefaultPort int
func init() {
DefaultPort = 8080
fmt.Println("config package initialized")
}
package main
import (
"fmt"
"myproject/config"
)
func main() {
// "config package initialized" prints first
fmt.Println("server port:", config.DefaultPort) // 8080
}
The execution order is:
init()of imported packages (in dependency order)init()of the main packagemain()
Mapping this sequence to a diagram shows the flow of program startup.
sequenceDiagram
participant Runtime as Go Runtime
participant Dep as Dependency packages
participant Main as main package
participant User as main()
Runtime->>Dep: Initialize vars in dependency order
Runtime->>Dep: Call init() (if any)
Dep-->>Runtime: Ready
Runtime->>Main: Initialize main package vars
Runtime->>Main: Call main.init() (if any)
Runtime->>User: Execute main()
User-->>Runtime: Program exits
init() is used for package-level variable initialization, loading config files, registering drivers, and similar purposes. The classic example is blank-importing a database driver package like import _ "github.com/lib/pq". You don’t use the package directly, but its init() side effect (driver registration) is needed.
However, overuse of init() should be avoided. The implicit initialization order makes debugging harder, and unwanted side effects can occur during testing. Unless truly necessary, creating an explicit initialization function is often the better choice.
Project Structure Conventions
Go doesn’t have an officially enforced project structure, but there are widely adopted community conventions.
myproject/
├── cmd/
│ └── server/
│ └── main.go # Executable entry point
├── internal/
│ ├── handler/
│ │ └── user.go # HTTP handlers
│ └── service/
│ └── user.go # Business logic
├── pkg/
│ └── validator/
│ └── email.go # Externally reusable utilities
├── go.mod
└── go.sum
Each directory’s role:
- cmd/: Entry points for executable binaries. Contains
mainpackages. Multiple binaries go incmd/server/,cmd/worker/, etc. - internal/: Packages used only within the project. The Go compiler blocks import from external modules.
- pkg/: Library code that external consumers may use. Recently, many projects skip
pkg/and place code at the root level.
For small projects, there’s no need to force this structure. If a single package suffices, not splitting into directories is actually cleaner. Structure should emerge naturally as the code grows — creating empty directories upfront is over-engineering.
internal Packages — Compiler-Enforced Boundaries
The internal directory has special meaning in Go. Packages inside this directory can only be imported from within the parent directory tree of internal.
myproject/
├── cmd/
│ └── server/
│ └── main.go # Can use internal/handler
├── internal/
│ └── handler/
│ └── user.go # Only accessible within this project
└── go.mod
// cmd/server/main.go — OK
import "myproject/internal/handler"
// From another module — compile error
// import "myproject/internal/handler"
// use of internal package myproject/internal/handler not allowed
While the uppercase/lowercase rule controls visibility within packages, internal controls visibility at module boundaries. Placing implementation details you don’t want to expose in internal lets the compiler prevent accidental external usage.
This is especially useful when building libraries. Put public APIs at the root or in pkg/, and hide internal implementations in internal/ to keep the API surface clean.
Import Organization
Import blocks in Go files are typically organized into three groups. Separating them with blank lines lets the goimports tool auto-sort them.
import (
// Standard library
"fmt"
"net/http"
// External packages
"github.com/gorilla/mux"
"go.uber.org/zap"
// Project internal packages
"myproject/internal/handler"
"myproject/internal/service"
)
In Go, importing something and not using it causes a compile error. This strict rule might feel inconvenient at first, but it prevents unused code from accumulating, which is increasingly appreciated as projects grow.
With Go plugins installed in your editor (VS Code, GoLand), imports are auto-organized on save, so you rarely need to manage them manually.
Packages and modules are the skeleton of a Go project. Visibility is controlled with uppercase/lowercase, dependencies are managed with go.mod, and boundaries are guarded with internal. These rules may seem simple, but they provide tremendous power in maintaining consistent code structure as projects scale. Rather than trying to design the perfect structure from the start, letting packages naturally separate as code grows is the Go way.
The next part covers generics introduced in Go 1.18, along with table-driven tests, build tags, and go generate. As the final part of the series, it will be a time to synthesize everything we’ve learned.




Loading comments...