Files

473 lines
13 KiB
Markdown
Raw Permalink Normal View History

2026-07-16 10:13:46 +03:30
# Go Basics, Part 3 — Interfaces, Errors, Collections, Packages, and Concurrency
This is the last basics lesson. It covers everything else the main course
leans on: interfaces (how `http.Handler` and similar types work),
proper error handling patterns, slices and maps, how packages/modules/imports
actually work, a first look at goroutines (needed for graceful shutdown),
and JSON encoding/decoding.
## 1. Interfaces — Go's version of "any type that can do X"
An **interface** is a type defined purely by a set of method signatures.
Any type that has those methods automatically satisfies the interface —
there's no `implements` keyword, no explicit declaration. This is called
**structural typing** or "duck typing, but checked at compile time."
```go
package main
import "fmt"
// Any type with a Speak() string method satisfies Speaker - automatically.
type Speaker interface {
Speak() string
}
type Dog struct{}
func (d Dog) Speak() string { return "Woof!" }
type Cat struct{}
func (c Cat) Speak() string { return "Meow!" }
func announce(s Speaker) {
fmt.Println(s.Speak())
}
func main() {
announce(Dog{}) // Woof!
announce(Cat{}) // Meow!
}
```
`Dog` and `Cat` never mention `Speaker` anywhere in their code. They just
happen to have a method with the right name and signature, which is
enough. This is why, in the main course, `*chi.Mux` can be passed directly
to `http.Server{Handler: r}``http.Handler` is defined (in the standard
library) as:
```go
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
```
`*chi.Mux` happens to have a `ServeHTTP` method, so it automatically
satisfies `http.Handler`, with zero extra code. Same story for our own
handlers wrapped via `http.HandlerFunc(...)` — a small built-in adapter
type that turns any function shaped `func(w, r)` into something with a
`ServeHTTP` method, satisfying the interface.
### `any` (a.k.a. `interface{}`)
The empty interface — one with zero required methods — is satisfied by
**every** type, since every type trivially has "at least zero" methods.
Go has a built-in alias for this: `any` (added in Go 1.18; older code
uses the equivalent `interface{}`).
```go
func describe(v any) {
fmt.Printf("value: %v, type: %T\n", v, v)
}
describe(42) // value: 42, type: int
describe("hello") // value: hello, type: string
describe(User{}) // value: {}, type: main.User
```
You'll see `any` used for things like generic JSON response helpers
(`map[string]any`) where the value could be a string, a number, a nested
object — anything.
### Type assertions
If you have a value typed as an interface (or `any`) and need the
concrete type back out, use a **type assertion**:
```go
var v any = "hello"
s := v.(string) // single-value form - PANICS if v isn't actually a string
s, ok := v.(string) // two-value form - SAFE: ok is false on mismatch, no panic
if !ok {
fmt.Println("v was not a string")
}
```
**Always prefer the two-value form** unless you're absolutely certain of
the type — a failed single-value assertion crashes your program. This
shows up in the main course when reading a value back out of a
`context.Context` (Lesson 8) — the value is stored as `any`, so you need a
type assertion to get a concrete struct back.
## 2. Error handling, properly
Go's `error` is just an interface:
```go
type error interface {
Error() string
}
```
Any type with an `Error() string` method IS an error. The standard library
gives you two easy ways to create one:
```go
import (
"errors"
"fmt"
)
err1 := errors.New("something went wrong")
err2 := fmt.Errorf("failed to process user %d", 42)
```
### The `if err != nil` pattern
```go
func readConfig() (string, error) {
// pretend this can fail
return "", errors.New("config file not found")
}
func main() {
config, err := readConfig()
if err != nil {
fmt.Println("error:", err)
return // stop here - don't continue using `config`, it's meaningless
}
fmt.Println("config:", config)
}
```
Checking `err != nil` after every call that can fail, and handling it
immediately, is the single most repeated pattern in idiomatic Go — and in
the entire main course.
### Wrapping errors with `%w`
When an error crosses through several layers of your program, it's useful
to add context at each layer without losing the original error:
```go
func openFile() error {
return errors.New("file not found")
}
func loadConfig() error {
if err := openFile(); err != nil {
return fmt.Errorf("load config: %w", err) // %w WRAPS, preserving err
}
return nil
}
```
`%w` (as opposed to `%v` or `%s`) specifically **wraps** the original
error, meaning code further up the chain can still inspect what the
original error actually was, using `errors.Is` or `errors.As`.
### Sentinel errors and `errors.Is`
A **sentinel error** is a specific, predefined error value that callers
can check for by identity, not by comparing message strings (which is
fragile — messages change, causes bugs).
```go
var ErrNotFound = errors.New("not found")
func findUser(id int) (string, error) {
if id != 1 {
return "", ErrNotFound
}
return "Hamid", nil
}
func main() {
_, err := findUser(99)
if errors.Is(err, ErrNotFound) {
fmt.Println("no such user!")
}
}
```
`errors.Is` works correctly even if the error was wrapped with `%w`
several layers deep — it "unwraps" automatically to check. This exact
pattern (`var ErrUserNotFound = errors.New(...)`, then
`errors.Is(err, ErrUserNotFound)`) is used throughout the main course's
repository layer.
## 3. Slices and maps — Go's core collection types
### Slices — dynamically-sized lists
```go
// A slice literal
names := []string{"alice", "bob", "carol"}
fmt.Println(names[0]) // "alice" - zero-indexed
fmt.Println(len(names)) // 3
names = append(names, "dave") // append returns a NEW slice - reassign it!
fmt.Println(names) // [alice bob carol dave]
// An empty slice, grown later
var scores []int
scores = append(scores, 10)
scores = append(scores, 20)
// Looping (seen in Part 1, repeated here for completeness)
for i, name := range names {
fmt.Println(i, name)
}
```
Important: `append` may or may not modify the original underlying array —
you should always use the return value (`names = append(names, ...)`),
never assume the original variable was updated in place.
### Maps — key/value lookups
```go
ages := map[string]int{
"alice": 30,
"bob": 25,
}
fmt.Println(ages["alice"]) // 30
ages["carol"] = 28 // add/update a key
delete(ages, "bob") // remove a key
// Reading a key that doesn't exist returns the TYPE'S ZERO VALUE, not an
// error or nil-equivalent crash:
fmt.Println(ages["nobody"]) // 0 (the zero value for int)
// The "comma ok" idiom - check whether a key actually exists:
age, ok := ages["nobody"]
if !ok {
fmt.Println("no such key")
}
// Looping over a map (order is NOT guaranteed - it's randomized each run)
for name, age := range ages {
fmt.Println(name, age)
}
```
You'll see `map[string]any` used constantly in the main course for
building ad-hoc JSON responses, e.g. `map[string]any{"id": user.ID,
"email": user.Email}`.
## 4. Packages, imports, and modules — how a real project is organized
You already saw `package main` in Part 1. Any other folder full of `.go`
files declares its own package name (usually matching the folder name),
and can be imported by other code.
```
myproject/
├── go.mod
├── main.go -- package main
└── greeter/
└── greeter.go -- package greeter
```
**`greeter/greeter.go`**
```go
package greeter
func Hello(name string) string {
return "Hello, " + name + "!"
}
```
**`main.go`**
```go
package main
import (
"fmt"
"myproject/greeter" // import path = module path + folder path
)
func main() {
fmt.Println(greeter.Hello("Hamid"))
}
```
The import path `"myproject/greeter"` is built from the module's name
(declared in `go.mod` via `module myproject`) plus the folder path. This
is exactly the pattern behind every internal import you'll see in the main
course, e.g.:
```go
import "git.hamidsoltani.com/hamid/go-simple-api/internal/config"
```
— the module is `git.hamidsoltani.com/hamid/go-simple-api` (declared once,
at the top of the project's `go.mod`), and `internal/config` is the folder
path to that specific package.
### The special `internal/` folder
Any package inside a folder literally named `internal/` can ONLY be
imported by code within the same module (specifically, code rooted at the
parent of `internal/`). This is a compiler-enforced way to say "this code
is a private implementation detail of this project, not a public library
for others to import." The main course's entire codebase lives under
`internal/` for exactly this reason.
### External packages and `go.mod`
To use code someone else published (like the chi router), you add it as a
dependency:
```bash
go get github.com/go-chi/chi/v5@latest
```
This downloads the package, records it in `go.mod` (a "require" line with
a specific version), and records exact checksums in `go.sum` (so builds
are reproducible and verifiably untampered). After that, you import it
just like any other package:
```go
import "github.com/go-chi/chi/v5"
```
`go mod tidy` is a command you'll run often — it scans your code for
imports it doesn't yet know about, fetches them, and also removes
`go.mod` entries for anything you've stopped importing.
## 5. A first look at goroutines (needed for Lesson 1's graceful shutdown)
A **goroutine** is a lightweight, independently-running function — Go's
built-in concurrency primitive. You start one with the `go` keyword:
```go
package main
import (
"fmt"
"time"
)
func sayHello() {
fmt.Println("hello from a goroutine")
}
func main() {
go sayHello() // starts sayHello running CONCURRENTLY, doesn't block
fmt.Println("this may print before OR after 'hello from a goroutine'")
time.Sleep(100 * time.Millisecond) // give the goroutine time to run
// without this Sleep, main() might exit before sayHello ever runs -
// when main() returns, the WHOLE PROGRAM exits immediately, goroutines
// and all.
}
```
The key thing to understand: `go someFunction()` starts `someFunction`
running in the background and immediately continues to the next line —
it does **not** wait for `someFunction` to finish. This is exactly why the
main course wraps `srv.ListenAndServe()` in a goroutine in Lesson 1: that
call blocks forever (serving requests) — running it as a goroutine frees
up `main()`'s main line of execution to move on and listen for shutdown
signals (Ctrl+C) instead of getting stuck forever inside `ListenAndServe`.
We won't go deeper into concurrency (channels, `sync.WaitGroup`, etc.) in
this course — the main project only needs this one goroutine pattern.
## 6. JSON basics with `encoding/json`
Go's standard library can convert between Go values and JSON text
automatically, using struct tags (from Part 2) to control field naming.
### Encoding (Go value → JSON)
```go
package main
import (
"encoding/json"
"fmt"
)
type User struct {
Name string `json:"name"`
Age int `json:"age"`
}
func main() {
u := User{Name: "Hamid", Age: 31}
// Marshal converts a Go value into a []byte of JSON text
data, err := json.Marshal(u)
if err != nil {
fmt.Println("error:", err)
return
}
fmt.Println(string(data)) // {"name":"Hamid","age":31}
}
```
### Decoding (JSON → Go value)
```go
jsonText := `{"name":"Sara","age":28}`
var u User
err := json.Unmarshal([]byte(jsonText), &u) // note the & - Unmarshal WRITES into u
if err != nil {
fmt.Println("error:", err)
return
}
fmt.Println(u.Name, u.Age) // Sara 28
```
Note the `&u` — just like `rows.Scan(&x)` from database code, `Unmarshal`
needs to *write into* your variable, so it needs its address.
### Streaming versions: `Encoder`/`Decoder`
When working with HTTP requests/responses (which are streams, not
in-memory byte slices), you'll more often see the streaming forms:
```go
// Writing JSON directly to an io.Writer (e.g. http.ResponseWriter)
json.NewEncoder(w).Encode(u)
// Reading JSON directly from an io.Reader (e.g. an HTTP request body)
var u User
json.NewDecoder(r.Body).Decode(&u)
```
These do the same job as `Marshal`/`Unmarshal` but write/read directly to
a stream instead of requiring a full `[]byte` up front. You'll use
`NewDecoder(r.Body).Decode(...)` and `NewEncoder(w).Encode(...)` on nearly
every handler in the main course, starting in Lesson 1.
## 7. You're ready
That's everything the main course leans on. A quick self-check — if these
all feel familiar, you're ready for Lesson 1:
- Declaring variables with `:=` and `var`, and Go's zero values
- Writing functions with multiple return values, and the `if err != nil`
pattern
- Structs, exported vs. unexported fields, struct tags
- Pointers: `&` to get an address, `*` to dereference, and why functions
take `*User` instead of `User` when they need to modify it
- Methods with value vs. pointer receivers
- Interfaces being satisfied implicitly (no `implements` keyword)
- Slices (`append`, indexing, `range`) and maps (`map[string]any`, the
comma-ok idiom)
- How packages/imports/modules fit together, and what `internal/` means
- `go someFunc()` starting a goroutine, and why that matters for a
blocking call like `ListenAndServe`
- `json.NewEncoder(w).Encode(...)` / `json.NewDecoder(r.Body).Decode(&x)`
Head to `lesson-01-project-skeleton-chi-routing.md` next.