first commit
This commit is contained in:
@@ -0,0 +1,472 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user