Files

340 lines
9.7 KiB
Markdown
Raw Permalink Normal View History

2026-07-16 10:13:46 +03:30
# 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.