best practice of go backend service 2
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? 🚀