3 minutes
Getting Started with Golang Backend Development
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
- Install Go: Download the latest release from go.dev/dl (or
brew install goon macOS). - 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
- 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, orgin - 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.