best practice of go backend service 2

Table of Contents

Writing a backend service in Go efficiently involves following best practices for code structure, maintainability, and performance. Below is a simple and well-structured example of a RESTful backend service in Go with best practices applied.


Best Practices for Writing a Backend Service in Go

1. Project Structure

Organizing your code properly improves maintainability and scalability. A standard Go project structure looks like this:

myapp/
│── cmd/                # Main application entry points
│   ├── server/         # HTTP server main
│   │   └── main.go
│── internal/           # Business logic, database, API handlers
│   ├── handler/        # HTTP request handlers
│   ├── service/        # Business logic layer
│   ├── repository/     # Database operations
│── pkg/                # Utility packages (logging, config)
│── config/             # Configuration files
│── migrations/         # Database migrations
│── go.mod              # Go module file
│── go.sum
│── Makefile            # Automation commands
│── README.md

2. Use a Web Framework like chi

chi is lightweight and idiomatic, making it great for building Go APIs.

Install chi

go get github.com/go-chi/chi/v5

Create a Simple HTTP Server

cmd/server/main.go

package main

import (
    "fmt"
    "net/http"
    "myapp/internal/handler"
    "github.com/go-chi/chi/v5"
    "github.com/go-chi/chi/v5/middleware"
)

func main() {
    r := chi.NewRouter()

    // Middlewares
    r.Use(middleware.Logger)
    r.Use(middleware.Recoverer)

    // Routes
    r.Get("/health", handler.HealthCheck)
    r.Get("/users", handler.GetUsers)

    fmt.Println("Server running on :8080")
    http.ListenAndServe(":8080", r)
}

3. Separate Handlers, Services, and Repositories

Handler (HTTP Layer)

internal/handler/user_handler.go

package handler

import (
    "encoding/json"
    "net/http"
    "myapp/internal/service"
)

func HealthCheck(w http.ResponseWriter, r *http.Request) {
    w.WriteHeader(http.StatusOK)
    w.Write([]byte(`{"status": "ok"}`))
}

func GetUsers(w http.ResponseWriter, r *http.Request) {
    users := service.FetchUsers()
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(users)
}

Service (Business Logic Layer)

internal/service/user_service.go

package service

import "myapp/internal/repository"

func FetchUsers() []string {
    return repository.GetUsersFromDB()
}

Repository (Database Layer)

internal/repository/user_repository.go

package repository

func GetUsersFromDB() []string {
    return []string{"Alice", "Bob", "Charlie"}
}

4. Use Dependency Injection

Avoid using global variables for dependencies. Instead, pass them explicitly.

Example:

type UserService struct {
    repo repository.UserRepository
}

func NewUserService(repo repository.UserRepository) *UserService {
    return &UserService{repo: repo}
}

5. Configuration Management

Use viper to manage configuration.

Install Viper

go get github.com/spf13/viper

Load Config

package config

import (
    "log"
    "github.com/spf13/viper"
)

type Config struct {
    Port string
    DB   string
}

func LoadConfig() *Config {
    viper.SetConfigName("config")
    viper.SetConfigType("yaml")
    viper.AddConfigPath("config/")

    if err := viper.ReadInConfig(); err != nil {
        log.Fatalf("Error reading config file: %v", err)
    }

    return &Config{
        Port: viper.GetString("port"),
        DB:   viper.GetString("db"),
    }
}

6. Logging

Use logrus for structured logging.

Install Logrus

go get github.com/sirupsen/logrus

Logging Example

package logger

import (
    "github.com/sirupsen/logrus"
)

var log = logrus.New()

func InitLogger() {
    log.SetFormatter(&logrus.JSONFormatter{})
}

func Info(msg string) {
    log.Info(msg)
}

func Error(msg string, err error) {
    log.WithError(err).Error(msg)
}

7. Graceful Shutdown

Ensure the server shuts down properly on termination signals.

package main

import (
    "context"
    "net/http"
    "os"
    "os/signal"
    "syscall"
    "time"
)

func main() {
    server := &http.Server{Addr: ":8080", Handler: router}

    go func() {
        if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
            log.Fatalf("Listen: %s\n", err)
        }
    }()

    quit := make(chan os.Signal, 1)
    signal.Notify(quit, os.Interrupt, syscall.SIGTERM)

    <-quit
    log.Println("Shutting down server...")

    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
    defer cancel()

    if err := server.Shutdown(ctx); err != nil {
        log.Fatal("Server forced to shutdown:", err)
    }

    log.Println("Server exiting")
}

8. Unit Testing

Use httptest to test handlers.

Install Testify

go get github.com/stretchr/testify

Write a Test

internal/handler/user_handler_test.go

package handler

import (
    "net/http"
    "net/http/httptest"
    "testing"

    "github.com/go-chi/chi/v5"
    "github.com/stretchr/testify/assert"
)

func TestHealthCheck(t *testing.T) {
    req, _ := http.NewRequest("GET", "/health", nil)
    res := httptest.NewRecorder()

    handler := http.HandlerFunc(HealthCheck)
    handler.ServeHTTP(res, req)

    assert.Equal(t, http.StatusOK, res.Code)
}

9. Dockerizing the Service

Create a Dockerfile:

FROM golang:1.20
WORKDIR /app
COPY . .
RUN go mod tidy && go build -o myapp ./cmd/server/main.go
CMD ["./myapp"]
EXPOSE 8080

10. Automate with Makefile

run:
    go run cmd/server/main.go

test:
    go test ./... -v

build:
    go build -o myapp ./cmd/server/main.go

docker-build:
    docker build -t myapp .

Final Thoughts

These best practices ensure:
✅ Clean architecture
✅ Maintainability
✅ Scalability
✅ Performance optimization

Would you like a more advanced example, such as integrating PostgreSQL, Redis, or gRPC? 🚀

Comments |0|

Legend *) Required fields are marked
**) You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>
Category: golang