Go Expert Skill
Expert-level Go patterns for API development, concurrency, and idiomatic Go code.
Auto-Detection
This skill activates when:
-
Working with Go projects
-
Detected go.mod file
-
Working with *.go files
-
Using Gin, Echo, Fiber, or standard net/http
- Project Structure
Standard Layout
project/ ├── cmd/ │ └── api/ │ └── main.go # Entry point ├── internal/ │ ├── handlers/ # HTTP handlers │ ├── services/ # Business logic │ ├── repositories/ # Data access │ ├── models/ # Domain models │ └── middleware/ # HTTP middleware ├── pkg/ # Public packages ├── config/ # Configuration ├── migrations/ # DB migrations ├── go.mod └── go.sum
- Error Handling
Custom Errors
// ✅ GOOD - Typed errors
type AppError struct {
Code string json:"code"
Message string json:"message"
Status int json:"-"
}
func (e *AppError) Error() string { return e.Message }
var ( ErrNotFound = &AppError{Code: "NOT_FOUND", Message: "Resource not found", Status: 404} ErrUnauthorized = &AppError{Code: "UNAUTHORIZED", Message: "Unauthorized", Status: 401} ErrValidation = &AppError{Code: "VALIDATION", Message: "Validation failed", Status: 400} )
// ✅ GOOD - Wrap errors with context func (s *UserService) GetUser(id string) (*User, error) { user, err := s.repo.FindByID(id) if err != nil { if errors.Is(err, sql.ErrNoRows) { return nil, ErrNotFound } return nil, fmt.Errorf("failed to get user: %w", err) } return user, nil }
Error Checking
// ❌ BAD - Ignoring errors user, _ := service.GetUser(id)
// ✅ GOOD - Always handle errors user, err := service.GetUser(id) if err != nil { if errors.Is(err, ErrNotFound) { return c.JSON(404, gin.H{"error": "User not found"}) } return c.JSON(500, gin.H{"error": "Internal error"}) }
- Gin Framework Patterns
Handler Structure
// ✅ GOOD - Handler with dependency injection type UserHandler struct { userService *UserService }
func NewUserHandler(us *UserService) *UserHandler { return &UserHandler{userService: us} }
func (h *UserHandler) GetUser(c *gin.Context) { id := c.Param("id")
user, err := h.userService.GetUser(id)
if err != nil {
if errors.Is(err, ErrNotFound) {
c.JSON(404, gin.H{"error": "User not found"})
return
}
c.JSON(500, gin.H{"error": "Internal error"})
return
}
c.JSON(200, gin.H{"data": user})
}
func (h *UserHandler) CreateUser(c *gin.Context) { var req CreateUserRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(400, gin.H{"error": err.Error()}) return }
user, err := h.userService.CreateUser(&req)
if err != nil {
c.JSON(500, gin.H{"error": err.Error()})
return
}
c.JSON(201, gin.H{"data": user})
}
Request Validation
// ✅ GOOD - Struct tags for validation
type CreateUserRequest struct {
Email string json:"email" binding:"required,email"
Name string json:"name" binding:"required,min=2,max=100"
Password string json:"password" binding:"required,min=8"
}
Middleware
// ✅ GOOD - Authentication middleware func AuthMiddleware(secret string) gin.HandlerFunc { return func(c *gin.Context) { token := c.GetHeader("Authorization") if token == "" { c.AbortWithStatusJSON(401, gin.H{"error": "Missing token"}) return }
claims, err := validateToken(token, secret)
if err != nil {
c.AbortWithStatusJSON(401, gin.H{"error": "Invalid token"})
return
}
c.Set("user_id", claims.UserID)
c.Next()
}
}
- Concurrency Patterns
Goroutines with WaitGroup
// ✅ GOOD - Wait for multiple goroutines func processItems(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(item); err != nil {
errChan <- err
}
}(item)
}
wg.Wait()
close(errChan)
// Collect errors
var errs []error
for err := range errChan {
errs = append(errs, err)
}
if len(errs) > 0 {
return fmt.Errorf("processing failed: %v", errs)
}
return nil
}
Context with Timeout
// ✅ GOOD - Timeout handling func fetchWithTimeout(url string) ([]byte, error) { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel()
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return nil, err
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
return io.ReadAll(resp.Body)
}
Worker Pool
// ✅ GOOD - Bounded concurrency func processWithWorkers(jobs <-chan Job, results chan<- Result, workers int) { var wg sync.WaitGroup
for i := 0; i < workers; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for job := range jobs {
result := processJob(job)
results <- result
}
}()
}
wg.Wait()
close(results)
}
Channels for Communication
// ✅ GOOD - Select for channel operations func fetchAll(urls []string) []Response { results := make(chan Response, len(urls))
for _, url := range urls {
go func(url string) {
data, err := fetch(url)
results <- Response{URL: url, Data: data, Err: err}
}(url)
}
var responses []Response
for i := 0; i < len(urls); i++ {
responses = append(responses, <-results)
}
return responses
}
- Database Patterns
sqlx with Prepared Statements
// ✅ GOOD - Named queries type UserRepository struct { db *sqlx.DB }
func (r *UserRepository) FindByID(id string) (*User, error) { var user User err := r.db.Get(&user, "SELECT * FROM users WHERE id = $1", id) if err != nil { return nil, err } return &user, nil }
func (r *UserRepository) Create(user *User) error {
query := INSERT INTO users (id, email, name, password_hash, created_at) VALUES (:id, :email, :name, :password_hash, :created_at)
_, err := r.db.NamedExec(query, user)
return err
}
GORM Patterns
// ✅ GOOD - GORM with preloading
type User struct {
ID string gorm:"primaryKey"
Email string gorm:"uniqueIndex"
Name string
Posts []Post gorm:"foreignKey:AuthorID"
CreatedAt time.Time
}
func (r *UserRepository) FindWithPosts(id string) (*User, error) { var user User err := r.db.Preload("Posts").First(&user, "id = ?", id).Error if err != nil { return nil, err } return &user, nil }
// ✅ GOOD - Transactions func (r *UserRepository) CreateWithProfile(user *User, profile *Profile) error { return r.db.Transaction(func(tx *gorm.DB) error { if err := tx.Create(user).Error; err != nil { return err } profile.UserID = user.ID if err := tx.Create(profile).Error; err != nil { return err } return nil }) }
- Interface Design
// ✅ GOOD - Small, focused interfaces type UserReader interface { FindByID(id string) (*User, error) FindByEmail(email string) (*User, error) }
type UserWriter interface { Create(user *User) error Update(user *User) error Delete(id string) error }
type UserRepository interface { UserReader UserWriter }
// ✅ GOOD - Accept interfaces, return structs func NewUserService(repo UserRepository) *UserService { return &UserService{repo: repo} }
- Configuration
// ✅ GOOD - Struct-based config type Config struct { Server ServerConfig Database DatabaseConfig JWT JWTConfig }
type ServerConfig struct {
Port string env:"PORT" envDefault:"8080"
Host string env:"HOST" envDefault:"localhost"
}
type DatabaseConfig struct {
URL string env:"DATABASE_URL,required"
MaxOpenConns int env:"DB_MAX_OPEN_CONNS" envDefault:"25"
MaxIdleConns int env:"DB_MAX_IDLE_CONNS" envDefault:"5"
}
func LoadConfig() (*Config, error) { var cfg Config if err := env.Parse(&cfg); err != nil { return nil, err } return &cfg, nil }
- Testing
Table-Driven Tests
// ✅ GOOD - Table-driven tests func TestValidateEmail(t *testing.T) { tests := []struct { name string email string wantErr bool }{ {"valid email", "test@example.com", false}, {"invalid format", "invalid", true}, {"empty", "", true}, {"missing domain", "test@", true}, }
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := ValidateEmail(tt.email)
if (err != nil) != tt.wantErr {
t.Errorf("ValidateEmail(%q) error = %v, wantErr %v", tt.email, err, tt.wantErr)
}
})
}
}
HTTP Handler Tests
// ✅ GOOD - httptest for handlers func TestGetUser(t *testing.T) { mockService := &MockUserService{ GetUserFn: func(id string) (*User, error) { return &User{ID: id, Name: "Test"}, nil }, }
handler := NewUserHandler(mockService)
router := gin.Default()
router.GET("/users/:id", handler.GetUser)
req := httptest.NewRequest("GET", "/users/123", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
if w.Code != 200 {
t.Errorf("expected 200, got %d", w.Code)
}
}
Mocking with Interfaces
// ✅ GOOD - Interface-based mocking type MockUserRepository struct { FindByIDFn func(id string) (*User, error) CreateFn func(user *User) error }
func (m *MockUserRepository) FindByID(id string) (*User, error) { return m.FindByIDFn(id) }
func (m *MockUserRepository) Create(user *User) error { return m.CreateFn(user) }
- Logging
// ✅ GOOD - Structured logging with zerolog import "github.com/rs/zerolog/log"
func (s *UserService) CreateUser(req *CreateUserRequest) (*User, error) { log.Info(). Str("email", req.Email). Msg("Creating user")
user, err := s.repo.Create(req)
if err != nil {
log.Error().
Err(err).
Str("email", req.Email).
Msg("Failed to create user")
return nil, err
}
log.Info().
Str("user_id", user.ID).
Msg("User created successfully")
return user, nil
}
Quick Reference
checklist[12]{pattern,best_practice}: Errors,Always check errors wrap with %w Interfaces,Small focused accept interface return struct Goroutines,WaitGroup + errgroup for coordination Context,WithTimeout for cancellation Channels,Buffered for async unbuffered for sync Testing,Table-driven tests + httptest Config,Struct with env tags DB,Prepared statements + transactions Logging,Structured with zerolog/zap Validation,Struct tags for binding Middleware,gin.HandlerFunc pattern DI,Constructor injection
Version: 1.3.0