Go Coding Standards

General Go Coding Standards

File Naming:

Use lowercase letters and underscores. For example, user_handler.go.

Package Naming:

Use short, concise names in lowercase. For example, handlers, models.

Function and Variable Naming:

Use camelCase for local variables and function parameters.

main.go
// Correct
var userName string
var userID int
 
// Incorrect
var username string
var UserID int

Use PascalCase for exported functions and types:

Use concise and descriptive names. Avoid single-character names except for common idiomatic uses (e.g., i for loop counters).

main.go
// Correct
func GetUserByID(id int) User{}
type UserService struct{}
 
// Incorrect
func getU(id int) User{}
type us struct{}

Constants:

Use PascalCase for exported constants and camelCase for unexported constants. For example, const Pi = 3.14 and const maxSize = 1024.

main.go
// Correct for exported constants
const MaxRetries = 3
const DefaultTimeout = 30 * time.Second
 
// Correct for unexported constants
const maxRetries = 3  
const defaultTimeout = 30 * time.Second
 

Error Handling:

Always check for and handle errors. Do not ignore them. Log errors with logrus, otherwise if it is user facing, return an error response.

main.go
// Correct
if err != nil {
    logrus.Errorf("error: %v", err)
    return
}
 
// Also correct(using gin web framework)
if err != nil {
    c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
    return
}

Comments:

Use comments to explain the intent of the code, not the implementation. Use // for single-line comments and documentation of functions and /* ... */ for block comments. Provide comments for all exported functions, types, and constants.

main.go
// GetUserByID fetches a user by ID from the database.
func GetUserByID(id int) User{}
 
// User represents a user in the system.
type User struct{}

API Specific Best Practices

Project Structure:

Organize code into packages based on functionality, such as handlers, services, models, middlewares, etc. Separate business logic from HTTP handlers. Use service layers to handle business logic. Have a look at our backends organization structure below:

myapp/
├── cmd/
   └── occupi-backend/
       └── main.go //entry point
├── configs/
   └── config.go //env variables are imported in this file
├── pkg/
   ├── handlers/
      └── handlers.go //handles individual api endpoints
   ├── middleware/
      ├── middleware.go //a logger for each incoming api request
      └── auth.go //contains authentication middleware
   ├── models/
      └── models.go //contains database defined models(NOSQL)
   ├── database/
      ├── database.go //contains database struct
      └── migrations/
          └── //may perhaps contain database migrations
   ├── router/
      └── router.go //contains api endpoints
   └── utils/
       └── utils.go //contains universally used functions
├── tests/
   └── handlers_test.go //unit tests endpoints
|   └── unit_test.go //unit tests utils
├── Dockerfile // Containerization
├── docker-compose.yml
├── go.mod
├── go.sum
├── nginx.conf //reverse proxy config
└── docs/
    └── // go-lang function documentation

Security conscience

Global Variables:

Avoid using global mutable variables. Use dependency injection to pass dependencies to functions and methods.

main.go
// Correct
func NewServer(db *database.DB) *Server {
    return &Server{db: db}
}
 
// Incorrect
var db *database.DB //this may cause issues with concurrent requests
 
func NewServer() *Server {
    return &Server{db: db}
}

Configuration Management:

Use environment variables or configuration files for application configuration and storing secrets.

main.go
func GetPort() string {
	port := os.Getenv("PORT")
	if port == "" {
		port = "PORT"
	}
	return port
}

and .env file

PORT=8080

Router Setup:

Group routes logically and use middlewares for common functionalities like logging, authentication, and error handling.

main.go
func OccupiRouter(r *gin.Engine, db *mongo.Client) {
	ping := r.Group("/ping")
	{
		ping.GET("", func(c *gin.Context) { c.JSON(200, gin.H{"message": "pong -> I am alive and kicking"}) })
	}
	api := r.Group("/api")
	{
		api.GET("/resource", func(c *gin.Context) { handlers.FetchResource(c, db) })
	}
	auth := r.Group("/auth")
	{
		//auth.POST("/login", handlers.Login(db))
		auth.POST("/register", handlers.Register)
		auth.POST("/verify-otp", handlers.VerifyOTP)
	}
}

Response and Error Handling:

Standardize API responses using a consistent format. For example, always include status codes, messages, and data.Gin provides us with a .H object that will allow you to format responses.

main.go
// Correct
c.JSON(http.StatusOK, gin.H{"status": http.StatusOK, "message": "Resource fetched successfully", "data": resource})
 
// Incorrect
c.JSON(http.StatusOK, resource)

A typical success API response should have the following structure:

{
    "status": 200,
    "message": "Resource fetched successfully",
    "data": {
        // Your data here
    },
    "meta": {
        // Optional metadata here
    }
}

A typical error API response should have the following structure:

{
    "status": 400,
    "message": "Bad Request",
    "error": {
        "code": "INVALID_INPUT",
        "message": "The input provided is invalid",
        "details": {
            // Additional error details here
        }
    }
}

A typical success paginated API response should have the following structure:

{
    "status": 200,
    "message": "Request successful",
    "data": [
        // List of items here
    ],
    "meta": {
        "currentPage": 1,
        "totalPages": 10,
        "pageSize": 20,
        "totalCount": 200
    }
}
 

Testing:

Write unit tests for individual functions and methods. example:

main.go
func TestAdd(t *testing.T) {
    result := Add(1, 2)
    if result != 3 {
        t.Errorf("Add(1, 2) = %d; want 3", result)
    }
}

Write integration tests for API endpoints. example:

main.go
func TestGetResource(t *testing.T) {
    req, err := http.NewRequest("GET", "/api/resource", nil)
    if err != nil {
        t.Fatal(err)
    }
 
    rr := httptest.NewRecorder()
    handler := http.HandlerFunc(handlers.GetResource)
 
    handler.ServeHTTP(rr, req)
 
    if status := rr.Code; status != http.StatusOK {
        t.Errorf("handler returned wrong status code: got %v want %v", status, http.StatusOK)
    }
 
    expected := `{"message":"Hello, World!"}`
    if rr.Body.String() != expected {
        t.Errorf("handler returned unexpected body: got %v want %v", rr.Body.String(), expected)
    }
}

NB: Use testing frameworks like testing package and libraries like testify for assertions.