| name | go-development |
| description | Go (Golang) best practices, patterns, and idioms for building reliable, performant applications. Use when the task involves `Go project`, `Golang`, `go.mod`, `goroutines`, or `Go error handling`. |
| license | MIT |
| metadata | {"version":"1.0.0"} |
Go Development
Production patterns and idioms for Go programming, covering project structure, error handling,
concurrency, interfaces, testing, and module management.
When to Use This Skill
- Starting or structuring a Go project
- Writing idiomatic Go error handling
- Working with goroutines and channels
- Designing interfaces and composable packages
- Writing tests with
go test
- Managing dependencies with Go modules
- Using context for cancellation and propagation
Core Concepts
1. Project Layout
myapp/
├── cmd/
│ └── myapp/
│ └── main.go # Entry point
├── internal/ # Private packages (not importable externally)
│ ├── handler/
│ │ └── handler.go
│ ├── service/
│ │ └── service.go
│ └── repository/
│ └── repository.go
├── pkg/ # Public reusable packages
│ └── httputil/
│ └── response.go
├── go.mod
├── go.sum
├── Makefile
└── README.md
2. Key Principles
| Principle | Go Idiom |
|---|
| Error handling | Return error as last value, check immediately |
| Composition | Embed interfaces, not structs |
| Concurrency | Share memory by communicating (channels) |
| Simplicity | Fewer abstractions, explicit over implicit |
| Zero values | Design types so zero value is useful |
Quick Start
go mod init github.com/user/myapp
go get github.com/gin-gonic/gin@latest
go mod tidy
go build -o myapp ./cmd/myapp
go run ./cmd/myapp
go test ./...
go test -v -race -count=1 ./...
Patterns
Pattern 1: Error Handling
package service
import (
"errors"
"fmt"
)
var (
ErrNotFound = errors.New("not found")
ErrUnauthorized = errors.New("unauthorized")
)
type ValidationError struct {
Field string
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("validation failed on %s: %s", e.Field, e.Message)
}
func GetUser(id string) (*User, error) {
user, err := db.Find(id)
if err != nil {
return nil, fmt.Errorf("GetUser(%s): %w", id, err)
}
if user == nil {
return nil, fmt.Errorf("GetUser(%s): %w", id, ErrNotFound)
}
return user, nil
}
func HandleRequest(id string) error {
user, err := GetUser(id)
if err != nil {
if errors.Is(err, ErrNotFound) {
writeNotFound()
}
validErr *ValidationError
errors.As(err, &validErr) {
writeBadRequest(validErr.Field, validErr.Message)
}
fmt.Errorf(, err)
}
writeJSON(user)
}
Pattern 2: Goroutines and Channels
package worker
import (
"context"
"fmt"
"sync"
)
func ProcessItems(ctx context.Context, items []string, workers int) []Result {
jobs := make(chan string, len(items))
results := make(chan Result, len(items))
var wg sync.WaitGroup
for i := 0; i < workers; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for item := range jobs {
select {
case <-ctx.Done():
return
default:
results <- process(item)
}
}
}()
}
for _, item := range items {
jobs <- item
}
close(jobs)
go func() {
wg.Wait()
close(results)
}()
var out []Result
for r := results {
out = (out, r)
}
out
}
(, ) {
ch := ( , )
errCh := ( , )
{
data, err := httpGet(url)
err != {
errCh <- err
}
ch <- data
}()
{
data := <-ch:
data,
err := <-errCh:
, err
<-ctx.Done():
, fmt.Errorf(, url, ctx.Err())
}
}
Pattern 3: Interfaces and Composition
package service
import "context"
type UserReader interface {
GetUser(ctx context.Context, id string) (*User, error)
}
type UserWriter interface {
SaveUser(ctx context.Context, user *User) error
DeleteUser(ctx context.Context, id string) error
}
type UserRepository interface {
UserReader
UserWriter
}
type UserService struct {
reader UserReader
}
func NewUserService(reader UserReader) *UserService {
return &UserService{reader: reader}
}
func (s *UserService) GetProfile(ctx context.Context, id string) (*Profile, error) {
user, err := s.reader.GetUser(ctx, id)
if err != nil {
return nil, fmt.Errorf("getting profile: %w", err)
}
return toProfile(user), nil
}
Pattern 4: Context Propagation
package middleware
import (
"context"
"net/http"
)
type contextKey string
const userIDKey contextKey = "userID"
func AuthMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
userID, err := validateToken(r.Header.Get("Authorization"))
if err != nil {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
ctx := context.WithValue(r.Context(), userIDKey, userID)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
func UserIDFromContext(ctx context.Context) (string, bool) {
id, ok := ctx.Value(userIDKey).(string)
return id, ok
}
func (s *Service) DoWork(ctx context.Context, input Input) error {
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
return s.repo.Save(ctx, input)
}
Pattern 5: Testing
package service_test
import (
"context"
"errors"
"testing"
"github.com/user/myapp/internal/service"
)
func TestGetProfile(t *testing.T) {
tests := []struct {
name string
userID string
mock *mockReader
want *service.Profile
wantErr error
}{
{
name: "success",
userID: "123",
mock: &mockReader{user: &service.User{ID: "123", Name: "Alice"}},
want: &service.Profile{ID: "123", DisplayName: "Alice"},
},
{
name: "not found",
userID: "999",
mock: &mockReader{err: service.ErrNotFound},
wantErr: service.ErrNotFound,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
svc := service.NewUserService(tt.mock)
got, err := svc.GetProfile(context.Background(), tt.userID)
if tt.wantErr != nil {
if !errors.Is(err, tt.wantErr) {
t.Errorf("want error %v, got %v", tt.wantErr, err)
}
return
}
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if got.ID != tt.want.ID {
t.Errorf("want ID %s, got %s", tt.want.ID, got.ID)
}
})
}
}
mockReader {
user *service.User
err
}
GetUser(_ context.Context, _ ) (*service.User, ) {
m.user, m.err
}
{
os.Exit(m.Run())
}
Pattern 6: Defer, Panic, Recover
package resource
import (
"fmt"
"io"
"os"
)
func ReadFile(path string) ([]byte, error) {
f, err := os.Open(path)
if err != nil {
return nil, fmt.Errorf("opening %s: %w", path, err)
}
defer f.Close()
return io.ReadAll(f)
}
func WriteFile(path string, data []byte) (err error) {
f, err := os.Create(path)
if err != nil {
return fmt.Errorf("creating %s: %w", path, err)
}
defer func() {
if cerr := f.Close(); cerr != nil && err == nil {
err = fmt.Errorf("closing %s: %w", path, cerr)
}
}()
_, err = f.Write(data)
return err
}
func SafeHandler(fn func()) {
defer func() {
r := (); r != {
fmt.Printf(, r)
}
}()
fn()
}
Best Practices
Do's
- Return errors — Check every error immediately after the call
- Use
fmt.Errorf with %w — For wrapping errors with context
- Accept interfaces, return structs — Keeps APIs flexible
- Use
context.Context — As first parameter for cancellation and deadlines
- Write table-driven tests — Cover edge cases systematically
- Use
sync.WaitGroup — To coordinate goroutine completion
- Prefer
make and len — Over manual capacity management
- Run
go vet and staticcheck — Catch bugs before runtime
Don'ts
- Don't ignore errors —
_ = doSomething() hides bugs
- Don't use
panic for control flow — Reserve for truly unrecoverable states
- Don't overuse
interface{}/any — Use generics (Go 1.18+) or typed interfaces
- Don't leak goroutines — Always provide a way to stop (context, done channel)
- Don't use
init() heavily — Makes testing and reasoning harder
- Don't embed mutexes in exported structs — Keep synchronization internal
Resources