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