340 lines
9.7 KiB
Markdown
340 lines
9.7 KiB
Markdown
# Go Basics, Part 2 — Functions, Structs, Methods, and Pointers
|
|||
|
|
|
||
|
|
This continues directly from Part 1. By the end of this lesson you'll
|
||
|
|
understand every syntactic shape used in the main course's handler and
|
||
|
|
repository code.
|
||
|
|
|
||
|
|
## 1. Functions
|
||
|
|
|
||
|
|
```go
|
||
|
|
package main
|
||
|
|
|
||
|
|
import "fmt"
|
||
|
|
|
||
|
|
// A function with two parameters (both int) and one return value (int).
|
||
|
|
// Parameters sharing a type can share the type annotation: "a, b int"
|
||
|
|
// means both a and b are int.
|
||
|
|
func add(a, b int) int {
|
||
|
|
return a + b
|
||
|
|
}
|
||
|
|
|
||
|
|
func main() {
|
||
|
|
sum := add(3, 4)
|
||
|
|
fmt.Println(sum) // 7
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### Multiple return values — used constantly in Go
|
||
|
|
|
||
|
|
This is one of Go's most distinctive features, and you'll see it on
|
||
|
|
almost every line of real Go code, especially for error handling:
|
||
|
|
|
||
|
|
```go
|
||
|
|
func divide(a, b int) (int, error) {
|
||
|
|
if b == 0 {
|
||
|
|
return 0, fmt.Errorf("cannot divide by zero")
|
||
|
|
}
|
||
|
|
return a / b, nil
|
||
|
|
}
|
||
|
|
|
||
|
|
func main() {
|
||
|
|
result, err := divide(10, 2)
|
||
|
|
if err != nil {
|
||
|
|
fmt.Println("error:", err)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
fmt.Println("result:", result)
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
The pattern `value, err := someFunc()` followed immediately by
|
||
|
|
`if err != nil { ... }` is THE dominant idiom in Go. You will type this
|
||
|
|
exact shape hundreds of times in the main course. There are no exceptions
|
||
|
|
/ try-catch in Go (with one narrow exception, `panic`/`recover`, which
|
||
|
|
we'll touch on briefly later) — errors are just regular return values that
|
||
|
|
you're expected to check every time.
|
||
|
|
|
||
|
|
`nil` is Go's "no value" — similar to `null` in other languages. It's the
|
||
|
|
zero value for pointers, interfaces, slices, maps, channels, and function
|
||
|
|
types. `error` is an interface (explained in Part 3), so `nil` is its zero
|
||
|
|
value too — "no error occurred."
|
||
|
|
|
||
|
|
### Named return values (used occasionally, good to recognize)
|
||
|
|
|
||
|
|
```go
|
||
|
|
func divide(a, b int) (result int, err error) {
|
||
|
|
if b == 0 {
|
||
|
|
err = fmt.Errorf("cannot divide by zero")
|
||
|
|
return
|
||
|
|
}
|
||
|
|
result = a / b
|
||
|
|
return
|
||
|
|
}
|
||
|
|
```
|
||
|
|
`result` and `err` are declared as part of the function signature; a bare
|
||
|
|
`return` sends back their current values. You won't write much code this
|
||
|
|
way in this course, but you'll see it in standard-library source if you
|
||
|
|
ever go looking.
|
||
|
|
|
||
|
|
### Anonymous functions and closures
|
||
|
|
|
||
|
|
A function can be defined without a name and assigned to a variable, or
|
||
|
|
passed directly as an argument:
|
||
|
|
|
||
|
|
```go
|
||
|
|
square := func(n int) int {
|
||
|
|
return n * n
|
||
|
|
}
|
||
|
|
fmt.Println(square(5)) // 25
|
||
|
|
```
|
||
|
|
|
||
|
|
A **closure** is an anonymous function that "remembers" variables from the
|
||
|
|
scope it was created in, even after that outer function has returned:
|
||
|
|
|
||
|
|
```go
|
||
|
|
func makeCounter() func() int {
|
||
|
|
count := 0
|
||
|
|
return func() int {
|
||
|
|
count++
|
||
|
|
return count
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func main() {
|
||
|
|
counter := makeCounter()
|
||
|
|
fmt.Println(counter()) // 1
|
||
|
|
fmt.Println(counter()) // 2
|
||
|
|
fmt.Println(counter()) // 3 - count persists between calls!
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
This is important: `makeCounter` returns a function, and that returned
|
||
|
|
function still has access to `count`, which technically belongs to
|
||
|
|
`makeCounter`'s own (finished) execution. This exact mechanism is what
|
||
|
|
makes Go's HTTP middleware pattern work — you'll see functions that take
|
||
|
|
some setup arguments and return another function, three layers deep, all
|
||
|
|
throughout the main course (starting in Lesson 2). Understanding this
|
||
|
|
closure example is the key to understanding that pattern.
|
||
|
|
|
||
|
|
## 2. Structs — Go's way of grouping data
|
||
|
|
|
||
|
|
Go doesn't have classes. Instead, it has **structs**: named groups of
|
||
|
|
fields.
|
||
|
|
|
||
|
|
```go
|
||
|
|
package main
|
||
|
|
|
||
|
|
import "fmt"
|
||
|
|
|
||
|
|
type User struct {
|
||
|
|
Name string
|
||
|
|
Age int
|
||
|
|
}
|
||
|
|
|
||
|
|
func main() {
|
||
|
|
// Construct a struct with a "struct literal"
|
||
|
|
u := User{
|
||
|
|
Name: "Hamid",
|
||
|
|
Age: 31,
|
||
|
|
}
|
||
|
|
|
||
|
|
fmt.Println(u.Name, u.Age) // access fields with dot notation
|
||
|
|
u.Age = 32 // fields are mutable
|
||
|
|
fmt.Println(u.Age)
|
||
|
|
|
||
|
|
// You can also build one without field names, in declared order
|
||
|
|
// (works, but fragile - prefer named fields)
|
||
|
|
u2 := User{"Sara", 28}
|
||
|
|
fmt.Println(u2)
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### Capitalization matters: exported vs. unexported
|
||
|
|
|
||
|
|
This is one of Go's most important and most beginner-surprising rules:
|
||
|
|
|
||
|
|
> **Any identifier (variable, function, type, struct field...) that
|
||
|
|
> starts with an UPPERCASE letter is "exported" — visible outside its
|
||
|
|
> package. Anything starting lowercase is "unexported" — private to its
|
||
|
|
> own package.**
|
||
|
|
|
||
|
|
There's no `public`/`private` keyword. Capitalization IS the access
|
||
|
|
control.
|
||
|
|
|
||
|
|
```go
|
||
|
|
type User struct {
|
||
|
|
Name string // exported - visible to other packages
|
||
|
|
age int // unexported - only visible inside THIS package
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
You'll see this constantly in the main course: struct fields like
|
||
|
|
`models.User.Email` are capitalized (need to be readable/settable from
|
||
|
|
`handlers`), while helper functions like `getEnv` in the config package
|
||
|
|
are lowercase (only used internally, no other package needs them).
|
||
|
|
|
||
|
|
### Struct tags — metadata attached to fields
|
||
|
|
|
||
|
|
```go
|
||
|
|
type LoginRequest struct {
|
||
|
|
Email string `json:"email"`
|
||
|
|
Password string `json:"password"`
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
The text in backticks after a field is a **struct tag** — a string of
|
||
|
|
metadata that other packages can read via reflection. `encoding/json`
|
||
|
|
(the standard library's JSON package) reads the `json:"..."` tag to know
|
||
|
|
"the JSON key `email` maps to this Go field," regardless of the Go field
|
||
|
|
name's capitalization. We use this on nearly every request/response struct
|
||
|
|
in the main course.
|
||
|
|
|
||
|
|
## 3. Pointers (`*` and `&`) — the single most important concept to nail down
|
||
|
|
|
||
|
|
Every variable lives somewhere in memory, at an address. A **pointer** is
|
||
|
|
a variable whose value IS a memory address — it "points to" where another
|
||
|
|
variable lives.
|
||
|
|
|
||
|
|
- `&x` — "give me the address of `x`" (turns a value into a pointer to it)
|
||
|
|
- `*T` (in a type position) — means "a pointer to a `T`", e.g. `*int`,
|
||
|
|
`*User`
|
||
|
|
- `*p` (in an expression) — "dereference `p`": go to the address it holds
|
||
|
|
and read/write the value stored there
|
||
|
|
|
||
|
|
```go
|
||
|
|
package main
|
||
|
|
|
||
|
|
import "fmt"
|
||
|
|
|
||
|
|
func main() {
|
||
|
|
x := 10
|
||
|
|
p := &x // p is a pointer to x; p holds x's memory address
|
||
|
|
|
||
|
|
fmt.Println(x) // 10
|
||
|
|
fmt.Println(p) // something like 0xc0000140a0 - an address
|
||
|
|
fmt.Println(*p) // 10 - dereferencing p gives x's value back
|
||
|
|
|
||
|
|
*p = 20 // dereference p, then assign through it - changes x itself!
|
||
|
|
fmt.Println(x) // 20
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### Why pointers matter: Go passes everything by value
|
||
|
|
|
||
|
|
When you pass a variable to a function, the function receives a **copy**.
|
||
|
|
If you want a function to actually modify the caller's variable, you must
|
||
|
|
pass a pointer, and the function must dereference it to write through.
|
||
|
|
|
||
|
|
```go
|
||
|
|
func double(n int) {
|
||
|
|
n = n * 2 // only changes the LOCAL COPY
|
||
|
|
}
|
||
|
|
|
||
|
|
func doublePtr(n *int) {
|
||
|
|
*n = *n * 2 // dereferences and changes the ORIGINAL
|
||
|
|
}
|
||
|
|
|
||
|
|
func main() {
|
||
|
|
x := 5
|
||
|
|
double(x)
|
||
|
|
fmt.Println(x) // 5 - unchanged!
|
||
|
|
|
||
|
|
doublePtr(&x)
|
||
|
|
fmt.Println(x) // 10 - changed
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### Pointers to structs, and why the main course uses them everywhere
|
||
|
|
|
||
|
|
```go
|
||
|
|
type Book struct {
|
||
|
|
Title string
|
||
|
|
Pages int
|
||
|
|
}
|
||
|
|
|
||
|
|
func addPages(b *Book, extra int) {
|
||
|
|
b.Pages += extra // note: no need to write (*b).Pages, Go allows b.Pages directly
|
||
|
|
}
|
||
|
|
|
||
|
|
func main() {
|
||
|
|
book := Book{Title: "Go 101", Pages: 100}
|
||
|
|
addPages(&book, 50)
|
||
|
|
fmt.Println(book.Pages) // 150
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
Note `b.Pages` instead of `(*b).Pages` — Go automatically dereferences
|
||
|
|
struct pointers for field access, as a convenience. Both work; everyone
|
||
|
|
writes `b.Pages`.
|
||
|
|
|
||
|
|
Two big reasons the main course uses pointers to structs constantly:
|
||
|
|
|
||
|
|
1. **Writing a result back into the caller's variable.** E.g. after
|
||
|
|
inserting a new row into the database, we want to write the newly
|
||
|
|
generated ID back into the struct the caller already has — that only
|
||
|
|
works if the function received a pointer.
|
||
|
|
2. **Sharing one instance instead of copying it.** Things like a database
|
||
|
|
connection pool or a logger should be ONE shared instance used
|
||
|
|
everywhere, not copied every time they're passed around. That's why
|
||
|
|
`sql.Open` returns `*sql.DB`, not `sql.DB` — every part of the app
|
||
|
|
needs to share the exact same pool.
|
||
|
|
|
||
|
|
### Methods and receivers
|
||
|
|
|
||
|
|
A **method** is a function attached to a specific type, via a **receiver**
|
||
|
|
declared between `func` and the method name:
|
||
|
|
|
||
|
|
```go
|
||
|
|
type Counter struct {
|
||
|
|
count int
|
||
|
|
}
|
||
|
|
|
||
|
|
// Value receiver - c is a COPY of the Counter this method was called on.
|
||
|
|
func (c Counter) Value() int {
|
||
|
|
return c.count
|
||
|
|
}
|
||
|
|
|
||
|
|
// Pointer receiver - c is the ADDRESS of the real Counter.
|
||
|
|
func (c *Counter) Increment() {
|
||
|
|
c.count++ // modifies the REAL struct, not a copy
|
||
|
|
}
|
||
|
|
|
||
|
|
func main() {
|
||
|
|
c := Counter{}
|
||
|
|
c.Increment()
|
||
|
|
c.Increment()
|
||
|
|
fmt.Println(c.Value()) // 2
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
**Rule of thumb, used throughout the main course:** if a method needs to
|
||
|
|
modify the struct, or the struct holds a resource like a database
|
||
|
|
connection, use a pointer receiver (`*Counter`). If the struct is small
|
||
|
|
and the method is purely read-only, a value receiver is fine — but
|
||
|
|
pointer receivers are the overwhelming default for anything nontrivial,
|
||
|
|
and that's what you'll see almost everywhere in this project (e.g. every
|
||
|
|
method on `*UserRepository`, `*AuthHandler`).
|
||
|
|
|
||
|
|
Go automatically inserts the `&` for you when calling a pointer-receiver
|
||
|
|
method on an addressable value — `c.Increment()` is really
|
||
|
|
`(&c).Increment()` behind the scenes. You don't need to write that `&`
|
||
|
|
yourself; just know it's happening.
|
||
|
|
|
||
|
|
## 4. Try it yourself
|
||
|
|
|
||
|
|
New scratch folder, `go mod init practice2`:
|
||
|
|
|
||
|
|
1. Define a `Book` struct with `Title string`, `Author string`, and
|
||
|
|
`Read bool`.
|
||
|
|
2. Write a function `NewBook(title, author string) *Book` that constructs
|
||
|
|
and returns a pointer to a `Book` (this is the exact "constructor"
|
||
|
|
pattern used throughout the main course — `NewXxx` returning `*Xxx`).
|
||
|
|
3. Write a method `func (b *Book) MarkAsRead()` that sets `Read = true`.
|
||
|
|
4. In `main`, create a book with `NewBook`, call `MarkAsRead()` on it, and
|
||
|
|
print the struct with `fmt.Printf("%+v\n", book)` to confirm `Read` is
|
||
|
|
now `true`.
|
||
|
|
|
||
|
|
Once this feels solid, move to Part 3 — interfaces, error handling,
|
||
|
|
slices/maps, packages, and a first look at goroutines and JSON, which
|
||
|
|
rounds out everything you need for Lesson 1.
|