more text
This commit is contained in:
@@ -0,0 +1,717 @@
|
||||
# Go: Core Language Fundamentals
|
||||
|
||||
## Learning Objectives
|
||||
|
||||
- Understand Go's type system and value model.
|
||||
- Use variables, constants, control flow, and core data structures idiomatically.
|
||||
- Understand how slices, maps, strings, and pointers actually behave.
|
||||
- Write functions, methods, structs, and interfaces with practical clarity.
|
||||
- Handle errors the Go way instead of forcing exception-style thinking onto the language.
|
||||
- Build intuition for memory, receivers, and abstraction choices in real backend code.
|
||||
|
||||
## Start with the Right Mental Model
|
||||
|
||||
Go is a statically typed language centered on values. That sounds simple, but it shapes almost everything.
|
||||
|
||||
In practice, most Go code is about:
|
||||
|
||||
- creating values
|
||||
- transforming values
|
||||
- passing values between functions
|
||||
- attaching behavior to named types
|
||||
- returning explicit errors when something goes wrong
|
||||
|
||||
Go is not a class hierarchy language. It is not an exception-driven language. It is not a macro-heavy metaprogramming language. It is a language that tries to keep data flow and control flow visible.
|
||||
|
||||
That visibility is why production Go code can be easy to reason about even when the system itself is large.
|
||||
|
||||
## Variables, Constants, and Zero Values
|
||||
|
||||
### What They Are
|
||||
|
||||
Variables store values whose contents can change. Constants are fixed compile-time values.
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import "fmt"
|
||||
|
||||
func main() {
|
||||
var retries int = 3
|
||||
timeoutSeconds := 10
|
||||
const serviceName = "billing-api"
|
||||
|
||||
fmt.Println(retries, timeoutSeconds, serviceName)
|
||||
}
|
||||
```
|
||||
|
||||
### Why Go Has Multiple Declaration Styles
|
||||
|
||||
Go gives you a few common forms:
|
||||
|
||||
- `var name Type` when the type matters or you want the zero value
|
||||
- `var name Type = value` when you want explicitness
|
||||
- `name := value` inside functions when the type is obvious
|
||||
- `const` for fixed values known at compile time
|
||||
|
||||
Idiomatic Go uses short declaration heavily inside functions because it keeps code readable without losing type safety.
|
||||
|
||||
### Zero Values Matter More Than They Seem
|
||||
|
||||
Every variable has a useful default value.
|
||||
|
||||
- numbers become `0`
|
||||
- booleans become `false`
|
||||
- strings become `""`
|
||||
- pointers, slices, maps, interfaces, channels, and functions become `nil`
|
||||
|
||||
This matters because many Go types are designed so the zero value is already valid.
|
||||
|
||||
Examples:
|
||||
|
||||
- a `bytes.Buffer` can be used immediately
|
||||
- a `sync.Mutex` can be locked immediately
|
||||
- a `time.Time` has a well-defined zero state
|
||||
|
||||
That design lowers initialization friction and reduces a whole class of bugs.
|
||||
|
||||
### When to Use `const`
|
||||
|
||||
Use constants for values that are conceptually fixed:
|
||||
|
||||
- protocol names
|
||||
- status labels
|
||||
- numeric tuning knobs that should not change at runtime
|
||||
|
||||
Do not use `const` just because a variable happens not to change in one function. Prefer clarity over ceremony.
|
||||
|
||||
### `iota` and Enumerated Constants
|
||||
|
||||
Go does not have enums in the Java sense, but it uses typed constants effectively.
|
||||
|
||||
```go
|
||||
type JobState int
|
||||
|
||||
const (
|
||||
JobPending JobState = iota
|
||||
JobRunning
|
||||
JobDone
|
||||
JobFailed
|
||||
)
|
||||
```
|
||||
|
||||
Why this exists:
|
||||
|
||||
- it gives you readable symbolic values
|
||||
- it keeps the type distinct from unrelated integers
|
||||
- it works well with switches, logging, and serialization helpers
|
||||
|
||||
## Control Flow: Small Set, High Clarity
|
||||
|
||||
Go intentionally keeps control flow simple.
|
||||
|
||||
### `if`
|
||||
|
||||
```go
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
```
|
||||
|
||||
This pattern appears constantly in Go because errors are explicit return values.
|
||||
|
||||
You will also see scoped initialization inside `if`:
|
||||
|
||||
```go
|
||||
if user, err := repo.Find(ctx, id); err != nil {
|
||||
return err
|
||||
} else {
|
||||
fmt.Println(user.Name)
|
||||
}
|
||||
```
|
||||
|
||||
Use this sparingly. It is useful when the scope should remain local, but overused it can make code denser than necessary.
|
||||
|
||||
### `for`
|
||||
|
||||
Go has one looping keyword: `for`.
|
||||
|
||||
```go
|
||||
for i := 0; i < 3; i++ {
|
||||
fmt.Println(i)
|
||||
}
|
||||
|
||||
for condition {
|
||||
fmt.Println("acts like while")
|
||||
break
|
||||
}
|
||||
|
||||
for {
|
||||
break
|
||||
}
|
||||
```
|
||||
|
||||
Why this is useful:
|
||||
|
||||
- fewer looping forms to remember
|
||||
- easier syntax surface for reading code
|
||||
- the language avoids duplicated constructs with minor differences
|
||||
|
||||
### `range`
|
||||
|
||||
`range` iterates over slices, arrays, strings, maps, and channels.
|
||||
|
||||
```go
|
||||
nums := []int{10, 20, 30}
|
||||
for index, value := range nums {
|
||||
fmt.Println(index, value)
|
||||
}
|
||||
```
|
||||
|
||||
Important practical details:
|
||||
|
||||
- ranging over a map does not guarantee order
|
||||
- ranging over a string gives runes, not raw bytes
|
||||
- ranging over a channel continues until the channel is closed
|
||||
|
||||
### `switch`
|
||||
|
||||
Go's `switch` is more flexible than many beginners expect.
|
||||
|
||||
```go
|
||||
switch state {
|
||||
case JobPending:
|
||||
fmt.Println("queued")
|
||||
case JobRunning:
|
||||
fmt.Println("in progress")
|
||||
default:
|
||||
fmt.Println("terminal state")
|
||||
}
|
||||
```
|
||||
|
||||
There is also expression-less `switch`, which is a readable alternative to long `if` ladders.
|
||||
|
||||
## Core Data Structures
|
||||
|
||||
### Arrays
|
||||
|
||||
Arrays have fixed length and are value types.
|
||||
|
||||
```go
|
||||
var ports [3]int
|
||||
ports[0] = 8080
|
||||
```
|
||||
|
||||
Arrays exist, but in day-to-day Go you use slices much more often.
|
||||
|
||||
Why arrays still matter:
|
||||
|
||||
- they are the underlying foundation for slices
|
||||
- fixed-size data can be useful for performance-sensitive code
|
||||
- array values emphasize that size is part of the type
|
||||
|
||||
### Slices
|
||||
|
||||
A slice is a small descriptor pointing at an underlying array.
|
||||
|
||||
```go
|
||||
ids := []int{101, 102, 103}
|
||||
ids = append(ids, 104)
|
||||
```
|
||||
|
||||
Internally, a slice conceptually contains:
|
||||
|
||||
- a pointer to backing storage
|
||||
- a length
|
||||
- a capacity
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
A[Slice Header] --> B[Pointer]
|
||||
A --> C[Length]
|
||||
A --> D[Capacity]
|
||||
B --> E[Backing Array]
|
||||
```
|
||||
|
||||
### Why Slices Exist
|
||||
|
||||
They give you dynamic-seeming sequences without hiding memory behavior completely. They are a practical middle ground:
|
||||
|
||||
- easier to use than raw arrays
|
||||
- cheaper than many boxed collection abstractions
|
||||
- explicit enough that performance behavior is still understandable
|
||||
|
||||
### How `append` Works Internally
|
||||
|
||||
If capacity is available, `append` writes into the existing backing array. If capacity is exhausted, Go allocates a new array, copies the old contents, and returns a new slice header.
|
||||
|
||||
That means two things:
|
||||
|
||||
- appending may reallocate
|
||||
- slices sharing backing storage can affect each other unexpectedly
|
||||
|
||||
Example:
|
||||
|
||||
```go
|
||||
base := []int{1, 2, 3, 4}
|
||||
a := base[:2]
|
||||
b := base[:3]
|
||||
|
||||
a = append(a, 99)
|
||||
fmt.Println(base, a, b)
|
||||
```
|
||||
|
||||
This surprises many beginners because `a` may overwrite data visible through `b` if both still share the same backing array.
|
||||
|
||||
### Maps
|
||||
|
||||
Maps are Go's hash table type.
|
||||
|
||||
```go
|
||||
counts := map[string]int{
|
||||
"ok": 12,
|
||||
"error": 3,
|
||||
}
|
||||
|
||||
counts["retry"]++
|
||||
```
|
||||
|
||||
Why maps exist:
|
||||
|
||||
- fast key lookup
|
||||
- natural representation for counters, indexes, sets, and lookup tables
|
||||
|
||||
Important details:
|
||||
|
||||
- reading a missing key returns the value type's zero value
|
||||
- use the two-result form when absence matters
|
||||
- map iteration order is deliberately not stable
|
||||
|
||||
```go
|
||||
value, ok := counts["missing"]
|
||||
fmt.Println(value, ok)
|
||||
```
|
||||
|
||||
Maps are reference-like structures managed by the runtime. A nil map can be read from, but writing to it panics.
|
||||
|
||||
### Strings, Bytes, and Runes
|
||||
|
||||
Go strings are immutable sequences of bytes, usually holding UTF-8 encoded text.
|
||||
|
||||
This distinction matters:
|
||||
|
||||
- `byte` is an alias for `uint8`
|
||||
- `rune` is an alias for `int32` and represents a Unicode code point
|
||||
|
||||
```go
|
||||
message := "Go cafe"
|
||||
fmt.Println(len(message))
|
||||
|
||||
for _, r := range message {
|
||||
fmt.Printf("%c\n", r)
|
||||
}
|
||||
```
|
||||
|
||||
Why this matters in real systems:
|
||||
|
||||
- HTTP payloads and file formats are byte-oriented
|
||||
- user-visible text is Unicode-oriented
|
||||
- confusing the two leads to subtle bugs in validation, truncation, and indexing
|
||||
|
||||
If you need mutable byte data, use `[]byte`, not `string`.
|
||||
|
||||
## Functions: Multiple Return Values and Defer
|
||||
|
||||
### Functions as the Unit of Composition
|
||||
|
||||
Go prefers straightforward function composition over deep inheritance trees.
|
||||
|
||||
```go
|
||||
func parsePort(value string) (int, error) {
|
||||
port, err := strconv.Atoi(value)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("parse port %q: %w", value, err)
|
||||
}
|
||||
|
||||
return port, nil
|
||||
}
|
||||
```
|
||||
|
||||
### Why Multiple Return Values Exist
|
||||
|
||||
This is one of the most important Go design choices. Instead of exceptions for normal failure, functions can return both a result and an error.
|
||||
|
||||
Benefits:
|
||||
|
||||
- the failure path is visible in the function signature
|
||||
- callers must consciously handle errors
|
||||
- control flow stays explicit
|
||||
|
||||
In backend systems, this reduces the hidden control-flow jumps that exception-heavy code can create.
|
||||
|
||||
### `defer`
|
||||
|
||||
`defer` schedules a function call to run when the surrounding function returns.
|
||||
|
||||
```go
|
||||
func readConfig(path string) error {
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// read file
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
Why it exists:
|
||||
|
||||
- resource cleanup should be hard to forget
|
||||
- cleanup code often belongs near acquisition code
|
||||
|
||||
Internally, deferred calls are recorded and executed in last-in, first-out order when the function exits.
|
||||
|
||||
Use `defer` for correctness first. In very hot paths, you may care about its cost, but in most business logic the clarity win is worth it.
|
||||
|
||||
## Pointers and Memory Intuition
|
||||
|
||||
### What a Pointer Is
|
||||
|
||||
A pointer stores the address of another value.
|
||||
|
||||
```go
|
||||
func increment(value *int) {
|
||||
*value++
|
||||
}
|
||||
```
|
||||
|
||||
Go pointers exist so you can:
|
||||
|
||||
- mutate shared state intentionally
|
||||
- avoid copying very large values when appropriate
|
||||
- define methods that update a receiver
|
||||
|
||||
### Why Go Pointers Feel Safer Than C Pointers
|
||||
|
||||
Go allows pointers, but it removes several sharp edges:
|
||||
|
||||
- no pointer arithmetic
|
||||
- garbage collection manages object lifetime
|
||||
- type safety is preserved
|
||||
|
||||
This is a very deliberate design choice. Go wants the practical utility of pointers without turning everyday backend code into manual memory management.
|
||||
|
||||
### Stack, Heap, and Escape Analysis
|
||||
|
||||
Do not think of Go as "everything is on the heap." The compiler decides where values live.
|
||||
|
||||
- if a value can stay local safely, it may live on the stack
|
||||
- if it must outlive the local frame or be referenced elsewhere, it may escape to the heap
|
||||
|
||||
This compiler decision process is called escape analysis.
|
||||
|
||||
Why it matters:
|
||||
|
||||
- heap allocation can increase GC pressure
|
||||
- unnecessary pointer-heavy designs can make code slower and harder to reason about
|
||||
|
||||
You usually do not hand-place objects yourself. Instead, you write clear code and learn enough about allocation behavior to avoid obviously wasteful patterns.
|
||||
|
||||
## Structs and Methods
|
||||
|
||||
### Structs: Go's Primary Data Modeling Tool
|
||||
|
||||
Structs group related fields.
|
||||
|
||||
```go
|
||||
type User struct {
|
||||
ID int64
|
||||
Name string
|
||||
Email string
|
||||
}
|
||||
```
|
||||
|
||||
Why structs matter:
|
||||
|
||||
- they model domain entities cleanly
|
||||
- they work naturally with JSON, databases, and configuration
|
||||
- they keep data layout explicit
|
||||
|
||||
### Methods
|
||||
|
||||
Methods are functions attached to a type.
|
||||
|
||||
```go
|
||||
type Counter struct {
|
||||
value int
|
||||
}
|
||||
|
||||
func (c *Counter) Inc() {
|
||||
c.value++
|
||||
}
|
||||
|
||||
func (c Counter) Value() int {
|
||||
return c.value
|
||||
}
|
||||
```
|
||||
|
||||
### Value Receiver or Pointer Receiver
|
||||
|
||||
Use a pointer receiver when:
|
||||
|
||||
- the method mutates the receiver
|
||||
- the struct is large enough that copying is undesirable
|
||||
- consistency across methods is clearer with pointers
|
||||
|
||||
Use a value receiver when:
|
||||
|
||||
- the value is small and conceptually immutable
|
||||
- copying is cheap and expected
|
||||
|
||||
In real projects, consistency matters. If a type usually uses pointer receivers, use them across the method set unless there is a strong reason not to.
|
||||
|
||||
### Composition Over Inheritance
|
||||
|
||||
Go does not have class inheritance. Instead, it leans on composition and embedding.
|
||||
|
||||
```go
|
||||
type Logger struct{}
|
||||
|
||||
func (Logger) Info(msg string) {
|
||||
fmt.Println("INFO:", msg)
|
||||
}
|
||||
|
||||
type Server struct {
|
||||
Logger
|
||||
addr string
|
||||
}
|
||||
```
|
||||
|
||||
Embedding can promote fields and methods, but use it to express a real structural relationship, not to imitate inheritance mechanically.
|
||||
|
||||
## Interfaces: Small, Behavioral, and Implicit
|
||||
|
||||
### What an Interface Is
|
||||
|
||||
An interface describes behavior, not concrete data layout.
|
||||
|
||||
```go
|
||||
type Store interface {
|
||||
Save(ctx context.Context, user User) error
|
||||
}
|
||||
```
|
||||
|
||||
Any type with a matching method set satisfies the interface automatically.
|
||||
|
||||
### Why Implicit Satisfaction Exists
|
||||
|
||||
Go avoids the ceremony of explicit `implements` declarations. The benefit is that interfaces are lightweight and decoupled from concrete types.
|
||||
|
||||
That means you can define interfaces where they are needed, usually at the consumer side.
|
||||
|
||||
Example:
|
||||
|
||||
```go
|
||||
type UserService struct {
|
||||
store Store
|
||||
}
|
||||
```
|
||||
|
||||
The service depends on behavior, not a particular database implementation.
|
||||
|
||||
### How Interfaces Work Internally
|
||||
|
||||
Conceptually, an interface value holds:
|
||||
|
||||
- the dynamic concrete type
|
||||
- the concrete value or pointer value
|
||||
|
||||
This is why the "nil interface" pitfall exists. An interface can contain a typed nil pointer and still be non-nil as an interface value.
|
||||
|
||||
That pitfall shows up in logging, error returns, and optional dependency wiring.
|
||||
|
||||
### Keep Interfaces Small
|
||||
|
||||
Good Go interfaces are often tiny.
|
||||
|
||||
```go
|
||||
type Clock interface {
|
||||
Now() time.Time
|
||||
}
|
||||
```
|
||||
|
||||
Why small interfaces are better:
|
||||
|
||||
- easier to implement
|
||||
- easier to test
|
||||
- less coupling
|
||||
- behavior stays focused
|
||||
|
||||
Large interfaces usually signal that an abstraction was designed too early or at the wrong level.
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A[HTTP Handler] --> B[UserService interface]
|
||||
B --> C[PostgresUserService]
|
||||
B --> D[InMemoryUserService]
|
||||
```
|
||||
|
||||
## Generics: Useful, But Not the Center of Go
|
||||
|
||||
Modern Go supports type parameters.
|
||||
|
||||
```go
|
||||
func MapSlice[T any, U any](items []T, fn func(T) U) []U {
|
||||
result := make([]U, 0, len(items))
|
||||
for _, item := range items {
|
||||
result = append(result, fn(item))
|
||||
}
|
||||
return result
|
||||
}
|
||||
```
|
||||
|
||||
Why generics exist:
|
||||
|
||||
- avoid repetitive boilerplate for reusable containers and algorithms
|
||||
- preserve static type safety
|
||||
- reduce reliance on `interface{}` or reflection for generic utilities
|
||||
|
||||
When to use them:
|
||||
|
||||
- collections
|
||||
- reusable helpers where type-specific duplication is obvious
|
||||
- infrastructure libraries
|
||||
|
||||
When not to use them:
|
||||
|
||||
- when a plain interface or a concrete type is simpler
|
||||
- when the abstraction is more confusing than the duplication it removes
|
||||
|
||||
Go generics are powerful, but idiomatic Go still prefers the simplest tool that expresses the problem clearly.
|
||||
|
||||
## Error Handling Idioms
|
||||
|
||||
### Errors Are Values
|
||||
|
||||
This is a core Go idea. Failure is usually represented as an `error` return value.
|
||||
|
||||
```go
|
||||
func loadUser(ctx context.Context, repo Store, user User) error {
|
||||
if err := repo.Save(ctx, user); err != nil {
|
||||
return fmt.Errorf("save user %d: %w", user.ID, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
### Why Go Chooses Explicit Errors
|
||||
|
||||
In systems code, unexpected control flow is expensive to reason about. Explicit errors keep the failure path visible.
|
||||
|
||||
This helps with:
|
||||
|
||||
- tracing the source of failures
|
||||
- adding context at each layer
|
||||
- making retry and fallback decisions deliberately
|
||||
|
||||
### Wrapping Errors
|
||||
|
||||
Use `%w` with `fmt.Errorf` when you want to preserve the underlying cause.
|
||||
|
||||
```go
|
||||
return fmt.Errorf("read config: %w", err)
|
||||
```
|
||||
|
||||
Then callers can inspect it with `errors.Is` or `errors.As`.
|
||||
|
||||
### Sentinel Errors and Typed Errors
|
||||
|
||||
Sentinel errors are package-level variables for meaningful known cases.
|
||||
|
||||
```go
|
||||
var ErrNotFound = errors.New("not found")
|
||||
```
|
||||
|
||||
Typed errors are useful when the caller needs structured details.
|
||||
|
||||
Use them carefully. Too many special-case errors can make APIs harder to use.
|
||||
|
||||
### When to Panic
|
||||
|
||||
`panic` is for truly unrecoverable programmer or runtime states, not normal business failures.
|
||||
|
||||
Good uses are rare:
|
||||
|
||||
- impossible invariants that indicate a bug
|
||||
- startup-time failures in very small programs where continuing makes no sense
|
||||
|
||||
Bad uses are common:
|
||||
|
||||
- validation failure from user input
|
||||
- database connection hiccups during a request
|
||||
- file-not-found in normal control flow
|
||||
|
||||
In production services, panicking on ordinary errors creates instability and poor operability.
|
||||
|
||||
## Real-World Usage Patterns
|
||||
|
||||
### Request and Response Structs
|
||||
|
||||
Structs commonly model HTTP payloads, database rows, queue messages, and config.
|
||||
|
||||
```go
|
||||
type CreateOrderRequest struct {
|
||||
CustomerID string `json:"customer_id"`
|
||||
AmountCents int64 `json:"amount_cents"`
|
||||
}
|
||||
```
|
||||
|
||||
### Interfaces at Boundaries
|
||||
|
||||
A service often depends on an interface for storage or external calls, while the concrete implementation stays in another package.
|
||||
|
||||
### Explicit Error Returns for Every I/O Layer
|
||||
|
||||
Anything involving the network, disk, serialization, or databases should return precise errors with context. This is normal, not noisy. It is how production Go code stays debuggable.
|
||||
|
||||
## Common Mistakes and Misconceptions
|
||||
|
||||
### Mistake: Treating Slices Like Independent Dynamic Arrays
|
||||
|
||||
Slices can share backing storage. Appends and sub-slices can interact in surprising ways if you ignore capacity and aliasing.
|
||||
|
||||
### Mistake: Using Pointers Everywhere
|
||||
|
||||
Beginners sometimes assume pointer-heavy code is more advanced. Often it is just harder to read and increases allocation pressure. Use pointers for a reason, not by default.
|
||||
|
||||
### Mistake: Designing Huge Interfaces Up Front
|
||||
|
||||
Go interfaces work best when they are small and shaped by actual use. Large "service interfaces" often become rigid and awkward.
|
||||
|
||||
### Mistake: Ignoring Unicode Details
|
||||
|
||||
Indexing a string operates on bytes, not necessarily user-visible characters. This matters for APIs, validation, and text handling.
|
||||
|
||||
### Mistake: Using `panic` for Routine Error Handling
|
||||
|
||||
That is usually a sign you are importing habits from another language rather than using Go idioms.
|
||||
|
||||
### Mistake: Confusing Nil Values
|
||||
|
||||
Nil slices, nil maps, nil pointers, and nil interfaces do not all behave the same. Learn their semantics explicitly.
|
||||
|
||||
## Summary
|
||||
|
||||
Go's core language is intentionally compact, but it is not shallow. The important ideas are practical:
|
||||
|
||||
- values and types are central
|
||||
- slices, maps, and strings have concrete runtime behavior worth understanding
|
||||
- functions return errors explicitly
|
||||
- structs and methods model data and behavior
|
||||
- interfaces describe behavior and stay best when small
|
||||
- pointers and memory behavior matter, but Go shields you from manual memory management
|
||||
|
||||
If you can read and write these fundamentals fluently, you are ready for the most distinctive part of Go: concurrency with goroutines, channels, synchronization primitives, and request-scoped cancellation.
|
||||
Reference in New Issue
Block a user