1. Create Project
Terminal
mkdir my-api && cd my-api
go mod init my-api
go get github.com/go-chi/chi/v5
go get github.com/go-chi/cors
go get github.com/google/uuidChi is a lightweight and fast Go router. It uses a middleware pattern similar to Express.js.
2. Write Your First API
main.go
package main
import (
"encoding/json"
"log"
"net/http"
"os"
"github.com/go-chi/chi/v5"
"github.com/go-chi/chi/v5/middleware"
"github.com/go-chi/cors"
)
func main() {
r := chi.NewRouter()
// Middleware
r.Use(middleware.Logger)
r.Use(middleware.Recoverer)
r.Use(cors.Handler(cors.Options{
AllowedOrigins: []string{"http://localhost:*"},
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE"},
AllowedHeaders: []string{"Content-Type"},
AllowCredentials: true,
}))
// Routes
r.Get("/health", healthHandler)
r.Get("/api/hello", helloHandler)
// Start server
port := os.Getenv("PORT")
if port == "" {
port = "8080"
}
log.Printf("Server starting on port %s", port)
http.ListenAndServe(":"+port, r)
}
func healthHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{
"status": "healthy",
})
}
func helloHandler(w http.ResponseWriter, r *http.Request) {
name := r.URL.Query().Get("name")
if name == "" {
name = "World"
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{
"message": "Hello, " + name + "!",
})
}Run
Terminal
go run main.go
# Test
curl http://localhost:8080/health
curl http://localhost:8080/api/hello?name=Choorai3. Add CRUD API
handlers.go
package main
import (
"encoding/json"
"net/http"
"sync"
"time"
"github.com/go-chi/chi/v5"
"github.com/google/uuid"
)
// Project model
type Project struct {
ID string `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
CreatedAt time.Time `json:"created_at"`
}
// In-memory storage
var (
projects = make(map[string]*Project)
mu sync.RWMutex
)
// List projects
func listProjects(w http.ResponseWriter, r *http.Request) {
mu.RLock()
defer mu.RUnlock()
items := make([]*Project, 0, len(projects))
for _, p := range projects {
items = append(items, p)
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"items": items,
"total": len(items),
})
}
// Create project
func createProject(w http.ResponseWriter, r *http.Request) {
var req struct {
Name string `json:"name"`
Description string `json:"description"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, `{"error":"invalid body"}`, http.StatusBadRequest)
return
}
project := &Project{
ID: uuid.New().String(),
Name: req.Name,
Description: req.Description,
CreatedAt: time.Now(),
}
mu.Lock()
projects[project.ID] = project
mu.Unlock()
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(project)
}
// Get project
func getProject(w http.ResponseWriter, r *http.Request) {
id := chi.URLParam(r, "id")
mu.RLock()
project, exists := projects[id]
mu.RUnlock()
if !exists {
http.Error(w, `{"error":"not found"}`, http.StatusNotFound)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(project)
}
// Delete project
func deleteProject(w http.ResponseWriter, r *http.Request) {
id := chi.URLParam(r, "id")
mu.Lock()
defer mu.Unlock()
if _, exists := projects[id]; !exists {
http.Error(w, `{"error":"not found"}`, http.StatusNotFound)
return
}
delete(projects, id)
w.WriteHeader(http.StatusNoContent)
}
// Router setup
func setupRoutes(r chi.Router) {
r.Route("/api/v1/projects", func(r chi.Router) {
r.Get("/", listProjects)
r.Post("/", createProject)
r.Get("/{id}", getProject)
r.Delete("/{id}", deleteProject)
})
}sync.RWMutex
Go uses sync.RWMutex for concurrency safety.
Multiple goroutines can read simultaneously, but only one can write at a time.
4. Deploy with Docker
Dockerfile
# Build stage
FROM golang:1.22-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-w -s" -o server .
# Runtime stage
FROM alpine:3.20
WORKDIR /app
RUN adduser -D -g '' appuser
COPY --from=builder /app/server .
RUN chown -R appuser:appuser /app
USER appuser
EXPOSE 8080
CMD ["./server"]Multi-stage builds ensure the final image contains only the binary. The resulting image size is approximately 15-20MB.
Terminal
# Local test
docker build -t my-go-api .
docker run -p 8080:8080 my-go-api
# Check image size (< 20MB!)
docker images my-go-api
# Cloud Run deployment
gcloud run deploy my-go-api \
--source . \
--region asia-northeast3 \
--allow-unauthenticated \
--min-instances 0 \
--max-instances 1Framework Comparison
| Category | Go (Chi) | Hono | FastAPI |
|---|---|---|---|
| Image Size | ~15MB | ~150MB | ~200MB |
| Cold Start | ~100ms | ~300ms | ~500ms |
| Memory Usage | ~20MB | ~50MB | ~80MB |
| Learning Curve | Medium | Easy | Easy |
| Type System | Static | Dynamic (TS) | Dynamic (Pydantic) |
When Should You Choose Go?
Full Example Code
Check out the Go version of the B2B Admin API: examples/b2b-admin/api-go