Idiomatic Go Guidelines

A guide to writing clean and idiomatic Go code

GitHub

Menu

Introduction

This document outlines the coding conventions and practices for the project, specifically tailored for golang development. Adhering to these standards ensures code consistency, readability, and maintainability across the project.


General Coding Standards

Naming Conventions

Variables and Functions

Use camelCase for variable.

Structs

Use PascalCase for struct names.

Good Example:

type User struct {
    ID   int
    Name string
}

users := []User{
    {ID: 1, Name: "Alice"},
    {ID: 2, Name: "Bob"},
}

var userCount int

func GetUserByID(userID int) {
    // Implementation
}

// private
func getUserByID(userID int) {
    // Implementation
}

Bad Example:

type UserStruct struct {
    UserID   int
    UserName string
}

userSlice := []UserStruct{
    {UserID: 1, UserName: "Alice"},
    {UserID: 2, UserName: "Bob"},
}

var user_count int

func getUserByID(userID int) {
    // Implementation
}

// private
func GetUserByID(userID int) {
    // Implementation
}

Interfaces

Good Example:

type IUserService interface {
    GetUserByID(id int) (*User, error)
}

Bad Example:

type UserService interface {
    GetUserByID(id int) (*User, error)
}

Constants

Use MixedCaps for constants.

Good Example:

const MaxLength = 10
const minPasswordLength = 8

Bad Example:

const MAX_LENGTH = 10
const MIN_PASSWORD_LENGTH = 8

Packages

Go package names should be short and contain only lowercase letters. A package name composed of multiple words should be left unbroken in all lowercase. For example, the package tabwriter is not named tabWriter, TabWriter, or tab_writer. Additionally the packages must be self-explanative and should not be named common, utils, helpers, etc.

To avoid the code duplication, functions inside the packages must not be named the same as the package name.

Good Example:

package user

func GetByID(id int) (*User, error) {
    // Implementation
}
package main

import "github.com/username/project/user"

func main() {
    user.GetByID(1)
}

Bad Example:

package user

func GetUserByID(id int) (*User, error) {
    // Implementation
}
package main

import "github.com/username/project/user"

func main() {
    user.GetUserByID(1)
}

One-letter variable names:

Avoid using one-letter variable names except in cases like loop indices or maths (i, j, k).

Good Example:

func calculateArea(length, width int) int {
    return length * width
}

Bad Example:

func calculateArea(l, w int) int {
    return l * w
}

Code Structure

File Naming

Use descriptive names, and separate words with underscores (e.g., user.go, user_validations.go).

Folder Structure

Organize code into meaningful packages and folders (e.g., controller/, service/, repository/, model/, pkg/)

Test Files

Name test files with _test.go suffix (e.g., user_test.go).

Good Example:

project/
    ├── controller/
    │   ├── user.go
    |   ├── user_test.go
    │   └── ...
    ├── service/
    │   ├── user.go
    |   ├── user_test.go
    │   └── ...
    ├── repository/
    │   ├── user.go
    |   ├── user_test.go
    │   └── ...
    |── model/
    |   ├── user.go
    └── main.go
project/
    ├── controllers/
    │   ├── controller_user.go
    |   ├── controller_userTest.go
    │   └── ...
    ├── services/
    │   ├── userService.go
    |   ├── userService_test.go
    │   └── ...
    ├── repositories/
    │   ├── userRepository.go
    |   ├── userRepositorytest.go
    │   └── ...
    |── models/
    |   ├── user_model.go
    └── main.go

Formatting

Newlines

Ensure there is a newline after } when there is a return or new var and before var or any other line following a closing brace.

Also, include a newline after each case in a switch statement.

Good Example:

func example() {
    if condition {
        return
    }

    var x int
    x = 10
    if x > 5 {
        fmt.Println("x is greater than 5")
    }

    switch x {
    case 1:
        fmt.Println("x is 1")

    case 2:
        fmt.Println("x is 2")

    default:
        fmt.Println("x is neither 1 nor 2")
    }

    return x
}

Bad Example:

func example() {
    if condition {
        return
    }
    var x int
    x = 10
    if x > 5 {
        fmt.Println("x is greater than 5")
    }
    switch x {
    case 1:
        fmt.Println("x is 1")
    case 2:
        fmt.Println("x is 2")
    default:
        fmt.Println("x is neither 1 nor 2")
    }
    return x
}

Commenting

Add comments to explain complex logic or non-obvious code.


Error Handling

Error return

Prefer returning errors explicitly instead of using panic.

Good Example:

func getUserByID(userID int) (*User, error) {
    user, err := userRepository.FindByID(userID)
    if err != nil {
        return nil, fmt.Errorf("finding user by ID failed: %v", err)
    }

    return user, nil
}

Bad Example:

func getUserByID(userID int) *User {
    user, err := userRepository.FindByID(userID)
    if err != nil {
        panic(err)
    }

    return user
}

Error Messages

Provide meaningful error messages when returning errors.

Good Example:

func getUserByID(userID int) (*User, error) {
    user, err := userRepository.FindByID(userID)
    if err != nil {
        return nil, fmt.Errorf("finding user by ID failed: %v", err)
    }

    return user, nil
}

Bad Example:

func getUserByID(userID int) (*User, error) {
    user, err := userRepository.FindByID(userID)
    if err != nil {
        return nil, err
    }

    return user, nil
}

Error shorthand

Use the err shorthand for error variables.

Good Example:

if err := someFunction(); err != nil {
    return err
}

Bad Example:

err := someFunction()
if err != nil {
    return err
}

Enumerations

Enums should be defined within the model package with their respective constants.

Good Example:

package enum

type Coverage string

const (
	CoverageMostSegments      Coverage = "MOST_SEGMENTS"
	CoverageAtLeastOneSegment Coverage = "AT_LEAST_ONE_SEGMENT"
	CoverageAllSegments       Coverage = "ALL_SEGMENTS"
)

var Coverages = []string{
	string(CoverageMostSegments),
	string(CoverageAtLeastOneSegment),
	string(CoverageAllSegments),
}

Bad Example:

package enum

const (
    MostSegments      = "MOST_SEGMENTS"
    AtLeastOneSegment = "AT_LEAST_ONE_SEGMENT"
    AllSegments       = "ALL_SEGMENTS"
)

var Coverages = []string{
    MostSegments,
    AtLeastOneSegment,
    AllSegments,
}

Common Code Smells

Type any

Avoid using any or empty interfaces as a type, unless it’s absolutely necessary. any types can be used in case of formatting or marshalling, but should be avoided in other cases.

In bad example we can see, that the type safety which is propagated by Go is lost. The function can accept any type, and the code inside the function must use type assertions to determine the actual type of the argument.

Good Example:

func encodeToJSON(data any) ([]byte, error) {
    return json.Marshal(data)
}

type User struct {
    ID   int    `json:"id"`
    Name string `json:"name"`
}

func main() {
    user := User{ID: 1, Name: "John"}
    jsonData, err := encodeToJSON(user)
    if err != nil {
        log.Fatalf("Failed to encode: %v", err)
    }
    fmt.Println(string(jsonData))
}

Bad Example:

func processData(data any) {
    // Process data with type assertions
    switch v := data.(type) {
    case string:
        fmt.Println("String:", v)
    case int:
        fmt.Println("Int:", v)
    default:
        fmt.Println("Unknown type")
    }
}

func main() {
    processData("hello")
    processData(42)
    processData(true)
}

Variable Shadowing

Try to avoid variable shadowing. If check is needed for the outcome of variable, it’s better to either assign a value for the variable or create new variable within the check block.

Good Example:

func processUser(user User) error {
    // Outer scope variable
    id := user.ID

    if user.IsActive {
        // Update existing variable instead of shadowing
        // This clearly shows we're intentionally replacing the original ID value
        id = getUserRoleID(user)
        log.Printf("Processing active user with role ID: %d", id)
    }

    // The saveUserActivity function receives the ID that was potentially modified
    // If user was active, this is now the role ID, otherwise it's the original user ID
    return saveUserActivity(id)
}

Bad Example:

func processUser(user User) error {
    // Outer scope variable
    id := user.ID
    
    if user.IsActive {
        // Inner scope shadows outer 'id' variable
        id := getUserRoleID(user)
        // Here 'id' refers to the role ID, not the user ID
        log.Printf("Processing active user with role ID: %d", id)
        
        // The outer 'id' is completely inaccessible here
    }
    
    // Here 'id' is the user ID again
    return saveUserActivity(id)
}

Less else statements

Avoid else statements in if condition to avoid the complexity of the code. Use KISS principle when writing condition checks.

Simple Example

Good Example

func GetStatus(user *User) bool {
    if user.IsActive {
        return true
    }

    return false
}

*Bad Example

func GetStatus(user *User) bool {
    if user.IsActive {
        return true
    } else {
        return false
    }
}

Complex Example

Good Example

func join(s1,s2 string, max int) (string, error) {
    if s1 == "" {
        return "", errors.New("s1 is empty")
    }

    if s2 == "" {
        return "", errors.New("s2 is empty")
    }

    concat, err := concatenate(s1,s2)
    if err != nil {
        return "", err
    }

    if len(concat) > max {
        return concat[:max], nil
    }

    return concat, nil
}

Bad Example

func join(s1,s2 string, max int) (string, error) {
    if s1 == "" {
        return "", errors.New("s1 is empty")
    } else {
        if s2 == "" {
            return "", errors.New("s2 is empty")
        } else {
            concat, err := concatenate(s1, s2)
            if err != nil {
                return "", err
            } else {
                if len(concat) > max {
                    return concat[:max], nil
                } else {
                    return concat, nil
                }
            }
        }
    }
}

Getters & Setters

Even though in Golang Getters and Setters are not a common practice, they are used in some cases.

Getters never use Get prefix due to:

While Setters use Set prefix due to:

Good Example

package model

type User struct {
    name    string
    address string
}

// Getter - no "Get" prefix
func (u *User) Name() string {
    return u.name
}

func (u *User) Address() string {
    return u.address
}

// Setter - uses "Set" prefix
func (u *User) SetName(name string) {
    u.name = name
}

func (u *User) SetAddress(address string) {
    u.address = address
}

package main

import (
    "fmt"
    "github.com/username/project/model"
)

func main() {
    user := &model.User{}
    user.SetName("Alice")
    fmt.Println(user.Name())
    fmt.Println(user.Address())
}

Bad Example

package model

type Customer struct {
    Name    string
    Address string
}

// Bad: Using "Get" prefix for getters
func (c *Customer) GetName() string {
    return c.Name
}

func (c *Customer) GetAddress() string {
    return c.Address
}

// Bad: Inconsistent naming
func (c *Customer) UpdateName(name string) {
    c.Name = name
}

func (c *Customer) ChangeAddress(address string) { 
    c.Address = address
}
func main() {
    customer := &Customer{}
    customer.UpdateName("Bob")
    fmt.Println(customer.GetName())
    fmt.Println(customer.GetAddress())
}

Interfaces

Interfaces must be on consumers side

Interfaces should use the consumer side of the code. This means that the interface should be defined in the package where it is needed to use, not in the package where it is implemented. There are some exceptions, but in those exceptions developer MUST be sure, that it will be needed for other packages (ex: context.Context interface from context package)

Good Example:

package service

import "github.com/username/project/model"

// UserRepository interface is defined in the consumer package
type UserRepository interface {
    FindByID(id int) (*model.User, error)
    Create(user *model.User) error
    Update(user *model.User) error
}

// UserService depends on the interface, not the concrete implementation
type UserService struct {
    repo UserRepository
}

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

func (s *UserService) GetUserByID(id int) (*model.User, error) {
    return s.repo.FindByID(id)
}

Bad Example:

// In provider package (e.g., repository/user_repository.go)
package repository

import "github.com/username/project/model"

// Interface defined in the provider package - bad practice
type UserRepository interface {
    FindByID(id int) (*model.User, error)
    Create(user *model.User) error
    Update(user *model.User) error
}

// Implementation in the same package as the interface
type PostgresUserRepository struct {
    db *sql.DB
}

func (r *PostgresUserRepository) FindByID(id int) (*model.User, error) {
    // Implementation
    return &model.User{}, nil
}

// In service/user_service.go - now dependent on repository package for the interface
package service

import "github.com/username/project/repository"

type UserService struct {
    repo repository.UserRepository
}

Pollution

Interface pollution occurs when an interface has too many methods or is too broad, making it difficult to implement and understand. This can lead to confusion and make it harder to maintain the codebase.

The good example follows the Interface Segregation Principle by:

The bad example shows interface pollution by:

Good Example:

// Small, focused interfaces that are easy to implement
type Reader interface {
    Read(p []byte) (n int, err error)
}

type Writer interface {
    Write(p []byte) (n int, err error)
}

// Compose interfaces when needed
type ReadWriter interface {
    Reader
    Writer
}

// Implementation only needs to implement what it uses
type FileHandler struct{}

func (f *FileHandler) Read(p []byte) (n int, err error) {
    // Read implementation
    return len(p), nil
}

Bad Example:

// Interface pollution - too many methods in one interface
type FileSystem interface {
    Read(p []byte) (n int, err error)
    Write(p []byte) (n int, err error)
    Create(name string) error
    Delete(name string) error
    Rename(oldpath, newpath string) error
    Chmod(mode os.FileMode) error
    Chown(uid, gid int) error
    Stat() (os.FileInfo, error)
    Close() error
}

// Implementation forced to implement all methods
type SimpleFileHandler struct{}

func (f *SimpleFileHandler) Read(p []byte) (n int, err error) {
    return len(p), nil
}

// Must implement all other methods even if not needed
func (f *SimpleFileHandler) Write(p []byte) (n int, err error) {
    return 0, errors.New("not implemented")
}

func (f *SimpleFileHandler) Create(name string) error {
    return errors.New("not implemented")
}

// ... and so on for all other methods

Generics

In Golang generics were created to allow developers to write more flexible and reusable code. However, they can also lead to complexity and confusion if not used properly. To avoid this, it’s important to use generics judiciously and only when necessary.

Keep KISS principle in mind when using generics. If a function or type can be implemented without generics, it’s often better to do so. This keeps the code simpler and easier to understand.

Good Example:

// Custom constraint
type number interface {
    // Either int or float64 can be used
    int | float64
}

// Generic function that works with any type that satisfies the Number constraint
func universalAdd[T number](a, b T) T {
    return a + b
}

func main() {
    // Works
    fmt.Println(universalAdd[int](1, 2))
    fmt.Println(universalAdd[float64](1.5, 2.3))

    // Will not compile and will give error
    fmt.Println(universalAdd[string]("Hello", "World"))
}

Bad Example:

func universalAdd[T any](a, b T) string {
    // Convert everything to string and concatenate, then try to parse back
    aStr := fmt.Sprintf("%v", a)
    bStr := fmt.Sprintf("%v", b)
    
    // Try to handle different types with type assertions and string parsing
    if aInt, err1 := strconv.Atoi(aStr); err1 == nil {
        if bInt, err2 := strconv.Atoi(bStr); err2 == nil {
            return fmt.Sprintf("%v", aInt + bInt)
        }
    }
    
    if aFloat, err1 := strconv.ParseFloat(aStr, 64); err1 == nil {
        if bFloat, err2 := strconv.ParseFloat(bStr, 64); err2 == nil {
            return fmt.Sprintf("%v", aFloat + bFloat)
        }
    }
    
    // Fall back to string concatenation for everything else
    return aStr + bStr
}

func main() {
    // Works but inefficient and error-prone
    fmt.Println(universalAdd(1, 2))        // "3"
    fmt.Println(universalAdd(1.5, 2.3))    // "3.8"
    
    // Leads to unexpected results with string concatenation
    fmt.Println(universalAdd("Hello", "World"))  // "HelloWorld"
    
    // And completely breaks type safety
    fmt.Println(universalAdd(true, false))  // "truefalse"
    fmt.Println(universalAdd([]int{1}, []int{2}))  // "[1][2]" or runtime error
}

Embed Types

Go’s struct embedding allows one struct to include another struct’s fields and methods directly, enabling composition over inheritance. When used correctly, embedding creates cleaner code with better encapsulation.

Good Example:

// Base types with focused responsibilities
type logger struct{}

func (l logger) Log(message string) {
    fmt.Println("LOG:", message)
}

type authenticator struct{}

func (a authenticator) Authenticate(token string) bool {
    // Authentication logic
    return token != ""
}

// UserService composes functionality through embedding
type userService struct {
    // Embedded types
    logger
    authenticator
    
    // Service-specific fields
    users []user
}

func (s *userService) CreateUser(name string, token string) error {
    // Uses embedded Authenticator's method
    if !s.Authenticate(token) {
        // Uses embedded Logger's method
        s.Log("Authentication failed")
        return errors.New("unauthorized")
    }
    
    // Service-specific logic
    s.users = append(s.users, user{Name: name})
    s.Log("User created")
    return nil
}

Bad Example:

// Overloaded type containing too many concerns
type userManager struct {
    users []user
}

// Adding logging functionality through embedding but with name conflicts
type loggingUserManager struct {
    userManager
    logger
    
    // Duplicate field that shadows the embedded one
    users map[string]user
}

func (l *loggingUserManager) CreateUser(name string) {
    // Ambiguous - which users collection are we using?
    user := user{Name: name}
    
    // Awkward workaround for field collision
    l.UserManager.users = append(l.UserManager.users, user)
    l.users[name] = user // Using the shadowed field
    
    // Method name collision if Logger also has a CreateUser method
    // Would need explicit qualification: l.Logger.CreateUser()
}

// Multiple layers of embedding make it unclear where methods come from
type adminManager struct {
    loggingUserManager
    authService
    configManager
    // More embedded types...
}

func (a *adminManager) DoSomething() {
    // Where is each method coming from? Hard to tell at a glance
    a.Log("Starting operation")
    a.Authenticate()
    a.LoadConfig()
    a.CreateUser("admin")
}

Software Design

There are many software design principles available (e.g Builder, Factory, Singleton). Kindly use this website to check all the patterns and find the one that fits your needs.

SOLID

At Wisepace we’re using SOLID development principles to ensure the maintainability of the code.

1. Single Responsibility Principle (SRP)

Each module or class should have one, and only one, reason to change.

In Go, this means that a function or struct should only do one thing.

Bad:

type Report struct {
    Title string
    Data  []string
}

func (r *Report) Generate() string {
    return "Report Title: " + r.Title
}

func (r *Report) SaveToFile(filename string) error {
    // Saving logic here...
    return nil
}

Here, the Report struct handles both generating the report and saving it to a file. These are two separate responsibilities.

Good:

type Report struct {
    Title string
    Data  []string
}

func (r *Report) Generate() string {
    return "Report Title: " + r.Title
}

type FileSaver struct {}

func (fs *FileSaver) SaveToFile(filename string, content string) error {
    // Saving logic here...
    return nil
}

Now, Report is responsible for generating reports, while FileSaver is responsible for file-saving logic.

2. Open/Closed Principle (OCP)

Software entities should be open for extension but closed for modification.

In Go, this means you should design your code to allow new functionality to be added without changing existing code.

Bad:

type PaymentProcessor struct {}

func (p *PaymentProcessor) Process(paymentType string) {
    if paymentType == "credit" {
        // Process credit payment
    } else if paymentType == "paypal" {
        // Process PayPal payment
    }
}

Adding a new payment type requires modifying Process.

Good:

type PaymentMethod interface {
    Pay()
}

type CreditPayment struct {}

func (c *CreditPayment) Pay() {
    // Process credit payment
}

type PayPalPayment struct {}

func (p *PayPalPayment) Pay() {
    // Process PayPal payment
}

type PaymentProcessor struct {}

func (p *PaymentProcessor) Process(method PaymentMethod) {
    method.Pay()
}

Now, to add a new payment type, you just implement the PaymentMethod interface.

3. Liskov Substitution Principle (LSP)

Subtypes must be substitutable for their base types without altering the correctness of the program.

In Go, this means you should ensure that implementations of an interface adhere to its contract.

Bad:

type Bird interface {
    Fly()
}

type Sparrow struct {}

func (s *Sparrow) Fly() {
    // Sparrow flies
}

type Ostrich struct {}

func (o *Ostrich) Fly() {
    panic("Ostriches can't fly!")
}

Here, Ostrich violates the principle because it doesn’t conform to the expected behavior of Bird.

Good:

type Bird interface {
    Move()
}

type Sparrow struct {}

func (s *Sparrow) Move() {
    fmt.Println("Sparrow flies!")
}

type Ostrich struct {}

func (o *Ostrich) Move() {
    fmt.Println("Ostrich runs!")
}

Now both Sparrow and Ostrich adhere to the behavior defined by the Bird interface.

4. Interface Segregation Principle (ISP)

Clients should not be forced to depend on methods they do not use.

In Go, this means creating smaller, more focused interfaces.

Bad:

type Printer interface {
    Print()
    Scan()
    Fax()
}

type BasicPrinter struct {}

func (p *BasicPrinter) Print() {
    fmt.Println("Printing...")
}

func (p *BasicPrinter) Scan() {
    panic("Scan not supported!")
}

func (p *BasicPrinter) Fax() {
    panic("Fax not supported!")
}

A BasicPrinter doesn’t need to implement Scan or Fax.

Good:

type Printer interface {
    Print()
}

type Scanner interface {
    Scan()
}

type Faxer interface {
    Fax()
}

type BasicPrinter struct {}

func (p *BasicPrinter) Print() {
    fmt.Println("Printing...")
}

Now BasicPrinter only implements what it needs.

5. Dependency Inversion Principle (DIP)

Depend on abstractions, not concretions.

In Go, this means your code should depend on interfaces, not specific implementations.

Bad:

type Database struct {}

func (d *Database) Save(data string) {
    fmt.Println("Saving data:", data)
}

type UserService struct {
    DB Database
}

func (us *UserService) StoreUser(name string) {
    us.DB.Save(name)
}

Here, UserService is tightly coupled to Database.

Good:

type Storage interface {
    Save(data string)
}

type Database struct {}

func (d *Database) Save(data string) {
    fmt.Println("Saving data:", data)
}

type UserService struct {
    Storage Storage
}

func (us *UserService) StoreUser(name string) {
    us.Storage.Save(name)
}

Now, UserService depends on the Storage interface, so you can easily replace Database with another implementation.

Recap

SRP: One responsibility per module. OCP: Add new features without modifying existing code. LSP: Subtypes should behave as their base type expects. ISP: Create focused interfaces. DIP: Depend on interfaces, not implementations. Using these principles, your Go code will become easier to maintain, extend, and understand!

Code Review

Code reviews are main part of software delivery process, where product development team is responsible for the changes reaching the production.

Code Review Checklist

Add code review checklist, where developers can ensure, they have followed all the best practices.

Example checklist:

- [ ] My code follows the style guidelines of this project
- [ ] I have performed a self-review of my code
- [ ] I have commented my code, particularly in hard-to-understand areas
- [ ] I have made corresponding changes to the documentation
- [ ] My changes generate no new warnings
- [ ] I've performed/added tests/lints that prove my fix is effective or that my feature works

4-Eye Principle

All code changes must be reviewed at least by two team members before merging into the main branch. If no resources are available, the code can be reviewed by one senior team member or mentor.

Team members must test the code locally to ensure that it works as expected. If not possible, the reviewer should ask the author to provide a demo or test environment, where the feature can be tested.

Draft PR

Before creating a PR and assigning to team member, create a draft PR and check that code follows the guidelines mentioned on this page.

Commit Messages

Commit messages should be clear and descriptive.

Use conventional commits to maintain consistency across the project if any other process is not used.

Good Example:

feature: add user registration endpoint

Bad Example:

Added new feature

Observability

Logging

Logger Middleware

Logger Middleware is an important part of the application to log incoming and outgoing requests and responses.

Logger middleware must have the following parameters:

These parameters are going under MUST category because they are essential for debugging and monitoring the application.

Logger middleware should have the following parameters:

Add a logger middleware to log incoming requests and responses.

Log Levels

Log levels must be used to categorize log messages based on their severity. The following log levels are recommended:

Logging types

Error types will allow us to standardise and write more self-descriptive logs. We can easily determine on which side of our application we have the issue. Below are the key logging types and why they should be used.

Good Example:

_, err := db.Exec("DROP TABLE users")
if err != nil {
log.Printf("DatabaseError: Permission denied %v", err)
}
svc := sqs.New(session.Must(session.NewSession()))
_, err := svc.GetQueueUrl(&sqs.GetQueueUrlInput{
QueueName: aws.String("my-queue"),
})
if err != nil {
log.Printf("QueueError: Failed to retrieve the URL for queue 'my-queue' %v", err)
}
func validateEmail(email string) {
if !isValidEmail(email) {
log.Printf("ValidationError: Invalid email format: %s", email)
}
func main() {
err := startServer()
if err != nil {
log.Fatalf("ServerError: Failed to start the server %v", err)
}
func isValidEmail(email string) bool {
re := regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`)
return re.MatchString(email)
}

func validateEmail(email string) {
if !isValidEmail(email) {
log.Printf("ClientError: Invalid email format received from client: %s", email)
}
var err error
DB, err = database.NewDBConnection()
if err != nil {
log.Printf("InternalCommunicationError: Couldn't connect to database %v", err)
}

Bad Example:

func isValidEmail(email string) bool {
regex := regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`)
return regex.MatchString(email)
}

func validateEmail(email string) {
if !isValidEmail(email) {
log.Printf(err)
}

Conclusion

By following these coding conventions and best practices, we aim to maintain a high standard of code quality, readability, and maintainability across the project.

Consistency in coding style and structure will help streamline development and collaboration among team members.

For any questions or suggestions regarding these practices, please reach out to the team.

References