Go (Golang) is a great fit for backend services thanks to its fast compile times, small memory footprint, built-in concurrency primitives, and rich standard library. Here is a quick walkthrough of setting up a Go environment and building, testing, and deploying a minimal HTTP API.

1. Setting Up the Development Environment

  1. Install Go: Download the latest release from go.dev/dl (or brew install go on macOS).
  2. Initialize a Go Module: Modern Go uses Go Modules (go.mod) rather than requiring your code to live inside $GOPATH/src:
mkdir users-api && cd users-api
go mod init github.com/myuser/users-api
  1. Editor Setup: Either GoLand or VS Code (with the official Go extension and gopls) works out of the box.

2. Standard Library vs. Third-Party Packages

Go’s standard library (net/http, encoding/json, database/sql, context, testing) is expressive enough to build production HTTP services without a heavyweight framework. Since Go 1.22, net/http.ServeMux also supports HTTP method matching and path wildcards (GET /users/{id}).

When you do want third-party libraries, some common choices are:

  • HTTP routers/frameworks: chi, echo, or gin
  • Databases & Redis: pgx (PostgreSQL), go-redis, mongo-driver
  • Assertions & testing: stretchr/testify

3. Building a Simple CRUD API

Let’s build a minimal /users API backed by an in-memory slice:

POST   /users      (create a user)
GET    /users      (list all users)
GET    /users/{id} (get a user by ID)
DELETE /users/{id} (delete a user by ID)

Using Go 1.22+ http.NewServeMux routing in main.go:

package main

import (
	"encoding/json"
	"log"
	"net/http"
	"strconv"
)

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

var users = []User{
	{ID: 1, Name: "Alice"},
}

func listUsers(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Content-Type", "application/json")
	_ = json.NewEncoder(w).Encode(users)
}

func getUser(w http.ResponseWriter, r *http.Request) {
	id, err := strconv.Atoi(r.PathValue("id"))
	if err != nil {
		http.Error(w, "invalid user id", http.StatusBadRequest)
		return
	}

	for _, u := range users {
		if u.ID == id {
			w.Header().Set("Content-Type", "application/json")
			_ = json.NewEncoder(w).Encode(u)
			return
		}
	}
	http.Error(w, "user not found", http.StatusNotFound)
}

func createUser(w http.ResponseWriter, r *http.Request) {
	var u User
	if err := json.NewDecoder(r.Body).Decode(&u); err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}
	users = append(users, u)
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(http.StatusCreated)
	_ = json.NewEncoder(w).Encode(u)
}

func newRouter() *http.ServeMux {
	mux := http.NewServeMux()
	mux.HandleFunc("GET /users", listUsers)
	mux.HandleFunc("GET /users/{id}", getUser)
	mux.HandleFunc("POST /users", createUser)
	return mux
}

func main() {
	log.Fatal(http.ListenAndServe(":8080", newRouter()))
}

(If you are on an older Go version prior to 1.22, register a single http.HandleFunc("/users", ...) handler and switch on r.Method, since calling http.HandleFunc twice with the same pattern panics.)

4. Testing HTTP Handlers with httptest

Go’s net/http/httptest package lets you test handlers directly without opening a network port:

package main

import (
	"net/http"
	"net/http/httptest"
	"testing"
)

func TestListUsers(t *testing.T) {
	req := httptest.NewRequest(http.MethodGet, "/users", nil)
	rec := httptest.NewRecorder()

	newRouter().ServeHTTP(rec, req)

	if rec.Code != http.StatusOK {
		t.Fatalf("expected status 200, got %d", rec.Code)
	}
}

Run the test suite with:

go test -v ./...

5. Building and Deploying

Because Go compiles into a single static binary, deploying to a container or Linux VM is straightforward:

GOOS=linux GOARCH=amd64 go build -o myapp .

If you are deploying to an EC2 instance via GitHub Actions and S3, build the binary in CI, upload it to S3, and pull it from the instance using aws s3 cp:

name: Build and Deploy

on:
  push:
    branches: [ main ]

env:
  AWS_REGION: eu-central-1

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

      - name: Build Go Binary
        run: GOOS=linux GOARCH=amd64 go build -o myapp .

      - name: Upload Binary to S3
        run: aws s3 cp myapp s3://my-s3-bucket/myapp
        env:
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}

      - name: Deploy to EC2
        uses: appleboy/ssh-action@v1.0.3
        with:
          host: ${{ secrets.EC2_HOST }}
          username: ec2-user
          key: ${{ secrets.SSH_PRIVATE_KEY }}
          script: |
            aws s3 cp s3://my-s3-bucket/myapp /tmp/myapp
            chmod +x /tmp/myapp
            sudo mv /tmp/myapp /usr/local/bin/myapp
            sudo systemctl restart myapp

For more Go resources, libraries, and patterns, check out my Go notes page.