Skip to content
Skip to content
DocsGo LearningexpertIdiomatic Go
Chapter 14 of 14·expert·10 min read

Idiomatic Go

Go Thành Ngữ

Go proverbs, patterns, and best practices

Hover or tap any paragraph to see Vietnamese translation

Idiomatic Go

Idiomatic Go is code written in the way the Go community accepts and prefers. Go has strong design philosophy and clear guidance on how to write code. Focus on clarity, simplicity, and readability. Idiomatic Go code will be easy for others to understand and maintain.

Go Proverbs

Rob Pike, one of Go's creators, wrote Go proverbs that guide the design philosophy. These proverbs distill the wisdom of the Go community and should guide your code design.

Proverb 1: Don't communicate by sharing memory; share memory by communicating

Instead of using shared memory with mutexes, use channels to pass data between goroutines. Channels force you to think about data ownership and when it changes hands.

main.go
1// Bad: shared memory with mutex2var counter int3var mu sync.Mutex45func increment() {6	mu.Lock()7	counter++8	mu.Unlock()9}1011// Good: use channels12func incrementWithChannels() {13	ch := make(chan int)14	go func() {15		ch <- 116	}()17	value := <-ch  // receive ownership of value18}

Proverb 2: Concurrency is not parallelism

Concurrency is the ability to handle multiple tasks by switching between them. Parallelism is running multiple tasks simultaneously on multiple CPUs. Go supports concurrency well with goroutines, but parallelism depends on the number of CPUs.

main.go
1// Concurrent: multiple goroutines but may run on 1 CPU2go func() { doTask1() }()3go func() { doTask2() }()4go func() { doTask3() }()56// Parallelism requires multiple CPUs7// runtime.GOMAXPROCS(runtime.NumCPU())  // use all CPUs

Proverb 3: The bigger the interface, the weaker the abstraction

Small, focused interfaces are good. They enable decoupling and testability. Large interfaces usually have few implementations and are hard to use. The most popular Go interfaces (io.Reader, io.Writer) have just one method.

main.go
1// Bad: interface lớn2type DataStore interface {3	Get(key string) (interface{}, error)4	Set(key string, value interface{}) error5	Delete(key string) error6	Update(key string, value interface{}) error7	Exists(key string) (bool, error)8	GetAll() (map[string]interface{}, error)9	Clear() error10	// Hard to implement, hard to mock11}12

Proverb 4: Make the zero value useful

Types should be useful even when initialized with zero values (0, false, nil, empty string). Example: bytes.Buffer works immediately, sync.Mutex is useful without initialization. This reduces opportunities for errors.

main.go
1// Good: bytes.Buffer is useful immediately2var buf bytes.Buffer3buf.WriteString("Hello")  // works without explicit initialization4fmt.Println(buf.String()) // "Hello"56// Good: sync.Mutex is useful immediately7var mu sync.Mutex8mu.Lock()    // safe, no nil dereference9mu.Unlock()1011// Bad: requires explicit initialization12type Config struct {

Proverb 5: A little copying is better than a little dependency

Sometimes, copying a few lines of code is better than adding a package dependency. Dependencies have costs: maintenance, compatibility, security risk. If the code is small and self-contained, copying is reasonable.

Proverb 6: Clear is better than clever

Write clear, understandable code, even if it is longer. Clever tricks are not always good -> they make it hard for others to read. Go favors clarity over elegance.

main.go
1// Clever but hard to read2result := func(s []string) string {3	return strings.Join(append(make([]string, 0, len(s)), s...), ",")4}(data)56// Clear and straightforward7result := strings.Join(data, ",")

Proverb 7: Errors are values

Handle errors like any other value. You can store, pass, and check errors. There are no exceptions -> errors are returned and handled.

main.go
1// Good: errors as values2if err := doSomething(); err != nil {3	// handle error4	return fmt.Errorf("failed: %w", err)5}67// Good: create error values8var ErrNotFound = errors.New("not found")9var ErrInvalid = errors.New("invalid")1011// Check specific errors12if errors.Is(err, ErrNotFound) {13	// handle not found14}

Proverb 8: Don't just check errors, handle them gracefully

Do not just acknowledge errors -> provide useful context, rollback transactions, clean up resources. Error handling is important application logic.

Accept Interfaces, Return Structs

Functions should accept interfaces (allowing flexibility) but return concrete structs (so callers don't depend on abstractions). This creates powerful and testable APIs.

main.go
1// Accept interface (flexible)2func LoadConfig(r io.Reader) (*Config, error) {3	data, err := io.ReadAll(r)4	if err != nil {5		return nil, err6	}7	var cfg Config8	json.Unmarshal(data, &cfg)9	return &cfg, nil10}1112// Caller can pass file, network connection, buffer, etc.

Short Names for Loop Variables

Go has a convention: short names for short scope. Loop variables, function parameters, return values should have short names. Long names are for globals and functions.

main.go
1// Good: short names for loop variables2for i := 0; i < len(items); i++ {3	fmt.Println(items[i])4}56for _, v := range items {7	process(v)8}910for key, val := range m {11	fmt.Printf("%s: %v\n", key, val)12}

Avoid Premature Goroutines

Goroutines have overhead. Do not create a goroutine for every task. Start simple, then optimize based on profiling. Often a simple function on the main thread works fine.

Effective Go — Principles

Go's Effective Go documentation provides detailed guidance. Here are key points:

  • Commentary: Write comments for every exported package, type, function, method. Comments should be complete sentences starting with the exported name.
  • Naming — MixedCaps not snake_case: Go uses MixedCaps for exported (CapitalCase), lowercase for unexported. No snake_case.
  • Package names: short, lowercase, no underscores. The package name is the directory name. Variable names in the package should not repeat the package name.
  • Blank identifier: _ is used to discard values or import for side effects.
user.go
1// Good commenting2package user34// User represents a person in the system.5type User struct {6	// Name is the user's full name.7	Name string8	// Age is the user's age in years.9	Age int10}1112// NewUser creates a new User with the given name and age.

Key Takeaways

Điểm Chính

  • Use channels to pass data between goroutines, not shared memory with mutexesSử dụng channel để truyền dữ liệu giữa các goroutine, không phải shared memory với mutex
  • Small, focused interfaces are better than large ones with many methodsCác interface nhỏ, tập trung tốt hơn các interface lớn với nhiều phương thức
  • Functions should accept interfaces and return concrete structsHàm nên chấp nhận interface và trả về struct cụ thể
  • Go prioritizes clarity and simplicity over cleverness and eleganceGo ưu tiên sự rõ ràng và đơn giản hơn sự thông minh và thanh lịch

Practice

Test your understanding of this chapter

Quiz

According to Go proverbs, how should data be shared between goroutines?

Theo Go proverbs, dữ liệu nên được chia sẻ giữa các goroutine như thế nào?

Quiz

What is the idiomatic Go pattern for function signatures?

Mẫu idiomatic Go cho function signature là gì?

True or False

Go uses snake_case (like my_variable) for variable naming conventions.

Go sử dụng snake_case (như my_variable) cho quy ước đặt tên biến.

True or False

According to Go philosophy, it is better to write clever code that is hard to understand than simple code that is easy to read.

Theo triết học Go, tốt hơn là viết code thông minh khó hiểu hơn là code đơn giản dễ đọc.

Code Challenge

Fill in the Go naming convention for exported types

Điền vào quy ước đặt tên Go cho các kiểu được exported

// MyType is exported
// myType is unexported
Built: 6/25/2026, 3:03:36 PM