Industry standards for writing modern, scalable, clean, type-safe, and thread-safe Go applications. Covers project structure, concurrency patterns, error handling, testing, and production-ready code.
Embrace Go idioms. Write simple, readable code that follows Go conventions. Clarity over cleverness. Errors are values. Concurrency is not parallelism.
Follow the golang-standards/project-layout:
/cmd - Main applications (cmd/myapp/main.go)
/internal - Private application code (not importable)
/pkg - Public library code (importable by external projects)
/api - API definitions (OpenAPI/Swagger, protobuf)
/web - Web assets (templates, static files)
/configs - Configuration files
/scripts - Build, install, analysis scripts
/test - Additional test data and fixtures
/docs - Design and user documents
/tools - Supporting tools for this project
/vendor - Application dependencies (via go mod vendor)
httputil, not http_util)util, common, base - use descriptive names/internal for code that shouldn't be imported by other projects// Good structure
myapp/
cmd/myapp/main.go // package main
internal/user/ // package user
repository.go
service.go
internal/order/ // package order
repository.go
service.go
pkg/validator/ // package validator (public)
email.go
Avoid primitive obsession. Create domain-specific types:
// Bad - primitive obsession
func ProcessOrder(userID int64, amount float64, currency string) error
// Good - strong types
type UserID int64
type Money struct {
Amount decimal.Decimal
Currency Currency
}
func ProcessOrder(userID UserID, amount Money) error
// Use type aliases for clarity
type OrderID string
type ProductID string
// Use structs to prevent invalid states
type Order struct {
ID OrderID
Status OrderStatus // Use enums via const + custom type
CreatedAt time.Time
}
// Use iota for enums
type OrderStatus int
const (
OrderStatusPending OrderStatus = iota
OrderStatusProcessing
OrderStatusCompleted
OrderStatusCancelled
)
func (s OrderStatus) String() string {
return [...]string{"pending", "processing", "completed", "cancelled"}[s]
}
Use generics for type-safe data structures and algorithms:
// Generic stack
type Stack[T any] struct {
items []T
}
func (s *Stack[T]) Push(item T) {
s.items = append(s.items, item)
}
func (s *Stack[T]) Pop() (T, bool) {
if len(s.items) == 0 {
var zero T
return zero, false
}
item := s.items[len(s.items)-1]
s.items = s.items[:len(s.items)-1]
return item, true
}
// Generic constraints
type Number interface {
~int | ~int64 | ~float64
}
func Sum[T Number](nums []T) T {
var total T
for _, n := range nums {
total += n
}
return total
}
Prefer channels over shared memory. Use goroutines liberally but responsibly.
// Good - channel-based communication
func ProcessItems(items []Item) []Result {
results := make(chan Result, len(items))
var wg sync.WaitGroup
for _, item := range items {
wg.Add(1)
go func(i Item) {
defer wg.Done()
results <- ProcessItem(i)
}(item)
}
go func() {
wg.Wait()
close(results)
}()
var output []Result
for result := range results {
output = append(output, result)
}
return output
}
Use sync.Mutex for protecting shared state:
type SafeCounter struct {
mu sync.RWMutex
count map[string]int
}
func (c *SafeCounter) Inc(key string) {
c.mu.Lock()
defer c.mu.Unlock()
c.count[key]++
}
func (c *SafeCounter) Value(key string) int {
c.mu.RLock() // Read lock allows concurrent reads
defer c.mu.RUnlock()
return c.count[key]
}
Use sync/atomic for simple counters and flags:
type Server struct {
requestCount atomic.Int64
isShutdown atomic.Bool
}
func (s *Server) HandleRequest() {
if s.isShutdown.Load() {
return
}
s.requestCount.Add(1)
// ... handle request
}
func (s *Server) Shutdown() {
s.isShutdown.Store(true)
}
Ensure one-time initialization:
var (
instance *Database
once sync.Once
)
func GetDatabase() *Database {
once.Do(func() {
instance = &Database{
// expensive initialization
}
})
return instance
}
Always pass context.Context as the first parameter:
// Good - context-aware
func FetchUser(ctx context.Context, id UserID) (*User, error) {
select {
case <-ctx.Done():
return nil, ctx.Err()
default:
// fetch user
}
}
// Use context timeouts
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
user, err := FetchUser(ctx, userID)
type Job struct {
ID int
Data interface{}
}
type WorkerPool struct {
workerCount int
jobs chan Job
results chan Result
wg sync.WaitGroup
}
func NewWorkerPool(workerCount int) *WorkerPool {
return &WorkerPool{
workerCount: workerCount,
jobs: make(chan Job, 100),
results: make(chan Result, 100),
}
}
func (wp *WorkerPool) Start(ctx context.Context) {
for i := 0; i < wp.workerCount; i++ {
wp.wg.Add(1)
go wp.worker(ctx)
}
}
func (wp *WorkerPool) worker(ctx context.Context) {
defer wp.wg.Done()
for {
select {
case <-ctx.Done():
return
case job, ok := <-wp.jobs:
if !ok {
return
}
result := processJob(job)
wp.results <- result
}
}
}
func FanOut(input <-chan int, workers int) []<-chan int {
channels := make([]<-chan int, workers)
for i := 0; i < workers; i++ {
channels[i] = worker(input)
}
return channels
}
func FanIn(channels ...<-chan int) <-chan int {
out := make(chan int)
var wg sync.WaitGroup
for _, c := range channels {
wg.Add(1)
go func(ch <-chan int) {
defer wg.Done()
for n := range ch {
out <- n
}
}(c)
}
go func() {
wg.Wait()
close(out)
}()
return out
}
// Good - explicit error checks
result, err := DoSomething()
if err != nil {
return fmt.Errorf("failed to do something: %w", err) // Use %w for error wrapping
}
// Bad - ignoring errors
result, _ := DoSomething() // Never ignore errors
// Sentinel errors
var (
ErrNotFound = errors.New("not found")
ErrUnauthorized = errors.New("unauthorized")
ErrInvalidInput = errors.New("invalid input")
)
// Error types with context
type ValidationError struct {
Field string
Value interface{}
Err error
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("validation failed for field %s: %v", e.Field, e.Err)
}
func (e *ValidationError) Unwrap() error {
return e.Err
}
// Usage
if err != nil {
if errors.Is(err, ErrNotFound) {
// handle not found
}
var validationErr *ValidationError
if errors.As(err, &validationErr) {
// handle validation error
}
}
// Wrap errors for context
func GetUser(id UserID) (*User, error) {
user, err := repo.Find(id)
if err != nil {
return nil, fmt.Errorf("get user %v: %w", id, err)
}
return user, nil
}
// Unwrap to check original error
err := GetUser(123)
if errors.Is(err, sql.ErrNoRows) {
// handle not found
}
// Exported (public) - starts with capital
type User struct{}
func NewUser() *User
// Unexported (private) - starts with lowercase
type userRepository struct{}
func newUserRepository() *userRepository
// Acronyms - all caps or all lowercase
type HTTPClient struct{} // not HttpClient
var urlString string // not urlString when unexported
// Interface names
type Reader interface{} // -er suffix for single method
type UserRepository interface{} // descriptive for multiple methods
// Keep functions small and focused
// Good - single responsibility
func ValidateEmail(email string) error {
if !emailRegex.MatchString(email) {
return ErrInvalidEmail
}
return nil
}
// Bad - doing too much
func ProcessUser(email string, password string) error {
// validate email
// validate password
// hash password
// save to database
// send email
// log everything
}
// Prefer returning errors over panics
func Divide(a, b float64) (float64, error) {
if b == 0 {
return 0, errors.New("division by zero")
}
return a / b, nil
}
Accept interfaces, return structs:
// Good
type UserService struct {
repo UserRepository // Accept interface
}
func (s *UserService) GetUser(id UserID) (*User, error) {
return s.repo.Find(id) // Return struct
}
// Define interfaces where they're used, not where implemented
type UserRepository interface {
Find(id UserID) (*User, error)
Save(user *User) error
}
// Small interfaces are better
type Reader interface {
Read(p []byte) (n int, err error)
}
func TestSum(t *testing.T) {
tests := []struct {
name string
a, b int
expected int
}{
{"positive numbers", 2, 3, 5},
{"negative numbers", -2, -3, -5},
{"mixed", -2, 5, 3},
{"zeros", 0, 0, 0},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := Sum(tt.a, tt.b)
if result != tt.expected {
t.Errorf("Sum(%d, %d) = %d; want %d",
tt.a, tt.b, result, tt.expected)
}
})
}
}
func TestFetchUser(t *testing.T) {
t.Helper() // Mark as helper for better error messages
// Use testify/assert for cleaner assertions
user, err := FetchUser(context.Background(), 123)
assert.NoError(t, err)
assert.NotNil(t, user)
assert.Equal(t, "John", user.Name)
}
type MockUserRepository struct {
mock.Mock
}
func (m *MockUserRepository) Find(id UserID) (*User, error) {
args := m.Called(id)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).(*User), args.Error(1)
}
func TestUserService(t *testing.T) {
mockRepo := new(MockUserRepository)
mockRepo.On("Find", UserID(123)).Return(&User{Name: "John"}, nil)
service := &UserService{repo: mockRepo}
user, err := service.GetUser(123)
assert.NoError(t, err)
assert.Equal(t, "John", user.Name)
mockRepo.AssertExpectations(t)
}
func BenchmarkSum(b *testing.B) {
for i := 0; i < b.N; i++ {
Sum(100, 200)
}
}
// Run: go test -bench=. -benchmem
# Initialize module
go mod init github.com/username/project
# Add dependency
go get github.com/stretchr/testify
# Update dependencies
go get -u ./...
# Tidy up
go mod tidy
# Vendor dependencies
go mod vendor
// go.mod
module github.com/username/project
go 1.22
require (
github.com/gin-gonic/gin v1.10.0
github.com/stretchr/testify v1.9.0
)
// Pin specific version
// go get github.com/gin-gonic/[email protected]
import _ "net/http/pprof"
func main() {
go func() {
http.ListenAndServe("localhost:6060", nil)
}()
// your app code
}
// Access profiles:
// http://localhost:6060/debug/pprof/
// go tool pprof http://localhost:6060/debug/pprof/heap
// Pre-allocate slices when size is known
items := make([]Item, 0, expectedSize)
// Reuse buffers
var buf bytes.Buffer
buf.Reset() // Reuse instead of creating new
// Use string builders for concatenation
var sb strings.Builder
sb.WriteString("hello")
sb.WriteString(" world")
result := sb.String()
// Avoid unnecessary allocations
// Bad
s := fmt.Sprintf("%d", num)
// Good
s := strconv.Itoa(num)
import "log/slog"
func main() {
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
logger.Info("server started",
slog.String("address", ":8080"),
slog.Int("workers", 10),
)
logger.Error("failed to process request",
slog.String("user_id", "123"),
slog.String("error", err.Error()),
)
}
func main() {
srv := &http.Server{Addr: ":8080"}
go func() {
if err := srv.ListenAndServe(); err != http.ErrServerClosed {
log.Fatalf("server error: %v", err)
}
}()
// Wait for interrupt signal
quit := make(chan os.Signal, 1)
signal.Notify(quit, os.Interrupt, syscall.SIGTERM)
<-quit
// Graceful shutdown with timeout
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
log.Fatalf("server forced to shutdown: %v", err)
}
log.Println("server exited gracefully")
}
// Use environment variables with defaults
type Config struct {
Port string `env:"PORT" envDefault:"8080"`
DatabaseURL string `env:"DATABASE_URL,required"`
Timeout time.Duration `env:"TIMEOUT" envDefault:"30s"`
}
// Use viper or envconfig for loading
func healthHandler(w http.ResponseWriter, r *http.Request) {
health := map[string]string{
"status": "ok",
"version": version,
"timestamp": time.Now().Format(time.RFC3339),
}
// Check dependencies
if err := db.Ping(); err != nil {
health["status"] = "unhealthy"
health["database"] = "down"
w.WriteHeader(http.StatusServiceUnavailable)
}
json.NewEncoder(w).Encode(health)
}
-race flag_ for errors# Format code
go fmt ./...
# Lint code
golangci-lint run
# Vet code
go vet ./...
# Check for vulnerabilities
govulncheck ./...
# Run tests with race detector
go test -race ./...
# .github/workflows/go.yml