adnenre
#Golange

Go Language convention – best practices

This guide reflects the collective best practices and conventions of the Go community, as documented in Effective Go, the standard library, code review comments, and years of idiomatic Go code

#🧠 Introduction

If you’re coming to Go from another language, you’ll quickly notice that the community cares deeply about simplicity, readability, and uniformity. There’s no “one true style” debate – the language itself (via gofmt) and its idioms make most decisions for you.

This guide collects all essential idiomatic Go conventions in one place. It’s based on the Go standard library, the Effective Go document, code review comments from the core team, and years of real-world experience.

Whether you’re a beginner looking to write “Go-like” code, or an experienced developer preparing for a code review, this document will serve as your reference.

Let’s dive in.


#1. Case and Visibility (Exported vs Private)

The case of the first letter determines visibility.

package user

// EXPORTED (visible from other packages)
type User struct {
    Name string // Exported (uppercase)
    age  int    // Private (lowercase)
}

// EXPORTED
func NewUser(name string) *User {
    return &User{Name: name, age: 0}
}

// PRIVATE (only visible within the user package)
func normalizeName(name string) string {
    return strings.TrimSpace(name)
}
CaseVisibilityAccessible From
UserExported (public)Any package
userPrivateSame package only

#2. Project Structure (Standard Go Layout)

Conventional structure for a Go project.

myproject/
├── cmd/                    # Executables
│   ├── api/                # Command: myproject-api
│   │   └── main.go
│   └── worker/             # Command: myproject-worker
│       └── main.go
├── internal/               # Private code (not importable)
│   ├── auth/
│   └── storage/
├── pkg/                    # Public code (importable)
│   └── utils/
├── api/                    # OpenAPI/Protobuf contracts
│   └── openapi.yaml
├── web/                    # Web assets (HTML, CSS, JS)
│   ├── templates/
│   └── static/
├── scripts/                # Build/installation scripts
├── test/                   # Additional external tests
├── go.mod
└── main.go                 # For small projects

#3. Package Names

Short, lowercase, no underscores, singular.

package user      // ✅ GOOD
package users     // ⚠️ AVOID (prefer singular)
package userUtil  // ❌ BAD (too long)
package userutils // ❌ BAD
package my_package // ❌ BAD (no underscores)

Additional rule: Avoid util, common, misc, global as package names.


#4. Acronyms and Abbreviations

Acronyms keep their natural case.

// EXPORTED
type UserID struct{}    // ✅ ID (not Id)
type APIClient struct{} // ✅ API (not Api)
type HTTPRequest struct{} // ✅ HTTP (not Http)
type JSONParser struct{}  // ✅ JSON (not Json)
type URLBuilder struct{}  // ✅ URL (not Url)

// PRIVATE
var urlParser *parser   // ✅ url (not URLParser)
var userID int64        // ✅ userID (not userId)
var apiKey string       // ✅ apiKey (not ApiKey)
var httpClient *Client  // ✅ httpClient (not HTTPClient if private)

Special cases:

  • XMLXml only if truly a compound word (XmlParser is acceptable but XMLParser is preferred)
  • IPv4 → keeps its special case

#5. Method Receivers

1-2 letters, identical across all methods of the same type.

// ✅ GOOD: short, consistent receiver
type User struct {
    FirstName string
    LastName  string
}

func (u *User) FullName() string {
    return u.FirstName + " " + u.LastName
}

func (u *User) SetName(first, last string) {
    u.FirstName = first
    u.LastName = last
}

func (u *User) Validate() error {
    if u.FirstName == "" {
        return errors.New("first name required")
    }
    return nil
}

// ✅ GOOD: value receiver (not pointer)
type Point struct {
    X, Y int
}

func (p Point) Distance() float64 {
    return math.Sqrt(float64(p.X*p.X + p.Y*p.Y))
}

// ❌ BAD
func (this *User) FullName() string { ... }  // "this" is not idiomatic
func (user *User) FullName() string { ... }  // too long
func (u *User) GetName() string { ... }      // "Get" is redundant (prefer Name())

Rule: Avoid Get in getter method names. Prefer Name() over GetName().


#6. Single-Method Interfaces (-er suffix)

Interfaces with a single method often use the -er suffix.

// Idiomatic interfaces from the standard library
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
}

type Stringer interface {
    String() string
}

// Custom examples
type Greeter interface {
    Greet() string
}

type Validator interface {
    Validate() error
}

type Parser interface {
    Parse(data []byte) (interface{}, error)
}

Rule: If an interface has multiple methods, avoid the -er suffix. Use a descriptive name.


#7. Boolean Variables

Start with Is, Has, Can, Should, Allow.

var isReady bool
var hasPermission bool
var canEdit bool
var shouldRetry bool
var allowOverride bool
var exists bool      // ✅ acceptable for existence check

// Natural usage
if isReady && hasPermission {
    // ...
}

// ⚠️ AVOID
var ready bool       // too vague
var flag bool        // too vague

#8. Common Idiomatic Variables

ctx, cancel := context.WithTimeout(context.Background(), time.Second) // ctx

result, err := doSomething() // err

for i, item := range items { // i, item
    // ...
}

for k, v := range myMap { // k, v
    // ...
}

for _, v := range items { // _ (blank identifier) when key is unused
    // ...
}

var buf bytes.Buffer // buf

func handler(w http.ResponseWriter, req *http.Request) { // w, req
    // ...
}

// Channels
ch := make(chan int)  // ch
done := make(chan struct{}) // done

// WaitGroup
var wg sync.WaitGroup // wg

// Mutex
var mu sync.Mutex     // mu

Rule: The shorter the scope, the shorter the name. i for loop index, ctx for context, err for error.


#9. Error Handling

Error is always the last return parameter.

// ✅ IDIOMATIC
func DoSomething() (result string, err error) {
    if someCondition {
        return "", fmt.Errorf("something failed: %w", err)
    }
    return "success", nil
}

// ✅ ALWAYS CHECK ERRORS
data, err := os.ReadFile("config.json")
if err != nil {
    return fmt.Errorf("read config: %w", err)
}

// ❌ BAD (ignores error)
data, _ := os.ReadFile("config.json")

// ✅ Don't use panic for normal errors
func ReadConfig(path string) (*Config, error) {
    // return error, don't panic
}

// ✅ Sentinel errors (with var)
var ErrNotFound = errors.New("not found")
var ErrPermission = errors.New("permission denied")

// ✅ Error wrapping with %w
if err != nil {
    return fmt.Errorf("user %s: %w", id, ErrNotFound)
}

// ✅ Check with errors.Is/As
if errors.Is(err, ErrNotFound) {
    // ...
}

#10. Constructors (New + Type)

Constructors are named NewType() or simply New().

// Idiomatic constructor
type User struct {
    Name string
}

func NewUser(name string) *User {
    return &User{Name: name}
}

type Server struct {
    addr string
}

func NewServer(addr string) *Server {
    return &Server{addr: addr}
}

// Return the interface, not the concrete struct
type Repository interface {
    Save(data string) error
}

type repository struct{}

func NewRepository() Repository {
    return &repository{}
}

// For complex configurations: Option pattern
type ServerConfig struct {
    Port int
    Host string
}

type ServerOption func(*ServerConfig)

func WithPort(port int) ServerOption {
    return func(c *ServerConfig) {
        c.Port = port
    }
}

func NewServerWithOptions(opts ...ServerOption) *Server {
    cfg := &ServerConfig{Port: 8080} // default value
    for _, opt := range opts {
        opt(cfg)
    }
    return &Server{addr: fmt.Sprintf("%s:%d", cfg.Host, cfg.Port)}
}

#11. Comments on Exported Elements

Every exported symbol must have a comment starting with its name.

// User represents a system user.
type User struct {
    Name string
    Age  int
}

// NewUser creates a new user with the given name and age.
func NewUser(name string, age int) *User {
    return &User{Name: name, Age: age}
}

// FullName returns the user's full name.
func (u *User) FullName() string {
    return u.Name
}

// ErrNotFound is returned when a user is not found.
var ErrNotFound = errors.New("user not found")

// ❌ BAD: comment doesn't start with the name
// Creates a new user
func NewUser(name string) *User { ... }

// ❌ BAD: no comment on exported symbol
type Config struct { ... }

Rule: Use // for comments (not /* */ except for large blocks).


#12. Formatting with gofmt

Automatic formatting is mandatory in the Go ecosystem.

# Format all files in the project
gofmt -w .

# Check formatting (CI)
test -z "$(gofmt -l .)"

# Newer alternative (Go 1.19+)
go fmt ./...

# Simplify code (recommends simplifications)
gofmt -s -w .

There is no debate about brace placement or spaces: gofmt decides.

Additional tools:

  • go vet : detects suspicious constructs
  • staticcheck : advanced linting
  • golangci-lint : meta-linter

#13. Declaration Order in a File

Recommended conventional order:

package user

import (
    "context"
    "fmt"
)

// 1. Constants
const (
    MaxNameLength = 100
    DefaultAge    = 18
)

// 2. Global variables
var defaultUser *User

// 3. Interfaces
type Repository interface {
    Find(id string) (*User, error)
}

// 4. Types (structs)
type User struct {
    ID   string
    Name string
    Age  int
}

// 5. Constructors
func NewUser(name string) *User {
    return &User{Name: name, Age: DefaultAge}
}

// 6. Exported methods (alphabetical order recommended)
func (u *User) FullName() string {
    return u.Name
}

func (u *User) Save(ctx context.Context) error {
    // ...
}

// 7. Unexported methods (helpers)
func (u *User) normalize() {
    u.Name = strings.TrimSpace(u.Name)
}

#14. context.Context

Context is always the first parameter of functions that use it.

// ✅ GOOD
func FetchUser(ctx context.Context, id string) (*User, error) {
    // ...
}

// ✅ GOOD (in HTTP handlers)
func (c *UserController) GetUser(w http.ResponseWriter, r *http.Request) {
    ctx := r.Context()
    // ...
}

// ✅ Context propagation
func ProcessRequest(ctx context.Context, req *Request) error {
    data, err := fetchData(ctx, req.ID)
    if err != nil {
        return err
    }
    return saveData(ctx, data)
}

// ❌ BAD (context in second position)
func FetchUser(id string, ctx context.Context) (*User, error) { ... }

// ❌ BAD (context in a struct)
type Service struct {
    ctx context.Context  // AVOID: context in a struct
}

Rule: Don’t store context in a struct. Pass it as a parameter.


#15. Initialization (init functions)

Avoid init() whenever possible. Use main() or explicit constructors.

// ❌ AVOID except very specific cases
func init() {
    // Executes automatically before main()
    // Hard to test and understand
    loadConfig()
}

// ✅ PREFER: explicit initialization
func main() {
    cfg, err := loadConfig()
    if err != nil {
        log.Fatal(err)
    }
    // ...
}

Acceptable use cases for init():

  • Initializing static tables (math/rand.seed())
  • Registering SQL drivers (sql.Register())
  • Complex global variables that cannot be initialized otherwise

#16. Channel and Goroutine Management

// ✅ Always close channels from the sender side
func produce(ch chan<- int) {
    defer close(ch)
    for i := 0; i < 10; i++ {
        ch <- i
    }
}

// ✅ Use select for timeouts
select {
case result := <-ch:
    return result, nil
case <-time.After(5 * time.Second):
    return nil, errors.New("timeout")
}

// ✅ Use done channel for cancellation
done := make(chan struct{})
go func() {
    for {
        select {
        case <-done:
            return
        default:
            // work
        }
    }
}()
close(done) // stop the goroutine

// ❌ Don't read from a closed channel (panic)
// ❌ Don't close a channel from the receiver side

#17. Testing Conventions

// File: user_test.go
package user

import "testing"

// Functional test: Test + FunctionName
func TestFullName(t *testing.T) {
    u := User{FirstName: "John", LastName: "Doe"}
    expected := "John Doe"
    if got := u.FullName(); got != expected {
        t.Errorf("FullName() = %q, want %q", got, expected)
    }
}

// Table-driven test (recommended)
func TestValidate(t *testing.T) {
    tests := []struct {
        name    string
        user    User
        wantErr bool
    }{
        {"valid user", User{Name: "John"}, false},
        {"empty name", User{Name: ""}, true},
    }

    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            err := tt.user.Validate()
            if (err != nil) != tt.wantErr {
                t.Errorf("Validate() error = %v, wantErr %v", err, tt.wantErr)
            }
        })
    }
}

// Benchmark: Benchmark + FunctionName
func BenchmarkFullName(b *testing.B) {
    u := User{FirstName: "John", LastName: "Doe"}
    for i := 0; i < b.N; i++ {
        u.FullName()
    }
}

// Example (shown in documentation)
func ExampleFullName() {
    u := User{FirstName: "John", LastName: "Doe"}
    fmt.Println(u.FullName())
    // Output: John Doe
}

Rule: Test files are named *_test.go and placed in the same package.


#18. Zero Value Initialization

// Every uninitialized variable has a zero value
var i int        // 0
var s string     // ""
var b bool       // false
var p *User      // nil
var slice []int  // nil (but you can append)
var m map[string]int  // nil (warning: nil map cannot be written to)

// ✅ GOOD: leverage zero value
type Counter struct {
    count int  // automatically initialized to 0
}

// ❌ BAD: redundant initialization
var i int = 0
var s string = ""

#19. Duration and Time Conventions

// ✅ Use time.Duration for intervals
func Wait(d time.Duration) {
    time.Sleep(d)
}

// ✅ Readable duration constants
Wait(5 * time.Second)
Wait(500 * time.Millisecond)
Wait(2 * time.Minute)

// ❌ BAD: magic numbers
Wait(5000000000)  // 5 seconds? nobody knows

#20. Named Result Parameters

// ✅ GOOD: to document the return
func ReadFile(path string) (data []byte, err error) {
    data, err = os.ReadFile(path)
    return  // naked return (only usable with named results)
}

// ❌ BAD: excessive use (source of confusion)
func Process(x int) (result int, err error) {
    result = x * 2
    return  // OK but sometimes ambiguous
}

// ✅ PREFER for clarity
func Process(x int) (int, error) {
    return x * 2, nil
}

#21. Empty Struct (Zero Memory)

// ✅ Use struct{} for signal channels (zero memory)
done := make(chan struct{})  // 0 bytes

// ✅ For sets (keys only)
set := map[string]struct{}{}
set["key"] = struct{}{}

// ✅ For methods on types that only contain methods
type Receiver struct{}  // 0 bytes

// ❌ BAD: bool for signal channels
done := make(chan bool)  // 1 byte, ambiguous (true/false?)

#22. Type Aliases

// ✅ Type definition (new type)
type UserID string  // new type, no implicit operations

// ❌ Type alias (same type, for refactoring)
type MyString = string  // very rare, avoid unless migration

// Why? UserID("123") cannot be assigned to string without conversion
var uid UserID = "123"
var s string = string(uid)  // explicit conversion required

#23. Package-Level Variables

// ❌ BAD: mutable global variables
var DB *sql.DB  // global mutable, hard to test

// ✅ GOOD: immutable configuration
var Config = struct {
    Port int
    Host string
}{
    Port: 8080,
    Host: "localhost",
}

// ✅ GOOD: sentinel error variable (immutable)
var ErrNotFound = errors.New("not found")

// ✅ GOOD: singleton with sync.Once
var (
    instance *Singleton
    once     sync.Once
)

func GetInstance() *Singleton {
    once.Do(func() {
        instance = &Singleton{}
    })
    return instance
}

#24. Variable Shadowing (Avoid)

// ❌ BAD: variable shadowing
func Process() error {
    data, err := fetchData()
    if err != nil {
        return err
    }

    if condition {
        data, err := transform(data)  // NEW 'err' variable (shadowing)
        if err != nil {
            return err  // returns local error, not the original
        }
    }
    // 'data' here was NOT modified by the transformation!
}

// ✅ GOOD: reuse existing variable
func Process() error {
    data, err := fetchData()
    if err != nil {
        return err
    }

    if condition {
        var transformedData string
        transformedData, err = transform(data)  // reuse existing err
        if err != nil {
            return err
        }
        data = transformedData
    }
}

// Detection: go vet detects shadowing

#25. Slice Preallocation

// ❌ BAD: repeated append without preallocation
var result []string
for i := 0; i < 1000; i++ {
    result = append(result, fmt.Sprintf("item-%d", i))
    // Multiple memory reallocations
}

// ✅ GOOD: preallocation with make
result := make([]string, 0, 1000)  // capacity = 1000
for i := 0; i < 1000; i++ {
    result = append(result, fmt.Sprintf("item-%d", i))
}

// ✅ GOOD: when size is known in advance
result := make([]string, 1000)
for i := 0; i < 1000; i++ {
    result[i] = fmt.Sprintf("item-%d", i)
}

#26. Golden Rule: Don’t Panic

// ❌ BAD: panic for normal errors
func ReadConfig(path string) *Config {
    data, err := os.ReadFile(path)
    if err != nil {
        panic(err)  // DO NOT DO THIS
    }
    // ...
}

// ✅ GOOD: return error
func ReadConfig(path string) (*Config, error) {
    data, err := os.ReadFile(path)
    if err != nil {
        return nil, fmt.Errorf("read config: %w", err)
    }
    // ...
}

// ✅ OK for panic: unrecoverable programmer error
func MustCompile(regex string) *regexp.Regexp {
    re, err := regexp.Compile(regex)
    if err != nil {
        panic(err)  // OK: user provided invalid regex at build time
    }
    return re
}

#27. go:embed for Assets (Go 1.16+)

// ✅ GOOD: embed static files
import _ "embed"

//go:embed config.json
var configData []byte

//go:embed templates/*.html
var templateFS embed.FS

//go:embed static/style.css
var styleCSS string

// ❌ BAD: read files at runtime (less portable)
data, _ := os.ReadFile("config.json")  // depends on filesystem

#28. Error Comparison: errors.Is vs ==

// ❌ BAD: direct comparison (doesn't work with wrapped errors)
if err == sql.ErrNoRows {
    // ...
}

// ✅ GOOD: errors.Is (works with %w wrapping)
if errors.Is(err, sql.ErrNoRows) {
    // ...
}

// ✅ GOOD: errors.As for specific types
var pathErr *os.PathError
if errors.As(err, &pathErr) {
    fmt.Println("Path:", pathErr.Path)
}

#29. Defer LIFO Order

// ✅ GOOD: understand LIFO order
func Example() {
    defer fmt.Println("1")  // executed last
    defer fmt.Println("2")  // executed before "1"
    defer fmt.Println("3")  // executed first
    // Output: 3 2 1
}

// ✅ GOOD: defer to release resources
func ReadFile(path string) ([]byte, error) {
    f, err := os.Open(path)
    if err != nil {
        return nil, err
    }
    defer f.Close()  // called even if error occurs later
    return io.ReadAll(f)
}

// ⚠️ WARNING: defer in a loop (memory issue)
for i := 0; i < 1000000; i++ {
    f, _ := os.Open(fmt.Sprintf("file-%d.txt", i))
    defer f.Close()  // defers accumulate until function exits!
}

#30. String Concatenation: strings.Builder vs +

// ❌ BAD: concatenation in a loop (O(n²))
var result string
for i := 0; i < 1000; i++ {
    result += "a"  // new allocation each iteration
}

// ✅ GOOD: strings.Builder
var builder strings.Builder
builder.Grow(1000)  // optional preallocation
for i := 0; i < 1000; i++ {
    builder.WriteString("a")
}
result := builder.String()

// ✅ GOOD: fmt.Sprintf for few elements
name := fmt.Sprintf("%s %s", firstName, lastName)

#31. Type Assertions: Comma Ok Form

// ❌ BAD: assertion without check (panics on failure)
var i interface{} = "hello"
s := i.(string)  // OK
n := i.(int)     // PANIC!

// ✅ GOOD: "comma ok" form
s, ok := i.(string)
if !ok {
    return fmt.Errorf("expected string, got %T", i)
}

// ✅ GOOD: type switch
switch v := i.(type) {
case string:
    fmt.Println("string:", v)
case int:
    fmt.Println("int:", v)
default:
    fmt.Println("unknown type")
}

#32. Range on Channel

// ✅ GOOD: range on channel (stops when channel is closed)
func consume(ch <-chan int) {
    for v := range ch {  // automatically stops when ch is closed
        fmt.Println(v)
    }
}

// ❌ BAD: infinite loop without closure
for {
    v, ok := <-ch
    if !ok {
        break
    }
    fmt.Println(v)
}

#33. time.After in Loops (Memory Leak)

// ❌ BAD: time.After in a loop (resource never released)
for {
    select {
    case <-ch:
        // ...
    case <-time.After(1 * time.Second):
        // time.After creates a timer each iteration
        // Timers are not garbage collected until they fire
    }
}

// ✅ GOOD: create a single timer and reuse it
timer := time.NewTimer(1 * time.Second)
defer timer.Stop()
for {
    timer.Reset(1 * time.Second)
    select {
    case <-ch:
        // ...
    case <-timer.C:
        // ...
    }
}

#34. Quick Reference Summary

Convention & RuleExample
Export/Private: uppercase first letter means exported, lowercase means private✅ Exported
User
❌ private
user
Package names: short, lowercase, singular, no underscores✅ Good
package user
❌ Bad
package userUtil
Acronyms: keep natural case (ID not Id, API not Api)✅ Good
userID
APIClient
❌ Bad
userId
ApiClient
Method receivers: 1-2 letters, consistent across all methods of the type✅ Good
func (u *User) FullName() string
func (u *User) Save() error
❌ Bad
func (this *User) GetName() string
Single-method interfaces: use -er suffix✅ Good
type Reader interface { Read(p []byte) (n int, err error) }
❌ Bad
type ReadInterface interface { Read(p []byte) (n int, err error) }
Getters: no Get prefix, just use the noun✅ Good
func (u *User) Name() string
❌ Bad
func (u *User) GetName() string
Boolean variables: start with Is, Has, Can, Should, Allow✅ Good
isReady
hasPermission
❌ Bad
ready
flag
Error handling: error is always the last return parameter, always check it✅ Good
func Do() (string, error)
data, err := os.ReadFile("file.txt")
❌ Bad
func Do() (error, string)
data, _ := os.ReadFile("file.txt")
Context: always the first parameter of a function, never stored in structs✅ Good
func FetchUser(ctx context.Context, id string) (*User, error)
❌ Bad
func FetchUser(id string, ctx context.Context) (*User, error)
type Service struct { ctx context.Context }
Constructors: name with New + Type or just New✅ Good
func NewUser(name string) *User
❌ Bad
func CreateUser(name string) *User
Formatting: always run gofmt or go fmt✅ Command
gofmt -w .
✅ Alternative
go fmt ./...
Name length: shorter for smaller scope, longer for larger scope✅ Short scope
i
✅ Large scope
configFilePath
Comments: every exported symbol must have a comment starting with its name✅ Good
// User represents a system user.
type User struct {}
❌ Bad
// This is a user struct
type User struct {}
init functions: avoid except for special cases (driver registration, static tables)❌ Avoid
func init() { loadConfig() }
✅ Acceptable
func init() { sql.Register("sqlite", &sqlite.Driver{}) }
Testing: files named *_test.go, functions named TestXxx, table-driven preferred✅ Good
func TestFullName(t *testing.T) { }
✅ File
user_test.go
Zero value: rely on implicit zero value instead of explicit initialization✅ Good
var i int
var s string
❌ Bad
var i int = 0
var s string = ""
Duration: use time.Duration with readable constants✅ Good
5 * time.Second
2 * time.Minute
❌ Bad
5000000000
Signal channel: use chan struct{} for empty signals✅ Good
done := make(chan struct{})
❌ Bad
done := make(chan bool)
Type alias: avoid, prefer type definition (new type)✅ Good
type UserID string
❌ Bad
type UserID = string
Package-level variables: prefer immutable or sync.Once initialization✅ Good
var ErrNotFound = errors.New("not found")
❌ Bad
var DB *sql.DB
Shadowing: avoid redeclaring variables in inner scopes❌ Bad
data, err := fetch()
if cond { data, err := transform(data) }
✅ Good
data, err := fetch()
if cond { var d string; d, err = transform(data); data = d }
Slice preallocation: use make with capacity when size is known✅ Good
make([]string, 0, 1000)
❌ Bad
var result []string
for i := 0; i < 1000; i++ { result = append(result, "a") }
Panic: only for unrecoverable programmer errors, not for normal errors✅ Acceptable
regexp.MustCompile("[0-9]+")
❌ Bad
func ReadConfig(path string) *Config { data, err := os.ReadFile(path); if err != nil { panic(err) } }
go:embed: prefer compile-time embedding over runtime file reads✅ Good
//go:embed config.json
var configData []byte
❌ Bad
data, _ := os.ReadFile("config.json")
Error comparison: use errors.Is for wrapped errors, not ==✅ Good
if errors.Is(err, ErrNotFound) { }
❌ Bad
if err == ErrNotFound { }
Defer: LIFO order (last deferred, first executed)✅ Example
defer fmt.Println("first")
defer fmt.Println("second")
// Output: second first
String concatenation in loops: use strings.Builder, not +✅ Good
var sb strings.Builder
for i := 0; i < 1000; i++ { sb.WriteString("a") }
❌ Bad
var s string
for i := 0; i < 1000; i++ { s += "a" }
Type assertion: always use comma-ok form to avoid panic✅ Good
s, ok := i.(string)
if !ok { return }
❌ Bad
s := i.(string)
Range on channel: loop stops when channel is closed✅ Good
for v := range ch { fmt.Println(v) }
❌ Bad
for { v, ok := <-ch; if !ok { break }; fmt.Println(v) }
time.After in loops: don’t use; create and reuse a timer instead❌ Bad
for { select { case <-ch: case <-time.After(1*time.Second): } }
✅ Good
timer := time.NewTimer(1*time.Second)
for { timer.Reset(1*time.Second); select { case <-ch: case <-timer.C: } }

#📚 References

Share this post