4 min read
On this page

Project Setup & Tooling

Go ships with everything you need to build, test, format, and manage dependencies. There are no build files, no package manager configs, and no IDE-specific project files. The go command is the build system, dependency manager, test runner, and formatter all in one.

Starting a Project

Every Go project starts with go mod init. This creates a go.mod file that declares your module path and Go version.

// Terminal: initialize a new module
// The module path is typically your repository URL
$ mkdir myservice && cd myservice
$ go mod init github.com/yourorg/myservice
go: creating new go.mod: module github.com/yourorg/myservice

This produces a go.mod file:

module github.com/yourorg/myservice

go 1.22

That is your entire project configuration. No pom.xml, no package.json, no Cargo.toml with 50 fields. Just the module path and the Go version.

go.mod & go.sum

go.mod

The go.mod file tracks your module name, Go version, and direct dependencies. When you import a third-party package and run go mod tidy, Go adds it here automatically.

module github.com/yourorg/myservice

go 1.22

require (
    github.com/go-chi/chi/v5 v5.0.12
    github.com/jackc/pgx/v5 v5.5.5
)

require (
    // indirect dependencies are managed automatically
    github.com/jackc/pgpassfile v1.0.0 // indirect
    github.com/jackc/pgservicefile v0.0.0-20231201171823-440d19dc326d // indirect
)

go.sum

The go.sum file contains cryptographic hashes of every dependency. It ensures reproducible builds -- if someone tampers with a module, the hash mismatch will cause a build failure. You never edit this file manually. Commit it to version control.

Key Commands for Dependencies

$ go mod tidy          # Add missing, remove unused dependencies
$ go mod download      # Download dependencies to local cache
$ go mod vendor        # Copy dependencies into vendor/ directory
$ go mod why <pkg>     # Explain why a package is a dependency
$ go get <pkg>@latest  # Add or update a dependency
$ go get <pkg>@v1.2.3  # Pin to a specific version

The Go Toolchain

go run

Compiles and runs a Go file in one step. Great for development, not for production.

$ go run main.go
$ go run .              # Run the package in the current directory
$ go run ./cmd/server   # Run a specific sub-command

go build

Compiles your code into a binary. The binary name defaults to the module or directory name.

$ go build -o myservice ./cmd/server
$ ls -la myservice
-rwxr-xr-x  1 user  staff  8234567  myservice

Cross-compilation is built in. Set GOOS and GOARCH to build for any supported platform:

$ GOOS=linux GOARCH=amd64 go build -o myservice-linux ./cmd/server
$ GOOS=darwin GOARCH=arm64 go build -o myservice-mac ./cmd/server
$ GOOS=windows GOARCH=amd64 go build -o myservice.exe ./cmd/server

No special toolchain installation required. It just works.

go test

Runs tests. Test files are named *_test.go and live alongside the code they test.

$ go test ./...                 # Test all packages recursively
$ go test -v ./internal/auth    # Verbose output for one package
$ go test -run TestLogin ./...  # Run tests matching a pattern
$ go test -race ./...           # Enable the race detector
$ go test -cover ./...          # Show coverage percentage

go fmt

Formats your code according to Go's canonical style. Tabs for indentation, specific alignment rules, consistent spacing. There are no options. There are no debates.

$ gofmt -w .           # Format all files in place
$ gofmt -d .           # Show diffs without modifying

Most editors run gofmt on save. If yours does not, configure it to do so. Every Go file in every Go project looks the same, and that is the point.

go vet

Static analysis that catches common mistakes the compiler misses: unreachable code, suspicious format strings, struct tags with typos, copying mutexes.

$ go vet ./...

Run go vet as part of your CI pipeline. It catches real bugs.

Project Structure

Go does not enforce a project structure, but conventions have emerged. Here are the practical options.

Small Projects: Keep It Flat

For a CLI tool, small library, or simple service, a flat structure works fine:

myservice/
    go.mod
    go.sum
    main.go
    handler.go
    handler_test.go
    store.go
    store_test.go

Do not create directories until you need them. Start flat, organize later.

Medium to Large Projects: cmd/ & internal/

When the project grows, the standard layout uses cmd/ for entry points and internal/ for private packages.

myservice/
    go.mod
    go.sum
    cmd/
        server/
            main.go        # Entry point for the API server
        worker/
            main.go        # Entry point for the background worker
        migrate/
            main.go        # Entry point for DB migrations
    internal/
        auth/
            auth.go
            auth_test.go
        store/
            postgres.go
            postgres_test.go
        handler/
            routes.go
            user.go
            user_test.go

What These Directories Mean

cmd/ -- Each subdirectory is a separate binary. Each contains a main.go with package main. This is where func main() lives and nowhere else.

internal/ -- Packages here cannot be imported by code outside your module. The Go compiler enforces this. Use internal/ for code that is specific to your application and should not be treated as a public API.

pkg/ -- Some projects use pkg/ for packages intended to be imported by other projects. This convention is falling out of favor. If you are building a library, put your packages at the module root. If you are building an application, put them in internal/.

// cmd/server/main.go
package main

import (
    "log"
    "net/http"

    "github.com/yourorg/myservice/internal/handler"
    "github.com/yourorg/myservice/internal/store"
)

func main() {
    db, err := store.Connect("postgres://localhost/myservice")
    if err != nil {
        log.Fatal(err)
    }
    defer db.Close()

    router := handler.NewRouter(db)
    log.Println("listening on :8080")
    log.Fatal(http.ListenAndServe(":8080", router))
}

go install for Tools

go install builds and installs a binary to $GOPATH/bin (or $GOBIN if set). Use it for installing Go-based development tools.

$ go install golang.org/x/tools/gopls@latest           # Go language server
$ go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
$ go install github.com/air-verse/air@latest            # Live reload

These are system-wide tools, not project dependencies. They do not appear in your go.mod.

Build Tags & ldflags

Build Tags

Conditional compilation via build tags lets you include or exclude files based on OS, architecture, or custom tags.

//go:build linux

package myservice

// This file is only compiled on Linux

ldflags for Version Injection

Inject build-time variables (version, commit hash) without config files:

$ go build -ldflags "-X main.version=1.2.3 -X main.commit=$(git rev-parse --short HEAD)" -o myservice
package main

var (
    version = "dev"
    commit  = "none"
)

func main() {
    fmt.Printf("myservice %s (%s)\n", version, commit)
}

A Practical Workflow

Here is what a typical development loop looks like:

$ go mod init github.com/yourorg/myservice   # Once, at project creation
$ go mod tidy                                 # After adding/removing imports
$ go fmt ./...                                # Before every commit (or on save)
$ go vet ./...                                # Before every commit
$ go test ./...                               # Before every commit
$ go build -o myservice ./cmd/server          # When you need a binary
$ go test -race -cover ./...                  # In CI

Common Pitfalls

  • Creating complex directory structures for small projects. Start flat. Add cmd/ and internal/ when you actually have multiple binaries or need to hide packages. Over-structuring a 500-line project is waste.
  • Vendoring without a reason. Go modules with go.sum already guarantee reproducible builds. Vendoring is useful for air-gapped environments or when you want dependencies visible in code review, but it is not the default.
  • Forgetting go mod tidy. If you add an import and your build fails, run go mod tidy. It resolves missing dependencies and removes unused ones.
  • Not running gofmt. If your team is debating formatting, you are doing it wrong. Run gofmt on save. The discussion is over.
  • Using GOPATH mode. GOPATH-based dependency management is legacy. Always use modules (go mod init). If you are following a tutorial that mentions GOPATH without modules, find a newer tutorial.

Key Takeaways

  • go mod init creates your project. go.mod and go.sum manage dependencies. That is the entire build configuration.
  • The go command handles building, testing, formatting, vetting, and dependency management. No third-party build tools required.
  • Start with a flat project structure. Graduate to cmd/ and internal/ when complexity demands it.
  • gofmt enforces one canonical format. Run it on every save. There is nothing to configure.
  • Cross-compilation is built in: set GOOS and GOARCH, then go build.
  • Use go install for development tools, not for project dependencies.