Cristhian Villegas
Backend10 min read2 views

Packages, Modules and Tooling in Go — Go Course #8

Packages, Modules and Tooling in Go — Go Course #8

Introduction: Go's package system

Go Logo

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.

go
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}
Important rule: The 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
go
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
No keywords needed: Go does not need 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

bash
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:

go
1module github.com/your-user/my-project
2
3go 1.22

Adding and cleaning dependencies

bash
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.

Always commit go.sum to your repository: Although it looks like a generated file that should not be versioned, 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:

bash
1my-project/
2  go.mod
3  main.go
4  utils/
5    math.go
6    strings.go
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}
go
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}
go
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:

PackagePurposeExample
fmtFormatting and outputfmt.Printf("Hello %s", name)
osOS, files, environmentos.Getenv("HOME")
ioI/O interfacesio.Copy(dst, src)
stringsString manipulationstrings.Contains(s, "go")
strconvType conversionstrconv.Atoi("42")
timeDate and timetime.Now()
mathMath functionsmath.Sqrt(16)
sortSortingsort.Ints(nums)
net/httpHTTP client and serverhttp.Get(url)
encoding/jsonSerialize/deserialize JSONjson.Marshal(data)
go
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.

bash
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:

go
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}
Finding packages: You can search for Go packages on 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.

bash
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 is mandatory: Unlike other languages where formatting is a matter of preference, in Go everyone uses the same format. The 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:

bash
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.

go
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}
Fields must be exported: Only fields that start with an uppercase letter are serialized/deserialized. If a field starts with a lowercase letter, 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.

go
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.

go
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:

go
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}
Try this example: Save the Go file as 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, and go 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.

Share:
CV

Cristhian Villegas

Software Engineer specializing in Java, Spring Boot, Angular & AWS. Building scalable distributed systems with clean architecture.

Comments

Sign in to leave a comment

No comments yet. Be the first!

Related Articles