원클릭으로
golang-patterns
Idiomatic Go patterns, best practices, and conventions for building robust, efficient, and maintainable Go applications.
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
메뉴
Idiomatic Go patterns, best practices, and conventions for building robust, efficient, and maintainable Go applications.
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
SOC 직업 분류 기준
Database migration best practices for schema changes, data migrations, rollbacks, and zero-downtime deployments across PostgreSQL, MySQL, and common ORMs (Prisma, Drizzle, Kysely, Django, TypeORM, golang-migrate).
Diátaxis Documentation Expert. An expert technical writer specializing in creating high-quality software documentation, guided by the principles and structure of the Diátaxis technical documentation authoring framework.
Create distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, artifacts, posters, or applications (examples include websites, landing pages, dashboards, React components, HTML/CSS layouts, or when styling/beautifying any web UI). Generates creative, polished code and UI design that avoids generic AI aesthetics.
Comprehensive guide for dependency injection (DI) in Golang. Covers why DI matters (testability, loose coupling, separation of concerns, lifecycle management), manual constructor injection, and DI library comparison (google/wire, uber-go/dig, uber-go/fx, samber/do). Use this skill when designing service architecture, setting up dependency injection, refactoring tightly coupled code, managing singletons or service factories, or when the user asks about inversion of control, service containers, or wiring dependencies in Go.
Build REST APIs with Go Gin. Use when creating Go web servers, adding Gin routes, writing handlers, or asking about middleware, binding, error handling, or project structure.
Implements concurrent Go patterns using goroutines and channels, designs and builds microservices with gRPC or REST, optimizes Go application performance with pprof, and enforces idiomatic Go with generics, interfaces, and robust error handling. Use when building Go applications requiring concurrent programming, microservices architecture, or high-performance systems. Invoke for goroutines, channels, Go generics, gRPC integration, CLI tools, benchmarks, or table-driven testing.
| name | golang-patterns |
| description | Idiomatic Go patterns, best practices, and conventions for building robust, efficient, and maintainable Go applications. |
| origin | ECC |
Idiomatic Go patterns and best practices for building robust, efficient, and maintainable applications.
Go favors simplicity over cleverness. Code should be obvious and easy to read.
// Good: Clear and direct
func GetUser(id string) (*User, error) {
user, err := db.FindUser(id)
if err != nil {
return nil, fmt.Errorf("get user %s: %w", id, err)
}
return user, nil
}
// Bad: Overly clever
func GetUser(id string) (*User, error) {
return func() (*User, error) {
if u, e := db.FindUser(id); e == nil {
return u, nil
} else {
return nil, e
}
}()
}
Design types so their zero value is immediately usable without initialization.
// Good: Zero value is useful
type Counter struct {
mu sync.Mutex
count int // zero value is 0, ready to use
}
func (c *Counter) Inc() {
c.mu.Lock()
c.count++
c.mu.Unlock()
}
// Good: bytes.Buffer works with zero value
var buf bytes.Buffer
buf.WriteString("hello")
// Bad: Requires initialization
type BadCounter struct {
counts map[string]int // nil map will panic
}
Functions should accept interface parameters and return concrete types.
// Good: Accepts interface, returns concrete type
func ProcessData(r io.Reader) (*Result, error) {
data, err := io.ReadAll(r)
if err != nil {
return nil, err
}
return &Result{Data: data}, nil
}
// Bad: Returns interface (hides implementation details unnecessarily)
func ProcessData(r io.Reader) (io.Reader, error) {
// ...
}
AcademyHub Requirement: ALWAYS wrap errors with fmt.Errorf using the %w verb to preserve the error chain. This enables proper error inspection with errors.Is and errors.As.
// ✅ GOOD: Wrap errors with context using %w
func LoadConfig(path string) (*Config, error) {
data, err := os.ReadFile(path)
if err != nil {
return nil, fmt.Errorf("load config %s: %w", path, err)
}
var cfg Config
if err := json.Unmarshal(data, &cfg); err != nil {
return nil, fmt.Errorf("parse config %s: %w", path, err)
}
return &cfg, nil
}
// ✅ ACADEMYHUB SPECIFIC: Service layer error wrapping with business context
func (s *StudentService) Create(ctx context.Context, req *dto.CreateStudentRequest) (*dto.StudentResponse, error) {
// Validate input
if err := s.validator.Validate(req); err != nil {
return nil, fmt.Errorf("validation failed for student creation: %w", err)
}
// Map DTO to domain entity
student := req.ToDomain()
// Repository operation with error wrapping
err := s.repo.Create(ctx, student)
if err != nil {
return nil, fmt.Errorf("failed to create student in repository: %w", err)
}
// Map domain entity to response DTO
return dto.ToStudentResponse(student), nil
}
// ✅ ACADEMYHUB SPECIFIC: Handler layer error handling with logging
func (h *StudentHandler) Create(c *gin.Context) {
var req dto.CreateStudentRequest
if err := c.ShouldBindJSON(&req); err != nil {
h.logger.Error("Invalid request payload", zap.Error(err))
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request"})
return
}
result, err := h.service.Create(c.Request.Context(), &req)
if err != nil {
// Log with structured context
h.logger.Error("Failed to create student",
zap.Error(err),
zap.String("student_name", req.Name),
zap.Uint("academyhub_id", req.AcademyHubID))
// Handle specific error types
if errors.Is(err, domain.ErrStudentAlreadyExists) {
c.JSON(http.StatusConflict, gin.H{"error": "Student already exists"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "Internal server error"})
return
}
c.JSON(http.StatusCreated, result)
}
// Define domain-specific errors
type ValidationError struct {
Field string
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("validation failed on %s: %s", e.Field, e.Message)
}
// Sentinel errors for common cases (AcademyHub convention: Err prefix)
var (
ErrNotFound = errors.New("resource not found")
ErrUnauthorized = errors.New("unauthorized")
ErrInvalidInput = errors.New("invalid input")
ErrStudentAlreadyExists = errors.New("student already exists") // Domain-specific
)
// Define domain-specific errors
type ValidationError struct {
Field string
Message string
}
func (e *ValidationError) Error() string {
return fmt.Sprintf("validation failed on %s: %s", e.Field, e.Message)
}
// Sentinel errors for common cases
var (
ErrNotFound = errors.New("resource not found")
ErrUnauthorized = errors.New("unauthorized")
ErrInvalidInput = errors.New("invalid input")
)
func HandleError(err error) {
// Check for specific error
if errors.Is(err, sql.ErrNoRows) {
log.Println("No records found")
return
}
// Check for error type
var validationErr *ValidationError
if errors.As(err, &validationErr) {
log.Printf("Validation error on field %s: %s",
validationErr.Field, validationErr.Message)
return
}
// Unknown error
log.Printf("Unexpected error: %v", err)
}
// Bad: Ignoring error with blank identifier
result, _ := doSomething()
// Good: Handle or explicitly document why it's safe to ignore
result, err := doSomething()
if err != nil {
return err
}
// Acceptable: When error truly doesn't matter (rare)
_ = writer.Close() // Best-effort cleanup, error logged elsewhere
func WorkerPool(jobs <-chan Job, results chan<- Result, numWorkers int) {
var wg sync.WaitGroup
for i := 0; i < numWorkers; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for job := range jobs {
results <- process(job)
}
}()
}
wg.Wait()
close(results)
}
Mandatory: All external calls (DB, APIs, file operations) MUST implement proper context cancellation with timeouts or deadlines. ALWAYS use defer cancel() immediately after creating a context.
// ✅ ACADEMYHUB SPECIFIC: Database operation with context timeout
func (r *studentRepository) FindByID(ctx context.Context, id uint) (*domain.Student, error) {
// Add timeout to prevent hanging database queries
ctx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
var student domain.Student
err := r.db.WithContext(ctx).Where("id = ?", id).First(&student).Error
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, fmt.Errorf("student not found with id %d: %w", id, domain.ErrNotFound)
}
return nil, fmt.Errorf("database error finding student: %w", err)
}
return &student, nil
}
// ✅ ACADEMYHUB SPECIFIC: HTTP client with context propagation
func (s *externalService) FetchUserData(ctx context.Context, userID string) (*UserData, error) {
// Propagate parent context with additional timeout
ctx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
req, err := http.NewRequestWithContext(ctx, "GET", s.baseURL+"/users/"+userID, nil)
if err != nil {
return nil, fmt.Errorf("create request: %w", err)
}
resp, err := s.client.Do(req)
if err != nil {
return nil, fmt.Errorf("fetch user data: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("unexpected status code %d", resp.StatusCode)
}
var userData UserData
if err := json.NewDecoder(resp.Body).Decode(&userData); err != nil {
return nil, fmt.Errorf("decode response: %w", err)
}
return &userData, nil
}
// ✅ ACADEMYHUB SPECIFIC: Service layer with context propagation
func (s *StudentService) GetWithExternalData(ctx context.Context, id uint) (*dto.StudentWithExternalResponse, error) {
// Get student from repository
student, err := s.repo.FindByID(ctx, id)
if err != nil {
return nil, fmt.Errorf("failed to find student: %w", err)
}
// Concurrently fetch external data with context propagation
var wg sync.WaitGroup
var userData *UserData
var userDataErr error
wg.Add(1)
go func() {
defer wg.Done()
userData, userDataErr = s.externalService.FetchUserData(ctx, student.ExternalID)
}()
wg.Wait()
if userDataErr != nil {
return nil, fmt.Errorf("failed to fetch external user data: %w", userDataErr)
}
return &dto.StudentWithExternalResponse{
Student: dto.ToStudentResponse(student),
UserData: userData,
}, nil
}
func GracefulShutdown(server *http.Server) {
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
log.Println("Shutting down server...")
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := server.Shutdown(ctx); err != nil {
log.Fatalf("Server forced to shutdown: %v", err)
}
log.Println("Server exited")
}
func GracefulShutdown(server *http.Server) {
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
log.Println("Shutting down server...")
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := server.Shutdown(ctx); err != nil {
log.Fatalf("Server forced to shutdown: %v", err)
}
log.Println("Server exited")
}
import "golang.org/x/sync/errgroup"
func FetchAll(ctx context.Context, urls []string) ([][]byte, error) {
g, ctx := errgroup.WithContext(ctx)
results := make([][]byte, len(urls))
for i, url := range urls {
i, url := i, url // Capture loop variables
g.Go(func() error {
data, err := FetchWithTimeout(ctx, url)
if err != nil {
return err
}
results[i] = data
return nil
})
}
if err := g.Wait(); err != nil {
return nil, err
}
return results, nil
}
// Bad: Goroutine leak if context is cancelled
func leakyFetch(ctx context.Context, url string) <-chan []byte {
ch := make(chan []byte)
go func() {
data, _ := fetch(url)
ch <- data // Blocks forever if no receiver
}()
return ch
}
// Good: Properly handles cancellation
func safeFetch(ctx context.Context, url string) <-chan []byte {
ch := make(chan []byte, 1) // Buffered channel
go func() {
data, err := fetch(url)
if err != nil {
return
}
select {
case ch <- data:
case <-ctx.Done():
}
}()
return ch
}
// Good: Single-method interfaces
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
}
// Compose interfaces as needed
type ReadWriteCloser interface {
Reader
Writer
Closer
}
// In the consumer package, not the provider
package service
// UserStore defines what this service needs
type UserStore interface {
GetUser(id string) (*User, error)
SaveUser(user *User) error
}
type Service struct {
store UserStore
}
// Concrete implementation can be in another package
// It doesn't need to know about this interface
type Flusher interface {
Flush() error
}
func WriteAndFlush(w io.Writer, data []byte) error {
if _, err := w.Write(data); err != nil {
return err
}
// Flush if supported
if f, ok := w.(Flusher); ok {
return f.Flush()
}
return nil
}
myproject/
├── cmd/
│ └── myapp/
│ └── main.go # Entry point
├── internal/
│ ├── handler/ # HTTP handlers
│ ├── service/ # Business logic
│ ├── repository/ # Data access
│ └── config/ # Configuration
├── pkg/
│ └── client/ # Public API client
├── api/
│ └── v1/ # API definitions (proto, OpenAPI)
├── testdata/ # Test fixtures
├── go.mod
├── go.sum
└── Makefile
// Good: Short, lowercase, no underscores
package http
package json
package user
// Bad: Verbose, mixed case, or redundant
package httpHandler
package json_parser
package userService // Redundant 'Service' suffix
// Bad: Global mutable state
var db *sql.DB
func init() {
db, _ = sql.Open("postgres", os.Getenv("DATABASE_URL"))
}
// Good: Dependency injection
type Server struct {
db *sql.DB
}
func NewServer(db *sql.DB) *Server {
return &Server{db: db}
}
Critical: To prevent "Fat Handlers" and maintain Clean Architecture, ALL mapping logic between DTOs and Domain entities MUST be encapsulated in dedicated mapper functions within the dto package.
func (req *CreateUserReq) ToDomain() *domain.Userfunc ToUserResponse(d *domain.User) *UserResponse// File: internal/dto/student.go
package dto
import (
"time"
"github.com/example-org/academyhub-backend/internal/domain"
)
// Request DTO
type CreateStudentRequest struct {
Name string `json:"name" validate:"required,min=3"`
Email string `json:"email" validate:"required,email"`
Phone string `json:"phone" validate:"required"`
ClassID uint `json:"class_id" validate:"required"`
AcademyHubID uint `json:"academyhub_id" validate:"required"`
}
// ✅ ACADEMYHUB SPECIFIC: Request → Domain mapper method
func (req *CreateStudentRequest) ToDomain() *domain.Student {
return &domain.Student{
Name: req.Name,
Email: req.Email,
Phone: req.Phone,
ClassID: req.ClassID,
AcademyHubID: req.AcademyHubID,
Status: "active", // Default status
}
}
// ✅ ACADEMYHUB SPECIFIC: Domain → Response mapper function with nil-pointer safety
func ToStudentResponse(d *domain.Student) *StudentResponse {
// Nil-pointer safety check
if d == nil {
return nil
}
// Handle nested relationships safely
var className, academyhubName string
if d.Class != nil {
className = d.Class.Name
}
if d.AcademyHub != nil {
academyhubName = d.AcademyHub.Name
}
return &StudentResponse{
ID: d.ID,
Name: d.Name,
Email: d.Email,
Phone: d.Phone,
ClassID: d.ClassID,
ClassName: className,
AcademyHubID: d.AcademyHubID,
AcademyHubName: academyhubName,
Status: d.Status,
CreatedAt: d.CreatedAt.Format(time.RFC3339),
UpdatedAt: d.UpdatedAt.Format(time.RFC3339),
}
}
// Response DTO
type StudentResponse struct {
ID uint `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
Phone string `json:"phone"`
ClassID uint `json:"class_id"`
ClassName string `json:"class_name,omitempty"`
AcademyHubID uint `json:"academyhub_id"`
AcademyHubName string `json:"academyhub_name,omitempty"`
Status string `json:"status"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
}
// List Response
type StudentListResponse struct {
Data []StudentResponse `json:"data"`
Total int64 `json:"total"`
Page int `json:"page"`
PageSize int `json:"page_size"`
TotalPages int `json:"total_pages"`
}
// ✅ ACADEMYHUB SPECIFIC: Clean handler using mappers
func (h *StudentHandler) Create(c *gin.Context) {
var req dto.CreateStudentRequest
if err := c.ShouldBindJSON(&req); err != nil {
h.logger.Error("Invalid request payload", zap.Error(err))
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid request"})
return
}
// Use mapper to convert request to domain entity
student := req.ToDomain()
// Pass standard context and domain entity to service
result, err := h.service.Create(c.Request.Context(), student)
if err != nil {
h.logger.Error("Failed to create student", zap.Error(err))
if errors.Is(err, domain.ErrStudentAlreadyExists) {
c.JSON(http.StatusConflict, gin.H{"error": "Student already exists"})
return
}
c.JSON(http.StatusInternalServerError, gin.H{"error": "Internal server error"})
return
}
// Use mapper to convert domain entity to response
response := dto.ToStudentResponse(result)
c.JSON(http.StatusCreated, response)
}
// ✅ ACADEMYHUB SPECIFIC: Service receiving domain entities (not DTOs from handlers)
func (s *StudentService) Create(ctx context.Context, student *domain.Student) (*domain.Student, error) {
// Business logic validation
if student.Email == "" {
return nil, fmt.Errorf("student email is required: %w", domain.ErrInvalidInput)
}
// Repository operation
err := s.repo.Create(ctx, student)
if err != nil {
return nil, fmt.Errorf("failed to create student: %w", err)
}
return student, nil
}
type Server struct {
addr string
timeout time.Duration
logger *log.Logger
}
type Option func(*Server)
func WithTimeout(d time.Duration) Option {
return func(s *Server) {
s.timeout = d
}
}
func WithLogger(l *log.Logger) Option {
return func(s *Server) {
s.logger = l
}
}
func NewServer(addr string, opts ...Option) *Server {
s := &Server{
addr: addr,
timeout: 30 * time.Second, // default
logger: log.Default(), // default
}
for _, opt := range opts {
opt(s)
}
return s
}
// Usage
server := NewServer(":8080",
WithTimeout(60*time.Second),
WithLogger(customLogger),
)
type Logger struct {
prefix string
}
func (l *Logger) Log(msg string) {
fmt.Printf("[%s] %s\n", l.prefix, msg)
}
type Server struct {
*Logger // Embedding - Server gets Log method
addr string
}
func NewServer(addr string) *Server {
return &Server{
Logger: &Logger{prefix: "SERVER"},
addr: addr,
}
}
// Usage
s := NewServer(":8080")
s.Log("Starting...") // Calls embedded Logger.Log
}
// Bad: Grows slice multiple times
func processItems(items []Item) []Result {
var results []Result
for _, item := range items {
results = append(results, process(item))
}
return results
}
// Good: Single allocation
func processItems(items []Item) []Result {
results := make([]Result, 0, len(items))
for _, item := range items {
results = append(results, process(item))
}
return results
}
var bufferPool = sync.Pool{
New: func() interface{} {
return new(bytes.Buffer)
},
}
func ProcessRequest(data []byte) []byte {
buf := bufferPool.Get().(*bytes.Buffer)
defer func() {
buf.Reset()
bufferPool.Put(buf)
}()
buf.Write(data)
// Process...
return buf.Bytes()
}
// Bad: Creates many string allocations
func join(parts []string) string {
var result string
for _, p := range parts {
result += p + ","
}
return result
}
// Good: Single allocation with strings.Builder
func join(parts []string) string {
var sb strings.Builder
for i, p := range parts {
if i > 0 {
sb.WriteString(",")
}
sb.WriteString(p)
}
return sb.String()
}
// Best: Use standard library
func join(parts []string) string {
return strings.Join(parts, ",")
}
# Build and run
go build ./...
go run ./cmd/myapp
# Testing
go test ./...
go test -race ./...
go test -cover ./...
# Static analysis
go vet ./...
staticcheck ./...
golangci-lint run
# Module management
go mod tidy
go mod verify
# Formatting
gofmt -w .
goimports -w .
linters:
enable:
- errcheck
- gosimple
- govet
- ineffassign
- staticcheck
- unused
- gofmt
- goimports
- misspell
- unconvert
- unparam
linters-settings:
errcheck:
check-type-assertions: true
govet:
check-shadowing: true
issues:
exclude-use-default: false
| Idiom | Description |
|---|---|
| Accept interfaces, return structs | Functions accept interface params, return concrete types |
| Errors are values | Treat errors as first-class values, not exceptions |
| Don't communicate by sharing memory | Use channels for coordination between goroutines |
| Make the zero value useful | Types should work without explicit initialization |
| A little copying is better than a little dependency | Avoid unnecessary external dependencies |
| Clear is better than clever | Prioritize readability over cleverness |
| gofmt is no one's favorite but everyone's friend | Always format with gofmt/goimports |
| Return early | Handle errors first, keep happy path unindented |
// Bad: Naked returns in long functions
func process() (result int, err error) {
// ... 50 lines ...
return // What is being returned?
}
// Bad: Using panic for control flow
func GetUser(id string) *User {
user, err := db.Find(id)
if err != nil {
panic(err) // Don't do this
}
return user
}
// Bad: Passing context in struct
type Request struct {
ctx context.Context // Context should be first param
ID string
}
// Good: Context as first parameter
func ProcessRequest(ctx context.Context, id string) error {
// ...
}
// Bad: Mixing value and pointer receivers
type Counter struct{ n int }
func (c Counter) Value() int { return c.n } // Value receiver
func (c *Counter) Increment() { c.n++ } // Pointer receiver
// Pick one style and be consistent
Remember: Go code should be boring in the best way - predictable, consistent, and easy to understand. When in doubt, keep it simple.