Familiar with Go Modules and Packages

Introduction

From JS to Go, I wasn’t very familiar with how Go modules work. Although there are similarities, the experience feels very simple, even minimalistic at times.

Initializing a Go Module

Terminal window
# Initialize a Go Module: The Module Path must be unique, typically using a GitHub Repo path.
go mod init github.com/riceball-tw/project
# Install a Go Module
go get github.com/fatih/color

You will get the following go.mod and go.sum plain text files to record module dependencies and the Go version. Packages that are not directly referenced will be marked as indirect:

go.mod
module github.com/riceball-tw/project
go 1.25.6
require (
github.com/fatih/color v1.18.0 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
golang.org/x/sys v0.25.0 // indirect
)

main is the entry point

All compiled Go programs will always start by executing main() in package main — no exceptions. Multiple main packages can exist within the same module.

package main
func main()

Creating different packages

By convention, folder names are usually the package names, but it’s not mandatory.

greetings/
└── greetings.go
go.mod
go.sum
main.go
greetings.go
package greetings
import "fmt"
func Hello(name string) {
fmt.Printf("Hello %s!", name)
}

Capitalization controls whether variables are exported

A clever design in Go is that the capitalization of variables or functions controls whether other packages can access them. It’s a very unique approach without extra keywords or conventions.

  • Uppercase first letter: indicates exported (public), accessible by other packages
  • Lowercase first letter: indicates unexported (private), usable only within the same package

Importing other packages

By using import to combine the module name and package path, you can use identifiers from that package:

package main
import "github.com/riceball-tw/project/greetings"
func main() {
greetings.Hi("test")
}

Importing with package aliases

You can also use an alias to simplify a long package name:

import g "github.com/riceball-tw/project/greetings"
func main() {
g.Hello("test")
}

Internal package

Go has a special internal directory mechanism. Packages placed under an internal directory can only be imported by code in the parent directory and its subdirectories:

myproject/
├── internal/
│ └── helper/
│ └── helper.go # It can only be used by code within myproject package.
├── greetings/
│ └── greetings.go
└── main.go

Package testing

Go’s testing tools are built into the language. Test files must end with _test.go, and test functions must start with Test and accept a *testing.T parameter.

greetings.go
package greetings
// Hello is public function
func Hello(name string) string {
return formatMessage(name)
}
// formatMessage is private function
func formatMessage(name string) string {
return "Hello " + name + "!"
}
greetings_test.go - White-box testing
package greetings // Note: no _test
func TestHello(t *testing.T) {
Hello("World") // ✅ Can be call
}
func TestFormatMessage(t *testing.T) {
formatMessage("World") // ✅ Private function can be call
}
greetings_test.go - Black-box testing
package greetings_test // Note: Has _test
import "github.com/riceball-tw/project/greetings"
func TestHello(t *testing.T) {
greetings.Hello("World") // ✅ Can be call
}
func TestFormatMessage(t *testing.T) {
greetings.formatMessage("World") // ❌ Compile error! Can not access private function
}

Go provides a unique testing approach: if you use package packagename_test (with the _test suffix) in a test file, the test code can only access the package’s public API, simulating an external user’s perspective:

  • Enforces testing via public API: ensures tests are written from a user’s viewpoint and only test the public interface
  • Avoids testing implementation details: private functions are inaccessible, preventing over-coupling between tests and implementation
  • Better encapsulation: if internal implementation changes but the public API remains the same, tests don’t need to change

Summary

  • A module can contain multiple packages
  • Package access is controlled by capitalization: uppercase for exported, lowercase for unexported
  • Every executable Go program must have a main package and a main() function
  • go.mod manages dependencies; go.sum manages package checksums
  • The internal directory provides private package mechanisms within a module

Further reading