Building efficient, scalable applications with Go requires more than just understanding the language syntax. The most productive Go developers rely on a carefully selected toolkit of libraries, development tools, and environments to streamline their workflow. This guide explores the essential components of a modern Go developer’s toolkit, with practical examples and configuration tips to help you build better software faster.

Getting Started: Setting Up Your Go Environment

Before exploring specific tools and libraries, let’s ensure your Go development environment is properly configured. A solid foundation makes everything else more effective.

Installing Go

Installation varies by platform, but the process is straightforward:

macOS (using Homebrew):

brew install go

Ubuntu/Debian:

sudo apt update
sudo apt install golang-go

Windows: Download the installer from golang.org/dl and run it.

Configuring Your Environment

After installation, set up your environment variables. Add these to your .bashrc, .zshrc, or equivalent:

export GOPATH=$HOME/go
export PATH=$PATH:/usr/local/go/bin:$GOPATH/bin

The GOPATH is where your Go code, packages, and compiled binaries live. While Go modules have reduced the importance of GOPATH, having it properly set up is still beneficial for certain tools.

Verifying Your Installation

Ensure everything is working by running:

go version

You should see output like go version go1.21.0 linux/amd64 (version and platform will vary).

Essential Tools for Modern Go Development

The following tools form the backbone of efficient Go development, helping with everything from dependency management to code quality.

Go Modules: Dependency Management

Go Modules is the official dependency management system for Go. It allows you to track dependencies, their versions, and ensures reproducible builds.

How to use:

  1. Initialize a new module:

    go mod init github.com/username/projectname
    
  2. Add dependencies automatically by importing and using them in your code, then run:

    go mod tidy
    
  3. View your dependencies:

    go list -m all
    

Your dependencies are tracked in go.mod, while go.sum ensures their integrity.

Modern usage tips:

  • Use versioned imports in your code: import "github.com/user/repo/v2"
  • Pin specific versions in your go.mod with // indirect comments
  • Use workspace mode for multi-module repositories with go work

Code Quality Tools

1. golangci-lint

A fast, comprehensive linter that runs multiple linters concurrently.

Installation:

go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest

Basic usage:

golangci-lint run

Pro tip: Create a .golangci.yml configuration file to customize which linters run and their settings:

linters:
  enable:
    - gofmt
    - goimports
    - govet
    - staticcheck
    - revive
    - errcheck

linters-settings:
  errcheck:
    check-type-assertions: true
  
issues:
  exclude-rules:
    - path: _test\.go
      linters:
        - errcheck

2. gofumpt

An enhanced version of gofmt with additional formatting rules for more consistent code.

Installation:

go install mvdan.cc/gofumpt@latest

Usage:

gofumpt -l -w .

3. staticcheck

A state-of-the-art static analysis tool for finding bugs and performance issues.

Installation:

go install honnef.co/go/tools/cmd/staticcheck@latest

Usage:

staticcheck ./...

Code Generation and Documentation

1. mockgen

Generate mock implementations of interfaces for testing.

Installation:

go install go.uber.org/mock/mockgen@latest

Usage:

mockgen -source=interfaces.go -destination=mock_interfaces.go -package=mocks

2. swag

Generate Swagger documentation from Go annotations.

Installation:

go install github.com/swaggo/swag/cmd/swag@latest

Usage:

swag init -g main.go

Example annotation:

// @title User Service API
// @version 1.0
// @description Service for managing users

// @host localhost:8080
// @BasePath /api/v1

// @securityDefinitions.apikey ApiKeyAuth
// @in header
// @name Authorization

// CreateUser creates a new user
// @Summary Create a user
// @Description Create a new user in the system
// @Tags users
// @Accept json
// @Produce json
// @Param user body models.CreateUserRequest true "User information"
// @Success 201 {object} models.User
// @Failure 400 {object} models.ErrorResponse
// @Failure 500 {object} models.ErrorResponse
// @Router /users [post]
func (c *Controller) CreateUser(w http.ResponseWriter, r *http.Request) {
    // Implementation
}

3. go-bindata

Embeds assets (images, HTML, etc.) into your Go binary.

Installation:

go install github.com/kevinburke/go-bindata/go-bindata@latest

Usage:

go-bindata -o assets.go -pkg main assets/...

Development Environments and IDEs

Your choice of development environment significantly impacts productivity. Here are the top options for Go development:

1. Visual Studio Code with Go Extension

VSCode provides an excellent balance of features, performance, and customization.

Setup:

  1. Install Visual Studio Code
  2. Install the Go extension
  3. Open a Go file, and it will prompt you to install the necessary tools

Recommended settings:

{
  "go.useLanguageServer": true,
  "go.lintTool": "golangci-lint",
  "go.lintFlags": ["--fast"],
  "go.formatTool": "gofumpt",
  "go.testOnSave": true,
  "go.coverOnSave": true,
  "go.coverageDecorator": {
    "type": "highlight",
    "coveredHighlightColor": "rgba(64,128,128,0.5)",
    "uncoveredHighlightColor": "rgba(128,64,64,0.25)"
  },
  "go.testFlags": ["-v"],
  "[go]": {
    "editor.formatOnSave": true,
    "editor.codeActionsOnSave": {
      "source.organizeImports": true
    }
  }
}

Key features:

  • IntelliSense (autocompletion and parameter info)
  • Code navigation
  • Debugging support
  • Integrated testing and coverage visualization
  • Refactoring tools

2. GoLand

JetBrains’ dedicated Go IDE offers the most comprehensive feature set.

Key features:

  • Deep language understanding
  • Advanced refactoring tools
  • Built-in database tools
  • Docker and Kubernetes integration
  • Advanced debugger
  • Profiling tools integration

Pro tip: GoLand’s “File Watcher” can run linters and formatters automatically when files change:

  1. Go to Settings/PreferencesToolsFile Watchers
  2. Add a new watcher for gofumpt or golangci-lint

3. Vim/Neovim with Go Plugins

For developers who prefer terminal-based workflows, Vim with the right plugins provides an efficient experience.

Basic setup using vim-plug:

call plug#begin()
Plug 'fatih/vim-go', { 'do': ':GoUpdateBinaries' }
Plug 'neoclide/coc.nvim', {'branch': 'release'}
Plug 'preservim/tagbar'
call plug#end()

" Go syntax highlighting
let g:go_highlight_fields = 1
let g:go_highlight_functions = 1
let g:go_highlight_function_calls = 1
let g:go_highlight_types = 1

" Auto formatting and importing
let g:go_fmt_autosave = 1
let g:go_fmt_command = "goimports"

" Status line types/signatures
let g:go_auto_type_info = 1

" Run :GoBuild or :GoTestCompile based on the go file
function! s:build_go_files()
  let l:file = expand('%')
  if l:file =~# '^\f\+_test\.go$'
    call go#test#Test(0, 1)
  elseif l:file =~# '^\f\+\.go$'
    call go#cmd#Build(0)
  endif
endfunction

autocmd FileType go nmap <leader>b :<C-u>call <SID>build_go_files()<CR>
autocmd FileType go nmap <leader>r  <Plug>(go-run)
autocmd FileType go nmap <leader>t  <Plug>(go-test)

Essential Libraries for Go Development

The following libraries address common needs in Go applications and help you avoid reinventing the wheel.

Web Development

1. gorilla/mux - HTTP Router

Powerful URL router and dispatcher for matching URL paths to handlers.

package main

import (
    "fmt"
    "net/http"
    "github.com/gorilla/mux"
)

func main() {
    r := mux.NewRouter()
    
    // Route handlers
    r.HandleFunc("/", homeHandler)
    r.HandleFunc("/products", productsHandler)
    r.HandleFunc("/products/{id:[0-9]+}", productHandler)
    
    // Serve static files
    r.PathPrefix("/static/").Handler(http.StripPrefix("/static/", http.FileServer(http.Dir("./static/"))))
    
    // Middleware
    r.Use(loggingMiddleware)
    
    // Start server
    http.ListenAndServe(":8080", r)
}

func homeHandler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Welcome to the home page!")
}

func productsHandler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintf(w, "Products page")
}

func productHandler(w http.ResponseWriter, r *http.Request) {
    vars := mux.Vars(r)
    productID := vars["id"]
    fmt.Fprintf(w, "Product %s", productID)
}

func loggingMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        fmt.Println(r.RequestURI)
        next.ServeHTTP(w, r)
    })
}

2. chi - Lightweight, composable router

An alternative to gorilla/mux, chi is lightweight and composable.

package main

import (
    "net/http"
    "github.com/go-chi/chi/v5"
    "github.com/go-chi/chi/v5/middleware"
)

func main() {
    r := chi.NewRouter()
    
    // Middleware stack
    r.Use(middleware.RequestID)
    r.Use(middleware.RealIP)
    r.Use(middleware.Logger)
    r.Use(middleware.Recoverer)
    
    // Routes
    r.Get("/", func(w http.ResponseWriter, r *http.Request) {
        w.Write([]byte("Welcome"))
    })
    
    r.Route("/users", func(r chi.Router) {
        r.Get("/", listUsers)
        r.Post("/", createUser)
        
        r.Route("/{userID}", func(r chi.Router) {
            r.Get("/", getUser)
            r.Put("/", updateUser)
            r.Delete("/", deleteUser)
        })
    })
    
    http.ListenAndServe(":8080", r)
}

func listUsers(w http.ResponseWriter, r *http.Request) {
    // Implementation
}

func createUser(w http.ResponseWriter, r *http.Request) {
    // Implementation
}

func getUser(w http.ResponseWriter, r *http.Request) {
    userID := chi.URLParam(r, "userID")
    // Implementation using userID
}

func updateUser(w http.ResponseWriter, r *http.Request) {
    // Implementation
}

func deleteUser(w http.ResponseWriter, r *http.Request) {
    // Implementation
}

3. gin - Web framework

A fast, minimalist web framework with features like routing, middleware, and rendering.

package main

import (
    "net/http"
    "github.com/gin-gonic/gin"
)

type User struct {
    ID    string `json:"id"`
    Name  string `json:"name"`
    Email string `json:"email"`
}

func main() {
    r := gin.Default() // Includes Logger and Recovery middleware
    
    r.GET("/ping", func(c *gin.Context) {
        c.JSON(http.StatusOK, gin.H{
            "message": "pong",
        })
    })
    
    // Group routes
    api := r.Group("/api")
    {
        api.GET("/users", getUsers)
        api.GET("/users/:id", getUserByID)
        api.POST("/users", createUser)
    }
    
    r.Run(":8080")
}

func getUsers(c *gin.Context) {
    users := []User{
        {ID: "1", Name: "John Doe", Email: "john@example.com"},
        {ID: "2", Name: "Jane Smith", Email: "jane@example.com"},
    }
    
    c.JSON(http.StatusOK, users)
}

func getUserByID(c *gin.Context) {
    id := c.Param("id")
    
    // In a real app, fetch from database
    user := User{ID: id, Name: "John Doe", Email: "john@example.com"}
    
    c.JSON(http.StatusOK, user)
}

func createUser(c *gin.Context) {
    var newUser User
    
    if err := c.ShouldBindJSON(&newUser); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
        return
    }
    
    // In a real app, save to database
    
    c.JSON(http.StatusCreated, newUser)
}

HTTP Clients

1. resty - Simple HTTP client

An expressive, simple HTTP client with a fluent API.

package main

import (
    "fmt"
    "github.com/go-resty/resty/v2"
)

type User struct {
    ID    int    `json:"id"`
    Name  string `json:"name"`
    Email string `json:"email"`
}

func main() {
    client := resty.New()
    
    // Set common headers, base URL, etc.
    client.SetHeader("Content-Type", "application/json")
    client.SetBaseURL("https://api.example.com")
    client.SetTimeout(10 * time.Second)
    
    // GET request
    resp, err := client.R().
        SetQueryParams(map[string]string{
            "page": "1",
            "limit": "10",
        }).
        Get("/users")
    
    if err != nil {
        fmt.Println("Error:", err)
        return
    }
    
    fmt.Println("Status:", resp.Status())
    fmt.Println("Body:", resp.String())
    
    // POST request with JSON
    user := &User{
        Name:  "John Doe",
        Email: "john@example.com",
    }
    
    resp, err = client.R().
        SetBody(user).
        SetResult(&User{}).  // Unmarshal response into User struct
        Post("/users")
    
    if err != nil {
        fmt.Println("Error:", err)
        return
    }
    
    createdUser := resp.Result().(*User)
    fmt.Println("Created user:", createdUser.ID, createdUser.Name)
}

Database Access

1. sqlx - Enhanced database access

An extension of the standard database/sql package with additional functionality.

package main

import (
    "fmt"
    "log"
    
    _ "github.com/lib/pq"
    "github.com/jmoiron/sqlx"
)

type User struct {
    ID        int    `db:"id"`
    FirstName string `db:"first_name"`
    LastName  string `db:"last_name"`
    Email     string `db:"email"`
}

func main() {
    // Connect to database
    db, err := sqlx.Connect("postgres", "user=postgres dbname=testdb sslmode=disable")
    if err != nil {
        log.Fatalln(err)
    }
    
    // Create schema
    schema := `
    CREATE TABLE IF NOT EXISTS users (
        id SERIAL PRIMARY KEY,
        first_name TEXT,
        last_name TEXT,
        email TEXT UNIQUE
    );`
    
    db.MustExec(schema)
    
    // Insert data using named parameters
    tx := db.MustBegin()
    tx.MustExec("INSERT INTO users (first_name, last_name, email) VALUES ($1, $2, $3)",
        "John", "Doe", "john@example.com")
    tx.NamedExec("INSERT INTO users (first_name, last_name, email) VALUES (:first, :last, :email)",
        map[string]interface{}{
            "first": "Jane",
            "last":  "Smith",
            "email": "jane@example.com",
        })
    tx.Commit()
    
    // Query data
    users := []User{}
    err = db.Select(&users, "SELECT * FROM users")
    if err != nil {
        log.Fatalln(err)
    }
    
    for _, user := range users {
        fmt.Printf("%d: %s %s (%s)\n", user.ID, user.FirstName, user.LastName, user.Email)
    }
    
    // Get a single record
    var user User
    err = db.Get(&user, "SELECT * FROM users WHERE id=$1", 1)
    if err != nil {
        log.Fatalln(err)
    }
    
    fmt.Printf("User 1: %s %s\n", user.FirstName, user.LastName)
}

A developer-friendly ORM with full-featured functionality.

package main

import (
    "fmt"
    "log"
    
    "gorm.io/driver/postgres"
    "gorm.io/gorm"
)

type Product struct {
    gorm.Model
    Code  string
    Price uint
}

func main() {
    dsn := "host=localhost user=postgres password=postgres dbname=testdb port=5432 sslmode=disable"
    db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
    if err != nil {
        log.Fatalln("Failed to connect to database:", err)
    }
    
    // Auto Migrate
    db.AutoMigrate(&Product{})
    
    // Create
    db.Create(&Product{Code: "D42", Price: 100})
    
    // Read
    var product Product
    db.First(&product, 1) // Find product with ID = 1
    fmt.Println("Product 1:", product.Code, product.Price)
    
    db.First(&product, "code = ?", "D42") // Find product with code = D42
    fmt.Println("Product D42:", product.ID, product.Price)
    
    // Update
    db.Model(&product).Updates(Product{Price: 200, Code: "D42"}) // Update non-zero fields
    
    // Delete
    db.Delete(&product, 1) // Delete product with ID = 1
}

Logging and Observability

1. zap - Fast, structured logger

A fast, structured logger with leveled logging and sampling.

package main

import (
    "go.uber.org/zap"
    "go.uber.org/zap/zapcore"
    "time"
)

func main() {
    // Production logger with JSON formatting
    logger, _ := zap.NewProduction()
    defer logger.Sync()
    
    logger.Info("Production logger initialized")
    
    // Log with structured fields
    logger.Info("User logged in",
        zap.String("username", "john_doe"),
        zap.String("ip", "192.168.1.1"),
        zap.Duration("latency", time.Millisecond*53),
    )
    
    // Custom logger configuration
    config := zap.NewProductionConfig()
    config.OutputPaths = []string{"stdout", "/var/log/myapp.log"}
    config.EncoderConfig.TimeKey = "timestamp"
    config.EncoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
    
    customLogger, _ := config.Build()
    defer customLogger.Sync()
    
    customLogger.Error("Failed login attempt",
        zap.String("username", "unknown"),
        zap.String("ip", "10.0.0.1"),
        zap.Int("attempts", 5),
    )
    
    // Create a sugared logger (slightly slower but more convenient API)
    sugar := logger.Sugar()
    sugar.Infow("Sugared logger", "key", "value", "count", 42)
    sugar.Infof("User %s logged in from %s", "jane_doe", "10.0.0.2")
}

2. prometheus client - Metrics collection

Collect and expose metrics for Prometheus monitoring.

package main

import (
    "net/http"
    "github.com/prometheus/client_golang/prometheus"
    "github.com/prometheus/client_golang/prometheus/promauto"
    "github.com/prometheus/client_golang/prometheus/promhttp"
)

var (
    httpRequestsTotal = promauto.NewCounterVec(
        prometheus.CounterOpts{
            Name: "http_requests_total",
            Help: "Total number of HTTP requests",
        },
        []string{"method", "endpoint", "status"},
    )
    
    httpDuration = promauto.NewHistogramVec(
        prometheus.HistogramOpts{
            Name:    "http_request_duration_seconds",
            Help:    "HTTP request duration in seconds",
            Buckets: prometheus.DefBuckets,
        },
        []string{"method", "endpoint"},
    )
)

func main() {
    // Register metrics handler
    http.Handle("/metrics", promhttp.Handler())
    
    // API endpoints
    http.HandleFunc("/api/users", metricsMiddleware(usersHandler))
    http.HandleFunc("/api/products", metricsMiddleware(productsHandler))
    
    http.ListenAndServe(":8080", nil)
}

func metricsMiddleware(next http.HandlerFunc) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()
        
        // Create a custom ResponseWriter to capture status code
        ww := newStatusResponseWriter(w)
        
        // Call the handler
        next(ww, r)
        
        // Record metrics
        duration := time.Since(start).Seconds()
        status := http.StatusText(ww.status)
        
        httpRequestsTotal.WithLabelValues(r.Method, r.URL.Path, status).Inc()
        httpDuration.WithLabelValues(r.Method, r.URL.Path).Observe(duration)
    }
}

// A ResponseWriter that tracks status code
type statusResponseWriter struct {
    http.ResponseWriter
    status int
}

func newStatusResponseWriter(w http.ResponseWriter) *statusResponseWriter {
    return &statusResponseWriter{w, http.StatusOK}
}

func (w *statusResponseWriter) WriteHeader(code int) {
    w.status = code
    w.ResponseWriter.WriteHeader(code)
}

func usersHandler(w http.ResponseWriter, r *http.Request) {
    // Implementation
}

func productsHandler(w http.ResponseWriter, r *http.Request) {
    // Implementation
}

Testing Tools

1. testify - Test assertions and mocks

Provides a toolkit for assertions, mocks, and suites in tests.

package main

import (
    "testing"
    "github.com/stretchr/testify/assert"
    "github.com/stretchr/testify/require"
)

func TestCalculator(t *testing.T) {
    // Basic assertions
    assert.Equal(t, 4, Add(2, 2), "2+2 should equal 4")
    assert.NotEqual(t, 5, Add(2, 2), "2+2 should not equal 5")
    
    // Assertions that stop the test on failure
    require.Equal(t, 4, Add(2, 2), "2+2 must equal 4")
    
    // Testing for errors
    result, err := Divide(10, 2)
    assert.NoError(t, err)
    assert.Equal(t, 5, result)
    
    // Expected errors
    _, err = Divide(10, 0)
    assert.Error(t, err)
    assert.Equal(t, "division by zero", err.Error())
    
    // Checking types
    assert.IsType(t, "", GetString())
    
    // Slices and maps
    assert.Contains(t, []string{"a", "b", "c"}, "b")
    assert.Subset(t, []int{1, 2, 3, 4}, []int{2, 4})
    assert.ElementsMatch(t, []int{1, 2, 3}, []int{3, 1, 2})
}

// Function implementations for the test
func Add(a, b int) int {
    return a + b
}

func Divide(a, b int) (int, error) {
    if b == 0 {
        return 0, errors.New("division by zero")
    }
    return a / b, nil
}

func GetString() string {
    return "test"
}

2. gomock - Mocking framework

Creating mock implementations of interfaces for testing.

//go:generate mockgen -destination=mocks/mock_repository.go -package=mocks github.com/myuser/myapp/repository UserRepository

package service

import (
    "context"
    "testing"
    
    "github.com/golang/mock/gomock"
    "github.com/myuser/myapp/models"
    "github.com/myuser/myapp/mocks"
    "github.com/stretchr/testify/assert"
)

func TestUserService_GetUser(t *testing.T) {
    ctrl := gomock.NewController(t)
    defer ctrl.Finish()
    
    // Create mock repository
    mockRepo := mocks.NewMockUserRepository(ctrl)
    
    // Set expectations
    mockRepo.EXPECT().
        GetByID(gomock.Any(), int64(123)).
        Return(&models.User{
            ID:   123,
            Name: "John Doe",
            Email: "john@example.com",
        }, nil)
    
    // Create service with mock repository
    service := NewUserService(mockRepo)
    
    // Call the method we want to test
    user, err := service.GetUser(context.Background(), 123)
    
    // Assert expectations
    assert.NoError(t, err)
    assert.NotNil(t, user)
    assert.Equal(t, int64(123), user.ID)
    assert.Equal(t, "John Doe", user.Name)
}

Development Workflow Enhancers

These tools help streamline your Go development workflow for maximum productivity.

Hot Reloading with air

air provides live reloading of your Go applications during development.

Installation:

go install github.com/cosmtrek/air@latest

Usage:

  1. Create a .air.toml configuration file:

    air init
    
  2. Run your app with hot reloading:

    air
    

Sample .air.toml configuration:

root = "."
tmp_dir = "tmp"

[build]
  cmd = "go build -o ./tmp/main ."
  bin = "tmp/main"
  delay = 1000
  exclude_dir = ["assets", "tmp", "vendor", "testdata"]
  exclude_file = []
  exclude_regex = ["_test\\.go"]
  exclude_unchanged = true
  follow_symlink = false
  full_bin = ""
  include_dir = []
  include_ext = ["go", "tpl", "tmpl", "html"]
  kill_delay = "0s"
  log = "build-errors.log"
  send_interrupt = false
  stop_on_error = true

[color]
  app = ""
  build = "yellow"
  main = "magenta"
  runner = "green"
  watcher = "cyan"

[log]
  time = false

[misc]
  clean_on_exit = false

Pre-commit Hooks with pre-commit

Using pre-commit hooks ensures code quality before committing changes.

Installation:

pip install pre-commit

Create a .pre-commit-config.yaml file:

repos:
-   repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v4.4.0
    hooks:
    -   id: trailing-whitespace
    -   id: end-of-file-fixer
    -   id: check-yaml

-   repo: https://github.com/dnephin/pre-commit-golang
    rev: v0.5.1
    hooks:
    -   id: go-fmt
    -   id: go-imports
    -   id: go-mod-tidy
    -   id: go-build
    -   id: go-test

-   repo: local
    hooks:
    -   id: golangci-lint
        name: golangci-lint
        description: Fast linters runner for Go
        entry: golangci-lint run
        types: [go]
        language: system
        pass_filenames: false

Install the hooks:

pre-commit install

Makefile for Common Tasks

Creating a Makefile simplifies common development tasks.

.PHONY: all build test clean lint run docker-build docker-run

# Default variables
APP_NAME=myapp
BUILD_DIR=build
MAIN_FILE=cmd/main.go

# Go build flags
LDFLAGS=-ldflags "-s -w"

all: clean lint test build

build:
	@echo "Building $(APP_NAME)..."
	@mkdir -p $(BUILD_DIR)
	@go build $(LDFLAGS) -o $(BUILD_DIR)/$(APP_NAME) $(MAIN_FILE)

test:
	@echo "Running tests..."
	@go test -v -race ./...

clean:
	@echo "Cleaning..."
	@rm -rf $(BUILD_DIR)
	@go clean

lint:
	@echo "Linting..."
	@golangci-lint run

run: build
	@echo "Running $(APP_NAME)..."
	@./$(BUILD_DIR)/$(APP_NAME)

docker-build:
	@echo "Building Docker image..."
	@docker build -t $(APP_NAME):latest .

docker-run: docker-build
	@echo "Running Docker container..."
	@docker run --rm -p 8080:8080 $(APP_NAME):latest

# Database migrations
migrate-up:
	@echo "Running migrations up..."
	@migrate -path ./migrations -database "postgres://postgres:password@localhost:5432/mydb?sslmode=disable" up

migrate-down:
	@echo "Running migrations down..."
	@migrate -path ./migrations -database "postgres://postgres:password@localhost:5432/mydb?sslmode=disable" down

# Generate code (mocks, protobuf, etc.)
generate:
	@echo "Generating code..."
	@go generate ./...

# Run with hot reload
dev:
	@air

Docker and Containerization

Containerizing Go applications ensures consistent environments from development to production.

Dockerfile Best Practices

Multistage build for minimal image size:

# Build stage
FROM golang:1.21-alpine AS builder

# Set working directory
WORKDIR /app

# Download dependencies first (for better caching)
COPY go.mod go.sum ./
RUN go mod download

# Copy source code
COPY . .

# Build the application
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -ldflags="-s -w" -o app .

# Final stage
FROM alpine:3.18

# Add certificates for HTTPS
RUN apk --no-cache add ca-certificates && update-ca-certificates

# Create non-root user
RUN addgroup -S appgroup && adduser -S appuser -G appgroup

# Set working directory
WORKDIR /app

# Copy the binary from builder
COPY --from=builder /app/app .

# Use non-root user
USER appuser

# Expose port
EXPOSE 8080

# Command to run
CMD ["./app"]

Docker Compose for Development

Create a docker-compose.yml for local development:

version: '3.8'

services:
  app:
    build:
      context: .
      dockerfile: Dockerfile.dev
    volumes:
      - .:/app
      - go-modules:/go/pkg/mod
    ports:
      - "8080:8080"
    depends_on:
      - postgres
    environment:
      - DB_HOST=postgres
      - DB_USER=postgres
      - DB_PASSWORD=postgres
      - DB_NAME=testdb
      - DB_PORT=5432
      - ENV=development

  postgres:
    image: postgres:15-alpine
    environment:
      - POSTGRES_USER=postgres
      - POSTGRES_PASSWORD=postgres
      - POSTGRES_DB=testdb
    volumes:
      - postgres-data:/var/lib/postgresql/data
    ports:
      - "5432:5432"

volumes:
  go-modules:
  postgres-data:

Development Dockerfile (Dockerfile.dev):

FROM golang:1.21

WORKDIR /app

# Install air for hot reloading
RUN go install github.com/cosmtrek/air@latest

# Copy air config
COPY .air.toml .

# Install dependencies
COPY go.mod go.sum ./
RUN go mod download

# Command to run
CMD ["air", "-c", ".air.toml"]

CI/CD Pipeline Setup

A well-configured CI/CD pipeline ensures code quality and simplifies deployment.

GitHub Actions Workflow

Create a .github/workflows/go.yml file:

name: Go

on:
  push:
    branches: [ main ]
  pull_request:
    branches: [ main ]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v3

    - name: Set up Go
      uses: actions/setup-go@v4
      with:
        go-version: '1.21'
        cache: true

    - name: Install dependencies
      run: go mod download

    - name: Lint
      uses: golangci/golangci-lint-action@v3
      with:
        version: latest

    - name: Test
      run: go test -v -race -coverprofile=coverage.out -covermode=atomic ./...

    - name: Upload coverage to Codecov
      uses: codecov/codecov-action@v3
      with:
        file: ./coverage.out
        
    - name: Build
      run: go build -v ./...

  docker:
    needs: build
    if: github.event_name == 'push' && github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v3
    
    - name: Set up Docker Buildx
      uses: docker/setup-buildx-action@v2
      
    - name: Login to DockerHub
      uses: docker/login-action@v2
      with:
        username: ${{ secrets.DOCKERHUB_USERNAME }}
        password: ${{ secrets.DOCKERHUB_TOKEN }}
        
    - name: Build and push
      uses: docker/build-push-action@v4
      with:
        context: .
        push: true
        tags: username/appname:latest,username/appname:${{ github.sha }}
        cache-from: type=registry,ref=username/appname:buildcache
        cache-to: type=registry,ref=username/appname:buildcache,mode=max

Conclusion: Building Your Optimal Go Toolkit

The Go ecosystem provides a wealth of tools and libraries to enhance your development experience. The optimal toolkit, however, depends on your specific needs and preferences.

Start with the essentials:

  1. Proper environment setup: Go Modules for dependency management
  2. Code quality tools: golangci-lint and gofumpt
  3. An IDE that suits your workflow: VSCode, GoLand, or Vim
  4. Core libraries for common tasks: web frameworks, database access, logging
  5. Testing tools for robust test suites

Then gradually incorporate more specialized tools as your needs evolve. Remember that the Go philosophy emphasizes simplicity and maintainability, so resist the urge to include dependencies that don’t provide significant value.

By thoughtfully curating your Go toolkit, you’ll spend less time fighting your tools and more time building high-quality, maintainable software that solves real problems.