Packages, Modules and Tooling in Go — Go Course #8
Introduction: Go's package system

Welcome to part 8 of our Go course for beginners. In this article you will learn how Go organizes code into packages, how to manage dependencies with modules, and how to leverage the powerful tools that ship with Go out of the box.
In Go, every file belongs to a package. A package is simply a directory containing one or more .go files with the same package declaration on the first line. Packages are the fundamental unit of code organization and reuse.
1// File: main.go
2package main // This file belongs to the "main" package
3
4import "fmt" // Import the "fmt" package from the standard library
5
6func main() {
7 fmt.Println("Hello from the main package")
8}
main package is special. It is the entry point of your program and must contain a main() function. When you run go run or go build, Go looks for this package to create the executable.
Exported names: the capitalization convention
In Go, the visibility of a name is determined by its first letter. This is one of the simplest and most elegant conventions in the language:
- Uppercase initial = exported (public): accessible from other packages. Example:
fmt.Println,math.Pi - Lowercase initial = unexported (private): only accessible within the same package. Example:
internal.helper
1package calculator
2
3import "math"
4
5// Add is exported - other packages can use it
6func Add(a, b int) int {
7 return a + b
8}
9
10// validate is NOT exported - only used within this package
11func validate(n int) bool {
12 return n >= 0
13}
14
15// Pi is an exported constant
16const Pi = 3.14159
17
18// maxRetries is a private constant
19const maxRetries = 5
public, private, or protected like Java or C#. The first letter says it all. It is simple, consistent, and unambiguous.
Go Modules: go.mod, go.sum and dependency management
Go Modules have been the official dependency management system since Go 1.11. Every Go project is a module, defined by a go.mod file at the project root.
Initializing a module
1# Create a directory for your project
2mkdir my-project
3cd my-project
4
5# Initialize the module
6go mod init github.com/your-user/my-project
7
8# This creates the go.mod file
The generated go.mod file looks like this:
1module github.com/your-user/my-project
2
3go 1.22
Adding and cleaning dependencies
1# Download a dependency
2go get github.com/gin-gonic/gin
3
4# Clean unused dependencies and download missing ones
5go mod tidy
6
7# List all dependencies
8go list -m all
9
10# Update a dependency
11go get -u github.com/gin-gonic/gin
12
13# Update all dependencies
14go get -u ./...
The go.sum file is generated automatically and contains cryptographic hashes of each dependency. Never edit it manually. Its purpose is to ensure that dependencies have not been tampered with.
go.sum must be in your Git repository. It guarantees reproducible builds and security in the dependency chain.
Creating your own packages
Let us see how to create and use your own packages within a project. Consider this structure:
1my-project/
2 go.mod
3 main.go
4 utils/
5 math.go
6 strings.go
1// File: utils/math.go
2package utils
3
4import "math"
5
6// EuclideanDistance calculates the distance between two 2D points
7func EuclideanDistance(x1, y1, x2, y2 float64) float64 {
8 dx := x2 - x1
9 dy := y2 - y1
10 return math.Sqrt(dx*dx + dy*dy)
11}
12
13// IsPrime checks if a number is prime
14func IsPrime(n int) bool {
15 if n < 2 {
16 return false
17 }
18 for i := 2; i*i <= n; i++ {
19 if n%i == 0 {
20 return false
21 }
22 }
23 return true
24}
1// File: utils/strings.go
2package utils
3
4import "strings"
5
6// Capitalize uppercases the first letter of each word
7func Capitalize(s string) string {
8 return strings.Title(s)
9}
10
11// WordCount counts the words in a text
12func WordCount(s string) int {
13 fields := strings.Fields(s)
14 return len(fields)
15}
1// File: main.go
2package main
3
4import (
5 "fmt"
6 "github.com/your-user/my-project/utils"
7)
8
9func main() {
10 // Use functions from our utils package
11 dist := utils.EuclideanDistance(0, 0, 3, 4)
12 fmt.Printf("Distance: %.2f\n", dist) // 5.00
13
14 fmt.Println(utils.IsPrime(17)) // true
15 fmt.Println(utils.WordCount("Hello world from Go")) // 4
16}
Standard library highlights
Go has an incredibly rich standard library. Here are the packages you will use most frequently:
| Package | Purpose | Example |
|---|---|---|
fmt | Formatting and output | fmt.Printf("Hello %s", name) |
os | OS, files, environment | os.Getenv("HOME") |
io | I/O interfaces | io.Copy(dst, src) |
strings | String manipulation | strings.Contains(s, "go") |
strconv | Type conversion | strconv.Atoi("42") |
time | Date and time | time.Now() |
math | Math functions | math.Sqrt(16) |
sort | Sorting | sort.Ints(nums) |
net/http | HTTP client and server | http.Get(url) |
encoding/json | Serialize/deserialize JSON | json.Marshal(data) |
1package main
2
3import (
4 "fmt"
5 "math"
6 "sort"
7 "strings"
8 "strconv"
9 "time"
10)
11
12func main() {
13 // strings
14 fmt.Println(strings.ToUpper("hello")) // HELLO
15 fmt.Println(strings.Contains("golang", "go")) // true
16 fmt.Println(strings.Split("a,b,c", ",")) // [a b c]
17 fmt.Println(strings.Join([]string{"x","y"}, "-")) // x-y
18
19 // strconv
20 num, _ := strconv.Atoi("42")
21 fmt.Println(num + 8) // 50
22 text := strconv.Itoa(100)
23 fmt.Println(text) // "100"
24
25 // math
26 fmt.Println(math.Max(10, 20)) // 20
27 fmt.Println(math.Ceil(3.2)) // 4
28
29 // sort
30 nums := []int{5, 3, 8, 1, 9}
31 sort.Ints(nums)
32 fmt.Println(nums) // [1 3 5 8 9]
33
34 // time
35 now := time.Now()
36 fmt.Println(now.Format("2006-01-02 15:04:05"))
37}
Third-party packages: go get
Beyond the standard library, you can use thousands of community-created packages. The go get command downloads and installs packages from remote repositories.
1# Install a popular web API framework
2go get github.com/gin-gonic/gin
3
4# Install an ORM
5go get gorm.io/gorm
6go get gorm.io/driver/sqlite
7
8# Install a package for environment variables
9go get github.com/joho/godotenv
After installing, you can import and use the package normally:
1package main
2
3import (
4 "github.com/gin-gonic/gin"
5)
6
7func main() {
8 r := gin.Default()
9 r.GET("/", func(c *gin.Context) {
10 c.JSON(200, gin.H{"message": "Hello world"})
11 })
12 r.Run(":8080")
13}
pkg.go.dev, the official Go documentation and package repository. There you can view documentation, source code, versions, and dependencies for any package.
Go toolchain: the tools you already have
Go ships with a complete set of integrated tools. You do not need to install anything extra to format, analyze, test, or compile your code.
1# Build and run
2go run main.go # Compile and run in one step
3go build -o my-app . # Compile to an executable binary
4
5# Format code (mandatory in Go)
6go fmt ./... # Format all files in the project
7gofmt -w main.go # Format a specific file
8
9# Static analysis
10go vet ./... # Detect common errors the compiler misses
11
12# Testing
13go test ./... # Run all tests
14go test -v ./... # Tests with verbose output
15go test -cover ./... # Tests with coverage report
16go test -race ./... # Tests with race condition detector
17
18# Documentation
19go doc fmt # View documentation for the fmt package
20go doc fmt.Println # View documentation for a specific function
21
22# Dependencies
23go mod init module # Initialize module
24go mod tidy # Clean up dependencies
25go get package # Download package
go fmt command automatically formats your code. There are no debates about tabs vs spaces, braces on the same line or the next, etc. This eliminates unnecessary discussions in teams.
Recommended project structure
Go does not enforce a rigid directory structure, but the community has established conventions that most projects follow:
1my-project/
2 cmd/ # Entry points (executables)
3 server/
4 main.go
5 cli/
6 main.go
7 internal/ # Private code (not importable by other projects)
8 database/
9 connection.go
10 auth/
11 jwt.go
12 pkg/ # Public code (importable by other projects)
13 validator/
14 email.go
15 config/ # Configuration files
16 config.go
17 config.json
18 go.mod
19 go.sum
20 README.md
The most important directories:
- cmd/: Contains the application entry points. Each subdirectory is a different executable.
- internal/: Private packages for the project. Go prevents other modules from importing from
internal/. - pkg/: Public packages that other projects can import. This is optional.
Working with JSON: encoding/json and struct tags
The encoding/json package lets you convert between Go structs and JSON. Use struct tags to control how fields are serialized.
1package main
2
3import (
4 "encoding/json"
5 "fmt"
6)
7
8type User struct {
9 Name string "json:\"name\""
10 Email string "json:\"email\""
11 Age int "json:\"age\""
12 Active bool "json:\"active\""
13 Roles []string "json:\"roles,omitempty\""
14 Password string "json:\"-\"" // Never serialized
15}
16
17func main() {
18 // Struct to JSON (Marshal)
19 user := User{
20 Name: "Ana Garcia",
21 Email: "[email protected]",
22 Age: 28,
23 Active: true,
24 Roles: []string{"admin", "editor"},
25 Password: "secret123",
26 }
27
28 jsonBytes, err := json.MarshalIndent(user, "", " ")
29 if err != nil {
30 fmt.Println("Error:", err)
31 return
32 }
33 fmt.Println(string(jsonBytes))
34 // {
35 // "name": "Ana Garcia",
36 // "email": "[email protected]",
37 // "age": 28,
38 // "active": true,
39 // "roles": ["admin", "editor"]
40 // }
41 // Note: Password does not appear thanks to json:"-"
42
43 // JSON to Struct (Unmarshal)
44 jsonStr := "{\"name\":\"Carlos\",\"email\":\"[email protected]\",\"age\":35,\"active\":true}"
45 var user2 User
46 err = json.Unmarshal([]byte(jsonStr), &user2)
47 if err != nil {
48 fmt.Println("Error:", err)
49 return
50 }
51 fmt.Printf("Name: %s, Email: %s\n", user2.Name, user2.Email)
52}
encoding/json ignores it completely. Use struct tags to control the name in the JSON output.
Environment variables with os.Getenv
Environment variables are the standard way to configure applications in production. Go reads them with os.Getenv.
1package main
2
3import (
4 "fmt"
5 "os"
6)
7
8func getEnv(key, defaultValue string) string {
9 value := os.Getenv(key)
10 if value == "" {
11 return defaultValue
12 }
13 return value
14}
15
16func main() {
17 // Read environment variables with default values
18 port := getEnv("PORT", "8080")
19 dbHost := getEnv("DB_HOST", "localhost")
20 dbName := getEnv("DB_NAME", "my_database")
21 env := getEnv("GO_ENV", "development")
22
23 fmt.Printf("Server on port: %s\n", port)
24 fmt.Printf("Database: %s@%s\n", dbName, dbHost)
25 fmt.Printf("Environment: %s\n", env)
26}
Practical example: CLI tool that reads config from JSON
Let us build a command-line tool that reads configuration from a JSON file, validates it, and displays a formatted report.
1package main
2
3import (
4 "encoding/json"
5 "fmt"
6 "os"
7 "sort"
8 "strings"
9)
10
11// Config represents the application configuration
12type Config struct {
13 AppName string "json:\"app_name\""
14 Version string "json:\"version\""
15 Port int "json:\"port\""
16 Debug bool "json:\"debug\""
17 AllowedHosts []string "json:\"allowed_hosts\""
18 Database DBConfig "json:\"database\""
19}
20
21type DBConfig struct {
22 Host string "json:\"host\""
23 Port int "json:\"port\""
24 Name string "json:\"name\""
25 User string "json:\"user\""
26 SSLMode string "json:\"ssl_mode\""
27}
28
29func loadConfig(path string) (Config, error) {
30 var config Config
31
32 data, err := os.ReadFile(path)
33 if err != nil {
34 return config, fmt.Errorf("could not read file: %w", err)
35 }
36
37 err = json.Unmarshal(data, &config)
38 if err != nil {
39 return config, fmt.Errorf("invalid JSON: %w", err)
40 }
41
42 return config, nil
43}
44
45func validateConfig(config Config) []string {
46 var errors []string
47
48 if config.AppName == "" {
49 errors = append(errors, "app_name is required")
50 }
51 if config.Port < 1 || config.Port > 65535 {
52 errors = append(errors, "port must be between 1 and 65535")
53 }
54 if config.Database.Host == "" {
55 errors = append(errors, "database.host is required")
56 }
57 if config.Database.Name == "" {
58 errors = append(errors, "database.name is required")
59 }
60
61 return errors
62}
63
64func printReport(config Config) {
65 separator := strings.Repeat("=", 50)
66
67 fmt.Println(separator)
68 fmt.Printf(" Application: %s v%s\n", config.AppName, config.Version)
69 fmt.Println(separator)
70 fmt.Printf(" Port: %d\n", config.Port)
71 fmt.Printf(" Debug: %v\n", config.Debug)
72
73 if len(config.AllowedHosts) > 0 {
74 sort.Strings(config.AllowedHosts)
75 fmt.Printf(" Hosts: %s\n", strings.Join(config.AllowedHosts, ", "))
76 }
77
78 fmt.Println()
79 fmt.Println(" Database:")
80 fmt.Printf(" Host: %s:%d\n", config.Database.Host, config.Database.Port)
81 fmt.Printf(" Name: %s\n", config.Database.Name)
82 fmt.Printf(" User: %s\n", config.Database.User)
83 fmt.Printf(" SSL: %s\n", config.Database.SSLMode)
84 fmt.Println(separator)
85}
86
87func main() {
88 // Determine the config file path
89 path := "config.json"
90 if len(os.Args) > 1 {
91 path = os.Args[1]
92 }
93
94 fmt.Printf("Loading configuration from: %s\n\n", path)
95
96 config, err := loadConfig(path)
97 if err != nil {
98 fmt.Fprintf(os.Stderr, "Error: %v\n", err)
99 os.Exit(1)
100 }
101
102 // Validate the configuration
103 errs := validateConfig(config)
104 if len(errs) > 0 {
105 fmt.Println("Validation errors:")
106 for _, e := range errs {
107 fmt.Printf(" - %s\n", e)
108 }
109 os.Exit(1)
110 }
111
112 // Print the report
113 printReport(config)
114 fmt.Println("Configuration loaded successfully.")
115}
And the sample config.json file:
1// config.json
2{
3 "app_name": "My API",
4 "version": "1.0.0",
5 "port": 8080,
6 "debug": true,
7 "allowed_hosts": ["localhost", "api.example.com"],
8 "database": {
9 "host": "localhost",
10 "port": 5432,
11 "name": "my_database",
12 "user": "admin",
13 "ssl_mode": "disable"
14 }
15}
main.go and the JSON as config.json in the same directory. Then run go run main.go. You can also pass a different path: go run main.go /path/to/other/config.json.
Summary and next steps
In this article you learned about Go's tooling ecosystem and code organization:
- Packages: the fundamental unit of organization with
package - Exported names: the uppercase-initial convention for visibility
- Go Modules:
go.mod,go.sum,go mod init, andgo mod tidy - Creating your own packages: organizing code into reusable directories
- Standard library: fmt, os, strings, strconv, time, sort, net/http, encoding/json
- Third-party packages: installing with
go get - Toolchain: go build, go run, go test, go fmt, go vet, go doc
- Project structure: cmd/, internal/, pkg/
- JSON: encoding/json with struct tags
- Environment variables: os.Getenv for configuration
With these 8 articles you now have a solid foundation in Go. You can create concurrent programs, organize your code into packages, manage dependencies, and use the ecosystem tools. The next step is to practice by building real projects.
Comments
Sign in to leave a comment
No comments yet. Be the first!