473 lines
13 KiB
Markdown
473 lines
13 KiB
Markdown
# 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.
|