Go
Language Overview¶
Go (or Golang) is a statically typed, compiled programming language designed at Google. Known for its simplicity, efficiency, and excellent concurrency support, Go is widely used for building cloud-native applications, microservices, CLI tools, and infrastructure automation.
Key Characteristics¶
- Paradigm: Concurrent, imperative, object-oriented (composition over inheritance)
- Typing: Static, strong, with type inference
- Runtime: Compiled to native binaries with garbage collection
- Primary Use Cases:
- Cloud-native applications and microservices
- CLI tools and utilities (Cobra, Viper)
- DevOps tooling (Docker, Kubernetes, Terraform are written in Go)
- High-performance backend services
- Infrastructure automation and network services
This Style Guide Covers¶
- Naming conventions following Go idioms
- Code formatting with gofmt and goimports
- Package organization and module structure
- Error handling patterns and best practices
- Concurrency patterns with goroutines and channels
- Testing standards with table-driven tests
- Documentation with godoc conventions
- Performance optimization and profiling
- Security best practices
- Tooling configuration (golangci-lint, gopls)
Supported Versions¶
| Version | Support Status | EOL Date | Recommended |
|---|---|---|---|
| 1.25.x | Active | 2026-08 | ✅ Yes |
| 1.24.x | Active | 2026-02 | ✅ Yes |
| 1.23.x | EOL | 2025-08 | ❌ No |
| 1.22.x | EOL | 2025-02 | ❌ No |
Recommendation: Use Go 1.24+ for new projects. Go maintains backward compatibility, so newer versions typically work with existing code.
EOL Policy: Go supports the two most recent major versions with security patches. Upgrade promptly when new versions are released.
Version Features:
- Go 1.25: Range function improvements, enhanced toolchain, performance gains
- Go 1.24: Generic type aliases, improved
go toolintegration, weak pointers - Go 1.23: Enhanced iterators, improved tooling, performance optimizations
- Go 1.22: Range over integers, improved HTTP routing, enhanced tooling
Quick Reference¶
| Category | Convention | Example | Notes |
|---|---|---|---|
| Naming | |||
| Variables | camelCase |
userCount, maxRetries |
Short names in limited scope |
| Constants | MixedCaps or camelCase |
MaxConnections, defaultTimeout |
Exported use MixedCaps |
| Functions | MixedCaps |
GetUser(), validateInput() |
Exported use capital letter |
| Methods | MixedCaps |
(u *User) Save() |
Receiver names are short |
| Interfaces | MixedCaps + er suffix |
Reader, Writer, Stringer |
Single method interfaces |
| Packages | lowercase |
httputil, strconv |
Short, no underscores |
| Files | snake_case.go |
user_service.go, http_handler.go |
Test files: *_test.go |
| Formatting | |||
| Line Length | No hard limit | Use gofmt | Break at natural points |
| Indentation | Tabs | gofmt enforces |
Tabs for indentation |
| Blank Lines | Minimal | Group related code | gofmt handles most |
| Braces | K&R style | Opening brace same line | Required by gofmt |
| Imports | |||
| Order | stdlib, external, internal | Grouped with blank lines | Use goimports |
| Style | Full package path | import "github.com/pkg/errors" |
Avoid dot imports |
| Documentation | |||
| Package | Package comment | // Package foo provides... |
First file alphabetically |
| Functions | Before declaration | // FuncName does X. |
Starts with function name |
| Files | |||
| Naming | snake_case.go |
user_repository.go |
Match primary type |
| Test files | *_test.go |
user_repository_test.go |
Same package or _test |
Project Structure¶
Standard Project Layout¶
myproject/
├── cmd/ # Application entrypoints
│ ├── api/
│ │ └── main.go # API server entrypoint
│ └── cli/
│ └── main.go # CLI tool entrypoint
├── internal/ # Private application code
│ ├── config/ # Configuration handling
│ │ └── config.go
│ ├── handler/ # HTTP handlers
│ │ ├── handler.go
│ │ └── handler_test.go
│ ├── middleware/ # HTTP middleware
│ │ └── auth.go
│ ├── model/ # Domain models
│ │ └── user.go
│ ├── repository/ # Data access layer
│ │ ├── user.go
│ │ └── user_test.go
│ └── service/ # Business logic
│ ├── user.go
│ └── user_test.go
├── pkg/ # Public library code
│ └── httputil/ # Reusable HTTP utilities
│ └── response.go
├── api/ # API specifications
│ └── openapi.yaml
├── configs/ # Configuration files
│ └── config.yaml
├── scripts/ # Build and utility scripts
│ └── build.sh
├── test/ # Integration tests
│ └── integration_test.go
├── go.mod # Module definition
├── go.sum # Dependency checksums
├── Makefile # Build automation
└── README.md
Module Initialization¶
// go.mod
module github.com/myorg/myproject
go 1.24
require (
github.com/gin-gonic/gin v1.9.1
github.com/spf13/cobra v1.8.0
github.com/spf13/viper v1.18.2
go.uber.org/zap v1.26.0
)
require (
// indirect dependencies managed by go mod tidy
)
Package Organization¶
// internal/config/config.go
package config
import (
"os"
"time"
"github.com/spf13/viper"
)
// Config holds application configuration
type Config struct {
Server ServerConfig
Database DatabaseConfig
Logger LoggerConfig
}
// ServerConfig holds HTTP server settings
type ServerConfig struct {
Host string `mapstructure:"host"`
Port int `mapstructure:"port"`
ReadTimeout time.Duration `mapstructure:"read_timeout"`
WriteTimeout time.Duration `mapstructure:"write_timeout"`
}
// DatabaseConfig holds database connection settings
type DatabaseConfig struct {
Host string `mapstructure:"host"`
Port int `mapstructure:"port"`
User string `mapstructure:"user"`
Password string `mapstructure:"password"`
Name string `mapstructure:"name"`
SSLMode string `mapstructure:"ssl_mode"`
}
// LoggerConfig holds logging settings
type LoggerConfig struct {
Level string `mapstructure:"level"`
Format string `mapstructure:"format"`
}
// Load reads configuration from file and environment
func Load(configPath string) (*Config, error) {
v := viper.New()
v.SetConfigFile(configPath)
v.SetConfigType("yaml")
// Environment variable overrides
v.SetEnvPrefix("APP")
v.AutomaticEnv()
// Set defaults
v.SetDefault("server.host", "0.0.0.0")
v.SetDefault("server.port", 8080)
v.SetDefault("server.read_timeout", "30s")
v.SetDefault("server.write_timeout", "30s")
v.SetDefault("logger.level", "info")
v.SetDefault("logger.format", "json")
if err := v.ReadInConfig(); err != nil {
return nil, err
}
var cfg Config
if err := v.Unmarshal(&cfg); err != nil {
return nil, err
}
return &cfg, nil
}
Naming Conventions¶
Variables¶
Convention: camelCase with short names for limited scope
// Good - descriptive names for package-level or longer-lived variables
var (
defaultTimeout = 30 * time.Second
maxRetryCount = 3
)
func processUsers(users []User) error {
// Good - short names for limited scope
for i, u := range users {
if err := u.Validate(); err != nil {
return fmt.Errorf("user %d: %w", i, err)
}
}
return nil
}
// Good - descriptive for function parameters
func createUser(ctx context.Context, username, email string) (*User, error) {
user := &User{
Username: username,
Email: email,
CreatedAt: time.Now(),
}
return user, nil
}
// Bad - overly verbose for limited scope
func processUsersBad(users []User) error {
for userIndex, currentUser := range users { // Too verbose
if validationError := currentUser.Validate(); validationError != nil {
return validationError
}
}
return nil
}
// Bad - single letter for longer-lived or unclear context
var t = 30 * time.Second // What is t?
Constants¶
Convention: MixedCaps for exported, camelCase for unexported
// Good - exported constants use MixedCaps
const (
MaxConnections = 100
DefaultTimeout = 30 * time.Second
APIVersion = "v1"
ContentTypeJSON = "application/json"
)
// Good - unexported constants use camelCase
const (
defaultBufferSize = 4096
maxRetries = 3
connectionTimeout = 10 * time.Second
)
// Good - iota for enumerations
type Status int
const (
StatusPending Status = iota
StatusActive
StatusInactive
StatusDeleted
)
func (s Status) String() string {
switch s {
case StatusPending:
return "pending"
case StatusActive:
return "active"
case StatusInactive:
return "inactive"
case StatusDeleted:
return "deleted"
default:
return "unknown"
}
}
// Bad - SCREAMING_CASE is not idiomatic Go
const MAX_CONNECTIONS = 100 // Not idiomatic
const DEFAULT_TIMEOUT = 30 // Not idiomatic
Functions and Methods¶
Convention: MixedCaps - exported start with capital, unexported with lowercase
// Good - exported function with clear verb-noun naming
func GetUserByID(ctx context.Context, id int64) (*User, error) {
// Implementation
}
// Good - unexported helper function
func validateEmail(email string) error {
if !strings.Contains(email, "@") {
return errors.New("invalid email format")
}
return nil
}
// Good - method with short receiver name
type UserService struct {
repo Repository
logger *zap.Logger
}
func (s *UserService) Create(ctx context.Context, req CreateUserRequest) (*User, error) {
if err := req.Validate(); err != nil {
return nil, fmt.Errorf("validation failed: %w", err)
}
user := &User{
Username: req.Username,
Email: req.Email,
CreatedAt: time.Now(),
}
if err := s.repo.Save(ctx, user); err != nil {
return nil, fmt.Errorf("failed to save user: %w", err)
}
s.logger.Info("user created", zap.Int64("id", user.ID))
return user, nil
}
// Good - constructor function naming
func NewUserService(repo Repository, logger *zap.Logger) *UserService {
return &UserService{
repo: repo,
logger: logger,
}
}
// Bad - receiver name too long
func (userService *UserService) CreateUser(ctx context.Context) error { // 'userService' is too long
return nil
}
// Bad - inconsistent receiver name
func (us *UserService) Delete(ctx context.Context) error { // Should match other methods
return nil
}
Interfaces¶
Convention: Single-method interfaces end with -er suffix
// Good - single method interfaces with -er suffix
type Reader interface {
Read(p []byte) (n int, err error)
}
type Writer interface {
Write(p []byte) (n int, err error)
}
type Closer interface {
Close() error
}
// Good - composed interfaces
type ReadWriter interface {
Reader
Writer
}
type ReadWriteCloser interface {
Reader
Writer
Closer
}
// Good - domain-specific interfaces
type UserRepository interface {
Create(ctx context.Context, user *User) error
GetByID(ctx context.Context, id int64) (*User, error)
Update(ctx context.Context, user *User) error
Delete(ctx context.Context, id int64) error
List(ctx context.Context, opts ListOptions) ([]*User, error)
}
type Validator interface {
Validate() error
}
type Stringer interface {
String() string
}
// Good - accept interfaces, return concrete types
func ProcessData(r Reader) (*Result, error) {
data, err := io.ReadAll(r)
if err != nil {
return nil, err
}
return &Result{Data: data}, nil
}
// Bad - interface pollution (too many methods)
type UserManager interface { // Too broad
Create(ctx context.Context, user *User) error
GetByID(ctx context.Context, id int64) (*User, error)
Update(ctx context.Context, user *User) error
Delete(ctx context.Context, id int64) error
List(ctx context.Context) ([]*User, error)
Validate(user *User) error
Hash(password string) string
SendEmail(user *User) error
GenerateToken(user *User) string
}
Packages¶
Convention: Short, lowercase, single word preferred
// Good package names
package http // Standard library style
package httputil // Utility for http
package strconv // String conversion
package json // JSON handling
// Good - descriptive but concise
package user // User domain
package auth // Authentication
package config // Configuration
// Bad - verbose package names
package http_utilities // Use httputil instead
package userManagement // Use user instead
package json_parsing // Use json instead
// Bad - generic/meaningless names
package util // Too vague
package common // What's common?
package helpers // Not descriptive
package misc // Avoid
Files¶
Convention: snake_case.go, test files end with _test.go
# Good file names
user.go # User type and methods
user_test.go # Tests for user.go
user_repository.go # User repository implementation
http_handler.go # HTTP handlers
middleware.go # HTTP middleware
config.go # Configuration
# Bad file names
User.go # Don't use PascalCase
userRepository.go # Don't use camelCase
http-handler.go # Don't use kebab-case
Code Formatting¶
Indentation and Braces¶
Go uses gofmt for consistent formatting. Tabs are used for indentation.
// Good - K&R brace style (required by gofmt)
func processRequest(ctx context.Context, req *Request) (*Response, error) {
if req == nil {
return nil, errors.New("request is nil")
}
result, err := doSomething(ctx, req.Data)
if err != nil {
return nil, fmt.Errorf("processing failed: %w", err)
}
return &Response{
Status: "success",
Data: result,
}, nil
}
// Good - struct initialization
user := User{
ID: 1,
Username: "johndoe",
Email: "john@example.com",
CreatedAt: time.Now(),
}
// Good - slice initialization
items := []string{
"apple",
"banana",
"cherry",
}
// Good - map initialization
config := map[string]interface{}{
"host": "localhost",
"port": 8080,
"debug": true,
"timeout": 30,
}
Import Organization¶
// Good - imports grouped: stdlib, external, internal
import (
// Standard library
"context"
"encoding/json"
"fmt"
"net/http"
"time"
// External packages
"github.com/gin-gonic/gin"
"github.com/pkg/errors"
"go.uber.org/zap"
// Internal packages
"github.com/myorg/myproject/internal/config"
"github.com/myorg/myproject/internal/model"
"github.com/myorg/myproject/internal/repository"
)
// Good - aliased imports for clarity
import (
"database/sql"
pgx "github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
)
// Bad - unsorted imports
import (
"github.com/gin-gonic/gin" // External before stdlib
"fmt"
"github.com/myorg/myproject/internal/config"
"net/http"
)
// Bad - dot imports (pollute namespace)
import (
. "github.com/onsi/ginkgo/v2" // Avoid dot imports
. "github.com/onsi/gomega"
)
Line Breaking¶
// Good - break long function signatures
func CreateUserWithOptions(
ctx context.Context,
username string,
email string,
options CreateUserOptions,
) (*User, error) {
// Implementation
}
// Good - break long function calls
result, err := userService.CreateUser(
ctx,
CreateUserRequest{
Username: username,
Email: email,
Role: "admin",
},
)
// Good - break long conditionals
if user != nil &&
user.IsActive &&
user.HasPermission("admin") &&
!user.IsLocked {
// Handle admin user
}
// Good - break long struct literals
config := &ServerConfig{
Host: "0.0.0.0",
Port: 8080,
ReadTimeout: 30 * time.Second,
WriteTimeout: 30 * time.Second,
MaxHeaderBytes: 1 << 20,
ShutdownTimeout: 10 * time.Second,
}
Error Handling¶
Basic Error Handling¶
// Good - check errors immediately
func readConfig(path string) (*Config, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("reading config file: %w", err)
}
var config Config
if err := json.Unmarshal(data, &config); err != nil {
return nil, fmt.Errorf("parsing config: %w", err)
}
return &config, nil
}
// Good - use errors.Is for sentinel errors
var ErrNotFound = errors.New("not found")
func GetUser(ctx context.Context, id int64) (*User, error) {
user, err := repo.FindByID(ctx, id)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, ErrNotFound
}
return nil, fmt.Errorf("querying user: %w", err)
}
return user, nil
}
// Good - use errors.As for type assertions
type ValidationError struct {
Field string
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("%s: %s", e.Field, e.Message)
}
func handleError(err error) {
var validationErr *ValidationError
if errors.As(err, &validationErr) {
fmt.Printf("Validation failed on %s: %s\n", validationErr.Field, validationErr.Message)
return
}
fmt.Printf("Unknown error: %v\n", err)
}
Error Wrapping¶
// Good - wrap errors with context using %w
func (s *UserService) Create(ctx context.Context, req CreateUserRequest) (*User, error) {
// Validate request
if err := req.Validate(); err != nil {
return nil, fmt.Errorf("invalid request: %w", err)
}
// Check for existing user
existing, err := s.repo.FindByEmail(ctx, req.Email)
if err != nil && !errors.Is(err, ErrNotFound) {
return nil, fmt.Errorf("checking existing user: %w", err)
}
if existing != nil {
return nil, fmt.Errorf("email already registered: %w", ErrDuplicateEmail)
}
// Create user
user := &User{
Username: req.Username,
Email: req.Email,
CreatedAt: time.Now(),
}
if err := s.repo.Save(ctx, user); err != nil {
return nil, fmt.Errorf("saving user: %w", err)
}
return user, nil
}
// Good - custom error types for domain errors
type NotFoundError struct {
Resource string
ID interface{}
}
func (e *NotFoundError) Error() string {
return fmt.Sprintf("%s with ID %v not found", e.Resource, e.ID)
}
func (e *NotFoundError) Is(target error) bool {
_, ok := target.(*NotFoundError)
return ok
}
func GetUserByID(ctx context.Context, id int64) (*User, error) {
user, err := repo.FindByID(ctx, id)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return nil, &NotFoundError{Resource: "user", ID: id}
}
return nil, fmt.Errorf("querying database: %w", err)
}
return user, nil
}
Error Handling Patterns¶
// Good - multiple return error handling
func processMultipleItems(ctx context.Context, items []Item) error {
var errs []error
for i, item := range items {
if err := processItem(ctx, item); err != nil {
errs = append(errs, fmt.Errorf("item %d: %w", i, err))
}
}
if len(errs) > 0 {
return errors.Join(errs...)
}
return nil
}
// Good - defer with error handling
func writeToFile(path string, data []byte) (err error) {
f, err := os.Create(path)
if err != nil {
return fmt.Errorf("creating file: %w", err)
}
defer func() {
if closeErr := f.Close(); closeErr != nil {
if err == nil {
err = fmt.Errorf("closing file: %w", closeErr)
}
}
}()
if _, err := f.Write(data); err != nil {
return fmt.Errorf("writing data: %w", err)
}
return nil
}
// Good - panic and recover (use sparingly)
func safeExecute(fn func()) (err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("panic recovered: %v", r)
}
}()
fn()
return nil
}
Concurrency Patterns¶
Goroutines and Channels¶
// Good - basic goroutine with channel
func fetchData(ctx context.Context, urls []string) ([]Result, error) {
results := make(chan Result, len(urls))
errs := make(chan error, len(urls))
for _, url := range urls {
go func(url string) {
result, err := fetch(ctx, url)
if err != nil {
errs <- err
return
}
results <- result
}(url)
}
var allResults []Result
var allErrors []error
for i := 0; i < len(urls); i++ {
select {
case result := <-results:
allResults = append(allResults, result)
case err := <-errs:
allErrors = append(allErrors, err)
case <-ctx.Done():
return nil, ctx.Err()
}
}
if len(allErrors) > 0 {
return allResults, errors.Join(allErrors...)
}
return allResults, nil
}
// Good - worker pool pattern
func processWithWorkerPool(ctx context.Context, items []Item, workers int) error {
jobs := make(chan Item, len(items))
results := make(chan error, len(items))
// Start workers
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 <- processItem(ctx, item)
}
}
}()
}
// Send jobs
for _, item := range items {
jobs <- item
}
close(jobs)
// Wait for workers to finish
go func() {
wg.Wait()
close(results)
}()
// Collect results
var errs []error
for err := range results {
if err != nil {
errs = append(errs, err)
}
}
if len(errs) > 0 {
return errors.Join(errs...)
}
return nil
}
Context Usage¶
// Good - pass context as first parameter
func (s *Service) GetUser(ctx context.Context, id int64) (*User, error) {
// Check for context cancellation
select {
case <-ctx.Done():
return nil, ctx.Err()
default:
}
return s.repo.FindByID(ctx, id)
}
// Good - context with timeout
func fetchWithTimeout(url string) (*Response, error) {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return nil, fmt.Errorf("creating request: %w", err)
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, fmt.Errorf("executing request: %w", err)
}
defer resp.Body.Close()
var result Response
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return nil, fmt.Errorf("decoding response: %w", err)
}
return &result, nil
}
// Good - context with values (use sparingly)
type contextKey string
const (
requestIDKey contextKey = "requestID"
userIDKey contextKey = "userID"
)
func WithRequestID(ctx context.Context, requestID string) context.Context {
return context.WithValue(ctx, requestIDKey, requestID)
}
func RequestIDFromContext(ctx context.Context) string {
if id, ok := ctx.Value(requestIDKey).(string); ok {
return id
}
return ""
}
Synchronization Primitives¶
// Good - sync.Mutex for protecting shared state
type SafeCounter struct {
mu sync.Mutex
count int
}
func (c *SafeCounter) Increment() {
c.mu.Lock()
defer c.mu.Unlock()
c.count++
}
func (c *SafeCounter) Value() int {
c.mu.Lock()
defer c.mu.Unlock()
return c.count
}
// Good - sync.RWMutex for read-heavy workloads
type Cache struct {
mu sync.RWMutex
data map[string]interface{}
}
func NewCache() *Cache {
return &Cache{
data: make(map[string]interface{}),
}
}
func (c *Cache) Get(key string) (interface{}, bool) {
c.mu.RLock()
defer c.mu.RUnlock()
val, ok := c.data[key]
return val, ok
}
func (c *Cache) Set(key string, value interface{}) {
c.mu.Lock()
defer c.mu.Unlock()
c.data[key] = value
}
// Good - sync.Once for initialization
type Database struct {
once sync.Once
conn *sql.DB
}
func (db *Database) Connection() *sql.DB {
db.once.Do(func() {
conn, err := sql.Open("postgres", os.Getenv("DATABASE_URL"))
if err != nil {
panic(err)
}
db.conn = conn
})
return db.conn
}
// Good - sync.WaitGroup for coordinating goroutines
func processAll(ctx context.Context, items []Item) error {
var wg sync.WaitGroup
errChan := make(chan error, len(items))
for _, item := range items {
wg.Add(1)
go func(item Item) {
defer wg.Done()
if err := process(ctx, item); err != nil {
errChan <- err
}
}(item)
}
wg.Wait()
close(errChan)
var errs []error
for err := range errChan {
errs = append(errs, err)
}
if len(errs) > 0 {
return errors.Join(errs...)
}
return nil
}
Graceful Shutdown¶
// Good - graceful HTTP server shutdown
func startServer(ctx context.Context, addr string, handler http.Handler) error {
server := &http.Server{
Addr: addr,
Handler: handler,
ReadTimeout: 15 * time.Second,
WriteTimeout: 15 * time.Second,
IdleTimeout: 60 * time.Second,
}
// Start server in goroutine
go func() {
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Printf("HTTP server error: %v", err)
}
}()
// Wait for shutdown signal
<-ctx.Done()
// Graceful shutdown with timeout
shutdownCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := server.Shutdown(shutdownCtx); err != nil {
return fmt.Errorf("server shutdown: %w", err)
}
return nil
}
// Good - main with signal handling
func main() {
ctx, cancel := context.WithCancel(context.Background())
// Handle shutdown signals
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
go func() {
<-sigChan
log.Println("Shutdown signal received")
cancel()
}()
// Run application
if err := run(ctx); err != nil {
log.Fatalf("Application error: %v", err)
}
}
func run(ctx context.Context) error {
// Initialize dependencies
cfg, err := config.Load("config.yaml")
if err != nil {
return fmt.Errorf("loading config: %w", err)
}
db, err := database.Connect(ctx, cfg.Database)
if err != nil {
return fmt.Errorf("connecting to database: %w", err)
}
defer db.Close()
// Start HTTP server
handler := setupRoutes(db)
return startServer(ctx, cfg.Server.Addr, handler)
}
Testing Standards¶
Table-Driven Tests¶
// Good - table-driven tests
func TestAdd(t *testing.T) {
tests := []struct {
name string
a int
b int
expected int
}{
{
name: "positive numbers",
a: 2,
b: 3,
expected: 5,
},
{
name: "negative numbers",
a: -2,
b: -3,
expected: -5,
},
{
name: "mixed numbers",
a: -2,
b: 3,
expected: 1,
},
{
name: "zero",
a: 0,
b: 0,
expected: 0,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := Add(tt.a, tt.b)
if result != tt.expected {
t.Errorf("Add(%d, %d) = %d; want %d", tt.a, tt.b, result, tt.expected)
}
})
}
}
// Good - table-driven tests with errors
func TestValidateEmail(t *testing.T) {
tests := []struct {
name string
email string
wantErr bool
errMsg string
}{
{
name: "valid email",
email: "user@example.com",
wantErr: false,
},
{
name: "missing @",
email: "userexample.com",
wantErr: true,
errMsg: "invalid email format",
},
{
name: "empty string",
email: "",
wantErr: true,
errMsg: "email is required",
},
{
name: "multiple @",
email: "user@@example.com",
wantErr: true,
errMsg: "invalid email format",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := ValidateEmail(tt.email)
if tt.wantErr {
if err == nil {
t.Errorf("ValidateEmail(%q) expected error, got nil", tt.email)
return
}
if !strings.Contains(err.Error(), tt.errMsg) {
t.Errorf("ValidateEmail(%q) error = %v; want error containing %q", tt.email, err, tt.errMsg)
}
} else {
if err != nil {
t.Errorf("ValidateEmail(%q) unexpected error: %v", tt.email, err)
}
}
})
}
}
Test Helpers and Fixtures¶
// Good - test helper functions
func setupTestDB(t *testing.T) *sql.DB {
t.Helper()
db, err := sql.Open("sqlite3", ":memory:")
if err != nil {
t.Fatalf("Failed to open database: %v", err)
}
// Run migrations
if _, err := db.Exec(schema); err != nil {
t.Fatalf("Failed to create schema: %v", err)
}
t.Cleanup(func() {
db.Close()
})
return db
}
func createTestUser(t *testing.T, db *sql.DB, username string) *User {
t.Helper()
user := &User{
Username: username,
Email: username + "@example.com",
CreatedAt: time.Now(),
}
result, err := db.Exec(
"INSERT INTO users (username, email, created_at) VALUES (?, ?, ?)",
user.Username, user.Email, user.CreatedAt,
)
if err != nil {
t.Fatalf("Failed to create test user: %v", err)
}
id, _ := result.LastInsertId()
user.ID = id
return user
}
// Good - using test helpers
func TestUserRepository_FindByID(t *testing.T) {
db := setupTestDB(t)
repo := NewUserRepository(db)
user := createTestUser(t, db, "testuser")
found, err := repo.FindByID(context.Background(), user.ID)
if err != nil {
t.Fatalf("FindByID() error = %v", err)
}
if found.Username != user.Username {
t.Errorf("FindByID() username = %q; want %q", found.Username, user.Username)
}
}
Mocking with Interfaces¶
// Good - define interface for mocking
type UserRepository interface {
Create(ctx context.Context, user *User) error
FindByID(ctx context.Context, id int64) (*User, error)
FindByEmail(ctx context.Context, email string) (*User, error)
}
// Good - mock implementation
type MockUserRepository struct {
CreateFunc func(ctx context.Context, user *User) error
FindByIDFunc func(ctx context.Context, id int64) (*User, error)
FindByEmailFunc func(ctx context.Context, email string) (*User, error)
}
func (m *MockUserRepository) Create(ctx context.Context, user *User) error {
if m.CreateFunc != nil {
return m.CreateFunc(ctx, user)
}
return nil
}
func (m *MockUserRepository) FindByID(ctx context.Context, id int64) (*User, error) {
if m.FindByIDFunc != nil {
return m.FindByIDFunc(ctx, id)
}
return nil, nil
}
func (m *MockUserRepository) FindByEmail(ctx context.Context, email string) (*User, error) {
if m.FindByEmailFunc != nil {
return m.FindByEmailFunc(ctx, email)
}
return nil, nil
}
// Good - test using mock
func TestUserService_Create(t *testing.T) {
tests := []struct {
name string
req CreateUserRequest
mockSetup func(*MockUserRepository)
wantErr bool
}{
{
name: "successful creation",
req: CreateUserRequest{
Username: "newuser",
Email: "newuser@example.com",
},
mockSetup: func(m *MockUserRepository) {
m.FindByEmailFunc = func(ctx context.Context, email string) (*User, error) {
return nil, ErrNotFound
}
m.CreateFunc = func(ctx context.Context, user *User) error {
user.ID = 1
return nil
}
},
wantErr: false,
},
{
name: "duplicate email",
req: CreateUserRequest{
Username: "newuser",
Email: "existing@example.com",
},
mockSetup: func(m *MockUserRepository) {
m.FindByEmailFunc = func(ctx context.Context, email string) (*User, error) {
return &User{ID: 1, Email: email}, nil
}
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
mockRepo := &MockUserRepository{}
tt.mockSetup(mockRepo)
service := NewUserService(mockRepo, zap.NewNop())
_, err := service.Create(context.Background(), tt.req)
if (err != nil) != tt.wantErr {
t.Errorf("Create() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
Benchmarks¶
// Good - benchmark function
func BenchmarkJSONMarshal(b *testing.B) {
user := &User{
ID: 1,
Username: "testuser",
Email: "test@example.com",
CreatedAt: time.Now(),
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _ = json.Marshal(user)
}
}
// Good - benchmark with different inputs
func BenchmarkProcessItems(b *testing.B) {
sizes := []int{10, 100, 1000, 10000}
for _, size := range sizes {
items := make([]Item, size)
for i := range items {
items[i] = Item{ID: i, Name: fmt.Sprintf("item-%d", i)}
}
b.Run(fmt.Sprintf("size=%d", size), func(b *testing.B) {
for i := 0; i < b.N; i++ {
_ = processItems(items)
}
})
}
}
// Good - benchmark with memory allocation reporting
func BenchmarkStringConcat(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
var s string
for j := 0; j < 100; j++ {
s += "x"
}
_ = s
}
}
func BenchmarkStringBuilder(b *testing.B) {
b.ReportAllocs()
for i := 0; i < b.N; i++ {
var sb strings.Builder
for j := 0; j < 100; j++ {
sb.WriteString("x")
}
_ = sb.String()
}
}
Documentation Standards¶
Package Documentation¶
// Package httputil provides HTTP utility functions for building
// web services. It includes helpers for response writing, error
// handling, and middleware.
//
// Basic usage:
//
// handler := func(w http.ResponseWriter, r *http.Request) {
// httputil.JSON(w, http.StatusOK, map[string]string{"status": "ok"})
// }
//
// Error responses:
//
// httputil.Error(w, http.StatusNotFound, "resource not found")
package httputil
import (
"encoding/json"
"net/http"
)
// JSON writes a JSON response with the given status code.
//
// Example:
//
// func handler(w http.ResponseWriter, r *http.Request) {
// data := map[string]string{"message": "Hello"}
// httputil.JSON(w, http.StatusOK, data)
// }
func JSON(w http.ResponseWriter, status int, data interface{}) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(data)
}
// Error writes an error response with the given status code and message.
//
// The response format is:
//
// {"error": "message"}
func Error(w http.ResponseWriter, status int, message string) {
JSON(w, status, map[string]string{"error": message})
}
Function and Type Documentation¶
// User represents a user account in the system.
// Users are created via the UserService and stored in the database.
type User struct {
// ID is the unique identifier for the user.
ID int64 `json:"id" db:"id"`
// Username is the user's chosen display name.
// Must be unique and between 3-50 characters.
Username string `json:"username" db:"username"`
// Email is the user's email address.
// Must be unique and valid email format.
Email string `json:"email" db:"email"`
// PasswordHash is the bcrypt hash of the user's password.
// Never exposed in JSON responses.
PasswordHash string `json:"-" db:"password_hash"`
// CreatedAt is when the user account was created.
CreatedAt time.Time `json:"created_at" db:"created_at"`
// UpdatedAt is when the user account was last modified.
UpdatedAt time.Time `json:"updated_at" db:"updated_at"`
}
// Validate checks if the user data is valid.
// It returns an error if validation fails.
func (u *User) Validate() error {
if u.Username == "" {
return errors.New("username is required")
}
if len(u.Username) < 3 || len(u.Username) > 50 {
return errors.New("username must be between 3 and 50 characters")
}
if u.Email == "" {
return errors.New("email is required")
}
if !strings.Contains(u.Email, "@") {
return errors.New("invalid email format")
}
return nil
}
// CreateUserRequest contains the data needed to create a new user.
type CreateUserRequest struct {
Username string `json:"username"`
Email string `json:"email"`
Password string `json:"password"`
}
// Validate checks if the create user request is valid.
func (r *CreateUserRequest) Validate() error {
if r.Username == "" {
return errors.New("username is required")
}
if r.Email == "" {
return errors.New("email is required")
}
if len(r.Password) < 8 {
return errors.New("password must be at least 8 characters")
}
return nil
}
Example Tests¶
// Example tests appear in godoc documentation
func ExampleJSON() {
w := httptest.NewRecorder()
data := map[string]string{"status": "ok"}
JSON(w, http.StatusOK, data)
fmt.Println(w.Code)
fmt.Println(w.Body.String())
// Output:
// 200
// {"status":"ok"}
}
func ExampleUser_Validate() {
user := &User{
Username: "johndoe",
Email: "john@example.com",
}
if err := user.Validate(); err != nil {
fmt.Println("Error:", err)
return
}
fmt.Println("Valid user")
// Output: Valid user
}
func ExampleNewUserService() {
// Create a mock repository for testing
repo := &MockUserRepository{}
logger := zap.NewNop()
service := NewUserService(repo, logger)
fmt.Printf("Service created: %T\n", service)
// Output: Service created: *UserService
}
Performance Optimization¶
Memory Management¶
// Good - pre-allocate slices when size is known
func processItems(n int) []Result {
results := make([]Result, 0, n)
for i := 0; i < n; i++ {
results = append(results, processItem(i))
}
return results
}
// Good - use sync.Pool for frequently allocated objects
var bufferPool = sync.Pool{
New: func() interface{} {
return new(bytes.Buffer)
},
}
func processRequest(data []byte) string {
buf := bufferPool.Get().(*bytes.Buffer)
defer func() {
buf.Reset()
bufferPool.Put(buf)
}()
buf.Write(data)
// Process buffer...
return buf.String()
}
// Good - avoid unnecessary allocations in hot paths
func concatenateStrings(parts []string) string {
// Calculate total length first
n := 0
for _, p := range parts {
n += len(p)
}
// Pre-allocate builder
var sb strings.Builder
sb.Grow(n)
for _, p := range parts {
sb.WriteString(p)
}
return sb.String()
}
// Good - use pointers for large structs
type LargeStruct struct {
Data [1024]byte
// ... many fields
}
// Pass by pointer to avoid copying
func processLarge(ls *LargeStruct) {
// Process...
}
// Good - use value receivers for small structs
type Point struct {
X, Y int
}
func (p Point) Distance(other Point) float64 {
dx := float64(p.X - other.X)
dy := float64(p.Y - other.Y)
return math.Sqrt(dx*dx + dy*dy)
}
Profiling¶
// Good - add pprof endpoints for profiling
import (
"net/http"
_ "net/http/pprof"
)
func main() {
// Expose pprof endpoints on a separate port
go func() {
http.ListenAndServe("localhost:6060", nil)
}()
// Main application...
}
// Profile with:
// go tool pprof http://localhost:6060/debug/pprof/heap
// go tool pprof http://localhost:6060/debug/pprof/profile?seconds=30
// go tool pprof http://localhost:6060/debug/pprof/goroutine
// Good - programmatic CPU profiling
func runWithProfiling() {
f, err := os.Create("cpu.prof")
if err != nil {
log.Fatal(err)
}
defer f.Close()
if err := pprof.StartCPUProfile(f); err != nil {
log.Fatal(err)
}
defer pprof.StopCPUProfile()
// Run workload...
}
// Good - memory profiling
func writeHeapProfile() {
f, err := os.Create("mem.prof")
if err != nil {
log.Fatal(err)
}
defer f.Close()
runtime.GC()
if err := pprof.WriteHeapProfile(f); err != nil {
log.Fatal(err)
}
}
Security Best Practices¶
Input Validation¶
// Good - validate and sanitize input
func CreateUser(w http.ResponseWriter, r *http.Request) {
var req CreateUserRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Invalid JSON", http.StatusBadRequest)
return
}
// Validate username
if len(req.Username) < 3 || len(req.Username) > 50 {
http.Error(w, "Username must be 3-50 characters", http.StatusBadRequest)
return
}
if !regexp.MustCompile(`^[a-zA-Z0-9_]+$`).MatchString(req.Username) {
http.Error(w, "Username can only contain alphanumeric characters and underscores", http.StatusBadRequest)
return
}
// Validate email
if !regexp.MustCompile(`^[^@]+@[^@]+\.[^@]+$`).MatchString(req.Email) {
http.Error(w, "Invalid email format", http.StatusBadRequest)
return
}
// Validate password strength
if len(req.Password) < 8 {
http.Error(w, "Password must be at least 8 characters", http.StatusBadRequest)
return
}
// Process valid request...
}
// Good - use a validation library
import "github.com/go-playground/validator/v10"
type CreateUserRequest struct {
Username string `json:"username" validate:"required,min=3,max=50,alphanum"`
Email string `json:"email" validate:"required,email"`
Password string `json:"password" validate:"required,min=8"`
}
var validate = validator.New()
func ValidateRequest(req interface{}) error {
return validate.Struct(req)
}
SQL Injection Prevention¶
// Good - use parameterized queries
func GetUserByEmail(ctx context.Context, db *sql.DB, email string) (*User, error) {
query := `SELECT id, username, email, created_at FROM users WHERE email = $1`
row := db.QueryRowContext(ctx, query, email)
var user User
err := row.Scan(&user.ID, &user.Username, &user.Email, &user.CreatedAt)
if err != nil {
return nil, err
}
return &user, nil
}
// Good - use query builder or ORM
import "github.com/Masterminds/squirrel"
func SearchUsers(ctx context.Context, db *sql.DB, filters UserFilters) ([]*User, error) {
query := squirrel.Select("id", "username", "email", "created_at").
From("users").
PlaceholderFormat(squirrel.Dollar)
if filters.Username != "" {
query = query.Where(squirrel.Like{"username": "%" + filters.Username + "%"})
}
if filters.Email != "" {
query = query.Where(squirrel.Eq{"email": filters.Email})
}
sql, args, err := query.ToSql()
if err != nil {
return nil, err
}
rows, err := db.QueryContext(ctx, sql, args...)
if err != nil {
return nil, err
}
defer rows.Close()
var users []*User
for rows.Next() {
var user User
if err := rows.Scan(&user.ID, &user.Username, &user.Email, &user.CreatedAt); err != nil {
return nil, err
}
users = append(users, &user)
}
return users, rows.Err()
}
// Bad - string concatenation (SQL injection vulnerability)
func GetUserByEmailUnsafe(db *sql.DB, email string) (*User, error) {
// NEVER DO THIS - vulnerable to SQL injection
query := fmt.Sprintf("SELECT * FROM users WHERE email = '%s'", email)
// ...
}
Secret Management¶
// Good - use environment variables for secrets
func LoadConfig() *Config {
return &Config{
DatabaseURL: os.Getenv("DATABASE_URL"),
APIKey: os.Getenv("API_KEY"),
JWTSecret: os.Getenv("JWT_SECRET"),
}
}
// Good - validate required secrets on startup
func validateSecrets() error {
required := []string{
"DATABASE_URL",
"API_KEY",
"JWT_SECRET",
}
var missing []string
for _, key := range required {
if os.Getenv(key) == "" {
missing = append(missing, key)
}
}
if len(missing) > 0 {
return fmt.Errorf("missing required environment variables: %v", missing)
}
return nil
}
// Good - use a secrets manager
import (
"github.com/aws/aws-sdk-go-v2/service/secretsmanager"
)
func GetSecret(ctx context.Context, client *secretsmanager.Client, secretID string) (string, error) {
input := &secretsmanager.GetSecretValueInput{
SecretId: &secretID,
}
result, err := client.GetSecretValue(ctx, input)
if err != nil {
return "", fmt.Errorf("getting secret: %w", err)
}
return *result.SecretString, nil
}
// Bad - hardcoded secrets
const (
APIKey = "sk_live_abc123" // NEVER DO THIS
JWTSecret = "supersecret" // NEVER DO THIS
)
Password Hashing¶
import "golang.org/x/crypto/bcrypt"
// Good - hash passwords with bcrypt
func HashPassword(password string) (string, error) {
bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return "", fmt.Errorf("hashing password: %w", err)
}
return string(bytes), nil
}
// Good - verify passwords
func CheckPassword(hashedPassword, password string) bool {
err := bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password))
return err == nil
}
// Good - complete authentication flow
func (s *AuthService) Authenticate(ctx context.Context, email, password string) (*User, error) {
user, err := s.repo.FindByEmail(ctx, email)
if err != nil {
if errors.Is(err, ErrNotFound) {
// Don't reveal whether email exists
return nil, ErrInvalidCredentials
}
return nil, fmt.Errorf("finding user: %w", err)
}
if !CheckPassword(user.PasswordHash, password) {
return nil, ErrInvalidCredentials
}
return user, nil
}
Anti-Patterns to Avoid¶
Ignoring Errors¶
// Bad - ignoring errors
func processFile(path string) {
data, _ := os.ReadFile(path) // Error ignored!
processData(data)
}
// Good - handle all errors
func processFile(path string) error {
data, err := os.ReadFile(path)
if err != nil {
return fmt.Errorf("reading file: %w", err)
}
return processData(data)
}
Goroutine Leaks¶
// Bad - goroutine leak (channel never closed)
func processItems(items []Item) {
results := make(chan Result)
for _, item := range items {
go func(item Item) {
results <- process(item) // May block forever
}(item)
}
// results channel never consumed if function returns early
}
// Good - proper goroutine cleanup
func processItems(ctx context.Context, items []Item) ([]Result, error) {
results := make(chan Result, len(items))
var wg sync.WaitGroup
for _, item := range items {
wg.Add(1)
go func(item Item) {
defer wg.Done()
select {
case results <- process(item):
case <-ctx.Done():
}
}(item)
}
go func() {
wg.Wait()
close(results)
}()
var allResults []Result
for result := range results {
allResults = append(allResults, result)
}
return allResults, nil
}
Nil Map Assignment¶
// Bad - nil map panic
func addToMap(key, value string) {
var m map[string]string
m[key] = value // Panic: assignment to entry in nil map
}
// Good - initialize map
func addToMap(key, value string) map[string]string {
m := make(map[string]string)
m[key] = value
return m
}
// Good - check before adding
func addToMapSafe(m map[string]string, key, value string) map[string]string {
if m == nil {
m = make(map[string]string)
}
m[key] = value
return m
}
Mutex Copying¶
// Bad - copying mutex (leads to data races)
type Counter struct {
mu sync.Mutex
count int
}
func (c Counter) Value() int { // Value receiver copies mutex!
c.mu.Lock()
defer c.mu.Unlock()
return c.count
}
// Good - use pointer receiver for mutex
type Counter struct {
mu sync.Mutex
count int
}
func (c *Counter) Value() int { // Pointer receiver
c.mu.Lock()
defer c.mu.Unlock()
return c.count
}
func (c *Counter) Increment() {
c.mu.Lock()
defer c.mu.Unlock()
c.count++
}
Interface Pollution¶
// Bad - overly broad interface
type DataManager interface {
Create(ctx context.Context, data interface{}) error
Read(ctx context.Context, id string) (interface{}, error)
Update(ctx context.Context, id string, data interface{}) error
Delete(ctx context.Context, id string) error
List(ctx context.Context) ([]interface{}, error)
Search(ctx context.Context, query string) ([]interface{}, error)
Validate(data interface{}) error
Transform(data interface{}) interface{}
Export(ctx context.Context, format string) ([]byte, error)
Import(ctx context.Context, data []byte) error
}
// Good - small, focused interfaces
type Reader interface {
Read(ctx context.Context, id string) (*Entity, error)
}
type Writer interface {
Write(ctx context.Context, entity *Entity) error
}
type Deleter interface {
Delete(ctx context.Context, id string) error
}
// Compose as needed
type ReadWriter interface {
Reader
Writer
}
Recommended Tools¶
Formatters¶
- gofmt: Standard Go formatter
- Run:
gofmt -w . -
Included with Go installation
-
goimports: Format + organize imports
- Installation:
go install golang.org/x/tools/cmd/goimports@latest - Run:
goimports -w .
Linters¶
- golangci-lint: Meta-linter running multiple linters
- Installation:
go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest - Run:
golangci-lint run - Configuration:
.golangci.yml
# .golangci.yml
run:
timeout: 5m
tests: true
linters:
enable:
- errcheck
- govet
- staticcheck
- unused
- gosimple
- ineffassign
- misspell
- gofmt
- goimports
- revive
- gosec
- prealloc
- unconvert
- gocritic
linters-settings:
errcheck:
check-type-assertions: true
check-blank: true
govet:
enable-all: true
revive:
rules:
- name: blank-imports
- name: context-as-argument
- name: context-keys-type
- name: error-return
- name: error-strings
- name: error-naming
- name: exported
- name: increment-decrement
- name: var-naming
- name: package-comments
- name: range
- name: receiver-naming
- name: time-naming
- name: unexported-return
- name: indent-error-flow
- name: errorf
gosec:
excludes:
- G104 # Audit errors not checked
issues:
exclude-rules:
- path: _test\.go
linters:
- errcheck
- gosec
Testing Tools¶
- go test: Built-in test runner
- Run:
go test ./... - With coverage:
go test -cover ./... -
Verbose:
go test -v ./... -
gotestsum: Enhanced test output
- Installation:
go install gotest.tools/gotestsum@latest - Run:
gotestsum --format testname
IDE Extensions¶
- gopls (Go Language Server): Official language server
- Provides code completion, navigation, refactoring
-
Used by VS Code Go extension and other editors
-
VS Code Go Extension: Rich Go support
- Auto-formatting on save
- Test discovery and running
- Debugging support
- Linter integration
Pre-commit Configuration¶
# .pre-commit-config.yaml
repos:
- repo: https://github.com/dnephin/pre-commit-golang
rev: v0.5.1
hooks:
- id: go-fmt
- id: go-imports
- id: go-vet
- id: go-lint
- id: go-mod-tidy
- repo: https://github.com/golangci/golangci-lint
rev: v1.55.2
hooks:
- id: golangci-lint
Complete Example¶
HTTP API Server¶
// cmd/api/main.go
package main
import (
"context"
"log"
"os"
"os/signal"
"syscall"
"time"
"github.com/myorg/myproject/internal/config"
"github.com/myorg/myproject/internal/handler"
"github.com/myorg/myproject/internal/repository"
"github.com/myorg/myproject/internal/service"
"go.uber.org/zap"
)
func main() {
// Initialize logger
logger, err := zap.NewProduction()
if err != nil {
log.Fatalf("Failed to initialize logger: %v", err)
}
defer logger.Sync()
// Load configuration
cfg, err := config.Load("config.yaml")
if err != nil {
logger.Fatal("Failed to load configuration", zap.Error(err))
}
// Run application
if err := run(cfg, logger); err != nil {
logger.Fatal("Application error", zap.Error(err))
}
}
func run(cfg *config.Config, logger *zap.Logger) error {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// Handle shutdown signals
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
go func() {
<-sigChan
logger.Info("Shutdown signal received")
cancel()
}()
// Initialize database
db, err := repository.NewDatabase(ctx, cfg.Database)
if err != nil {
return err
}
defer db.Close()
// Initialize repositories
userRepo := repository.NewUserRepository(db)
// Initialize services
userService := service.NewUserService(userRepo, logger)
// Initialize handlers
userHandler := handler.NewUserHandler(userService, logger)
// Setup HTTP server
server := handler.NewServer(cfg.Server, userHandler, logger)
// Start server
return server.Run(ctx)
}
// internal/handler/user.go
package handler
import (
"encoding/json"
"net/http"
"strconv"
"github.com/go-chi/chi/v5"
"github.com/myorg/myproject/internal/model"
"github.com/myorg/myproject/internal/service"
"go.uber.org/zap"
)
type UserHandler struct {
service *service.UserService
logger *zap.Logger
}
func NewUserHandler(service *service.UserService, logger *zap.Logger) *UserHandler {
return &UserHandler{
service: service,
logger: logger,
}
}
func (h *UserHandler) Routes() chi.Router {
r := chi.NewRouter()
r.Post("/", h.Create)
r.Get("/{id}", h.GetByID)
r.Put("/{id}", h.Update)
r.Delete("/{id}", h.Delete)
r.Get("/", h.List)
return r
}
func (h *UserHandler) Create(w http.ResponseWriter, r *http.Request) {
var req model.CreateUserRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
h.error(w, http.StatusBadRequest, "Invalid request body")
return
}
user, err := h.service.Create(r.Context(), req)
if err != nil {
h.logger.Error("Failed to create user", zap.Error(err))
h.error(w, http.StatusInternalServerError, "Failed to create user")
return
}
h.json(w, http.StatusCreated, user)
}
func (h *UserHandler) GetByID(w http.ResponseWriter, r *http.Request) {
id, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
if err != nil {
h.error(w, http.StatusBadRequest, "Invalid user ID")
return
}
user, err := h.service.GetByID(r.Context(), id)
if err != nil {
if err == service.ErrNotFound {
h.error(w, http.StatusNotFound, "User not found")
return
}
h.logger.Error("Failed to get user", zap.Error(err))
h.error(w, http.StatusInternalServerError, "Failed to get user")
return
}
h.json(w, http.StatusOK, user)
}
func (h *UserHandler) Update(w http.ResponseWriter, r *http.Request) {
id, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
if err != nil {
h.error(w, http.StatusBadRequest, "Invalid user ID")
return
}
var req model.UpdateUserRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
h.error(w, http.StatusBadRequest, "Invalid request body")
return
}
user, err := h.service.Update(r.Context(), id, req)
if err != nil {
if err == service.ErrNotFound {
h.error(w, http.StatusNotFound, "User not found")
return
}
h.logger.Error("Failed to update user", zap.Error(err))
h.error(w, http.StatusInternalServerError, "Failed to update user")
return
}
h.json(w, http.StatusOK, user)
}
func (h *UserHandler) Delete(w http.ResponseWriter, r *http.Request) {
id, err := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64)
if err != nil {
h.error(w, http.StatusBadRequest, "Invalid user ID")
return
}
if err := h.service.Delete(r.Context(), id); err != nil {
if err == service.ErrNotFound {
h.error(w, http.StatusNotFound, "User not found")
return
}
h.logger.Error("Failed to delete user", zap.Error(err))
h.error(w, http.StatusInternalServerError, "Failed to delete user")
return
}
w.WriteHeader(http.StatusNoContent)
}
func (h *UserHandler) List(w http.ResponseWriter, r *http.Request) {
users, err := h.service.List(r.Context())
if err != nil {
h.logger.Error("Failed to list users", zap.Error(err))
h.error(w, http.StatusInternalServerError, "Failed to list users")
return
}
h.json(w, http.StatusOK, users)
}
func (h *UserHandler) json(w http.ResponseWriter, status int, data interface{}) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(data)
}
func (h *UserHandler) error(w http.ResponseWriter, status int, message string) {
h.json(w, status, map[string]string{"error": message})
}
References¶
Official Documentation¶
Community Style Guides¶
Books and Resources¶
- The Go Programming Language - Donovan & Kernighan
- Go in Action - Kennedy, Ketelsen & Martin
- Concurrency in Go - Cox-Buday
Tools Documentation¶
Related Guides¶
- Terraform Style Guide - Go is used for Terraform providers
- Kubernetes Style Guide - Go is used for Kubernetes operators
- Docker Style Guide - Multi-stage builds for Go applications
- Metadata Schema Reference
Maintainer: Tyler Dukes