Go Intermediate
A Complete 3-Chapter Course
Table of Contents
- Structs in Full, Methods, and Pointers
- Interfaces
- Goroutines and Channels
Structs in Full, Methods, and Pointers
Fundamentals Chapter 7 previewed struct in passing. This chapter covers it properly: defining one, attaching behaviour to it with methods, and the pointer concept that decides whether a method can actually change the struct it's called on. Go has no class keyword and no this โ methods and pointers together are how Go achieves what JavaScript's object methods did with this (Fundamentals Chapter 7).
Defining and Creating a Struct
type Person struct { ... } defines a new named type with a fixed set of fields. Field access uses the same dot notation as JavaScript object properties โ but unlike a map (Chapter 7), a struct's fields are fixed at compile time: there's no way to add an unplanned field later, and each field can have its own distinct type.
Name and Age start with capital letters deliberately โ in Go, capitalisation controls visibility outside the current package (covered fully later). Lowercase struct fields still work everywhere in this course's single-file examples, but capitalised fields are the convention for anything meant to be used elsewhere.
Methods โ Functions Attached to a Type
(p Person) between func and the method name is the receiver โ it's what makes this an ordinary function into a method that can be called as p.Greet(). p inside the method body plays the same role JavaScript's this played inside an object method (Fundamentals Chapter 7) โ but it's an explicit, named parameter here, not an implicit keyword.
Pointers โ Why Greet() Can't Change p, But Something Else Can
&age ("address of") produces a pointer โ a value that points to where age actually lives in memory, rather than a copy of 35 itself. *agePointer ("dereference") goes the other way: follow the pointer back to the real value. This is a genuinely new concept with no real JavaScript equivalent โ every JavaScript variable is already accessed "directly," with no separate address-vs-value distinction to think about.
Why This Matters for Methods
A receiver of type Person (no *) receives a brand-new copy of the struct โ any changes inside the method vanish once it returns, the value-passing behaviour every Go function has by default. A receiver of type *Person (a pointer receiver) receives a pointer to the original struct instead, so changes made through it persist. Go automatically handles the & when calling person.HaveBirthdayPointer() โ no manual &person.HaveBirthdayPointer() needed.
p.Field and that change should be visible afterward, the receiver must be *Person, not Person.
| JavaScript | Go |
|---|---|
| { name: "Philip", age: 35 } | type Person struct { Name string; Age int } |
| greet: function() { ...this... } | func (p Person) Greet() string { ...p... } |
| this (implicit) | Receiver variable (explicit, named by you) |
| Objects are always reference-shared | Plain receiver = copy; pointer receiver (*Type) = shared |
| No address/value distinction | &value (address), *pointer (dereference) |
Coding Challenges
Define a struct Book with fields Title (string), Pages (int). Create one, then write a method (b Book) Describe() string that returns a sentence combining both fields. Print the result of calling Describe().
๐ View solutionDefine a struct Account with field Balance (float64). Write a method (a *Account) Deposit(amount float64) using a pointer receiver that adds amount to Balance. Create an account, call Deposit twice with different amounts, and print Balance after each call to confirm it actually changes.
๐ View solutionWrite a function double(n *int) that takes a pointer to an int and doubles the value it points to (using dereferencing, not a return value). Declare a variable, pass its address to double, and print the variable afterward to confirm it changed.
๐ View solutionChapter 1 Quick Reference (Intermediate)
- type Name struct { Field type } โ defines a fixed-shape record type
- func (receiver Type) MethodName() { } โ attaches a method to a type
- &value โ gets a pointer (the address) to a value
- *pointer โ dereferences a pointer, accessing/changing the real value
- Plain receiver (p Type) โ method gets a copy; changes don't persist
- Pointer receiver (p *Type) โ method gets the original; changes DO persist
- Go calls automatically handle & when invoking a pointer-receiver method on a plain variable
- Next chapter: interfaces โ how Go achieves polymorphism without classes or inheritance
Interfaces
Chapter 1 gave Person a method. This chapter asks: how does code write a function that works on any type that has a particular method, without caring exactly what that type is? Go's answer is the interface โ and Go's interfaces work in a way that has no real JavaScript equivalent, since JavaScript has no static types to satisfy in the first place.
Defining an Interface
An interface lists method signatures only โ no implementation, no fields. Shape says: "anything with an Area() float64 method counts as a Shape." Nothing else about the type matters.
Implementing an Interface โ Implicitly
Neither Circle nor Rectangle mentions Shape anywhere. There's no implements Shape keyword in Go at all โ a type satisfies an interface automatically, just by having every method the interface requires, with matching signatures. This is called structural typing: it's about shape, not declared relationship.
Writing a Function That Accepts the Interface
describe takes a Shape โ it works on Circle, Rectangle, or any future type with an Area() method, all through one function. This is polymorphism: one piece of code, many concrete types, achieved here with no class hierarchy, no extends, and no inheritance chain at all.
A Slice of an Interface Type
[]Shape can hold a mix of any concrete types that satisfy Shape โ Circle and Rectangle side by side in the same slice, the same loop, the same Area() call. This is the most common real-world use for interfaces: a heterogeneous collection processed uniformly.
The Empty Interface and any
any (an alias for the older interface{}) is an interface with zero required methods โ literally everything satisfies it, since there's nothing to satisfy. This is the closest Go gets to JavaScript's "accepts anything" flexibility, but it's used sparingly: reaching for any too often throws away the type-checking that makes Go, Go.
any doesn't know what's actually inside without a separate runtime check (a "type assertion" or "type switch," beyond this chapter's scope). Reaching for any as a default habit defeats the purpose of Go's type system โ it should be the exception, not the rule.
| Concept | JavaScript | Go |
|---|---|---|
| Shared behaviour across types | Duck typing โ works at runtime if methods exist | Interfaces โ checked at COMPILE time |
| Declaring a relationship | class X extends Y / implements Z | Nothing โ satisfied implicitly |
| "Accepts anything" | Default behaviour (no static types) | any โ explicit opt-out, used sparingly |
Coding Challenges
Define an interface Speaker with a method Speak() string. Define two structs, Dog and Cat, each with a Speak() method returning an appropriate sound. Write a function announce(s Speaker) that prints the result of Speak(), and call it with both a Dog and a Cat.
๐ View solutionUsing the Shape interface, Circle, and Rectangle from this chapter, create a slice []Shape containing at least 3 shapes (mixing circles and rectangles), then loop over it printing each one's area. Add up all the areas into a single total, printed at the end.
๐ View solutionWrite a function printAll(items []any) that takes a slice of any and prints each item on its own line. Call it with a slice containing a mix of an int, a string, and a Circle value.
๐ View solutionChapter 2 Quick Reference
- type Name interface { Method() returnType } โ lists required method signatures only
- No "implements" keyword โ a type satisfies an interface just by having matching methods
- Structural typing โ the relationship is about shape, never declared explicitly
- func f(x InterfaceType) โ accepts ANY concrete type that satisfies the interface
- []InterfaceType โ a slice that can mix different concrete types together
- any (interface{}) โ zero required methods; satisfied by literally everything
- Polymorphism in Go โ achieved via interfaces, with no classes or inheritance chains
- Next chapter: goroutines and channels โ Go's built-in approach to concurrency
Goroutines and Channels
Fundamentals Chapter 10 covered JavaScript's async/await โ useful for waiting on one thing at a time, like a single network request, without ever running two pieces of JavaScript truly simultaneously. Go's concurrency is fundamentally different: a Go program can run genuinely many things at once, using goroutines, and channels to let them communicate safely.
Starting a Goroutine with go
The keyword go placed before any function call launches it as a goroutine โ a lightweight, independently-running unit of execution โ and immediately continues with the next line, without waiting for it to finish. This is similar in spirit to JavaScript's "doesn't block" behaviour from Chapter 10's setTimeout, but a goroutine genuinely can run in parallel with everything else, on multi-core hardware.
main() returns before a goroutine finishes, the goroutine is simply abandoned โ there is no equivalent of JavaScript's event loop keeping things alive until they're done. The time.Sleep above is a crude workaround used only for this introductory example; real code uses the synchronization tools below instead.
Channels โ Sending Values Between Goroutines
make(chan int) creates a channel โ a typed pipe that goroutines use to send and receive values safely. results <- n*n sends a value in; <-results receives one out. Receiving from a channel blocks โ the receiving code waits patiently until a value actually arrives, which is exactly what removes the need for time.Sleep guesswork from the first example.
Running Several Goroutines and Collecting Results
make(chan int, len(numbers)) creates a buffered channel that can hold several values without anything having to receive immediately โ letting all three goroutines send without waiting on each other. The receiving loop then collects exactly as many results as goroutines were launched. The order they arrive in is not guaranteed โ whichever square finishes first sends first.
sync.WaitGroup โ Waiting for a Group of Goroutines
WaitGroup is the real-world tool for "wait until everything launched is finished" โ Add(1) registers one more goroutine to wait for, each goroutine calls Done() when finished, and Wait() blocks until the count returns to zero. defer wg.Done() guarantees Done() runs no matter how the function exits, even if it returns early or panics.
defer schedules a statement to run right before its surrounding function exits, regardless of which return path is taken. It's used constantly alongside resources that need cleanup (closing a file, unlocking something) โ wg.Done() here is the most common beginner-facing example.
| JavaScript | Go |
|---|---|
| Single-threaded event loop, never truly parallel | Goroutines genuinely run in parallel on multi-core hardware |
| await / .then() to wait for a result | <-channel blocks until a value arrives |
| Promise.all([...]) to wait for several | sync.WaitGroup โ Add/Done/Wait |
| No raw threads exposed to the developer | go keyword launches a real concurrent goroutine directly |
Coding Challenges
Write a function cube(n int, results chan int) that sends nยณ into results. In main, create an unbuffered channel, launch cube(4, results) as a goroutine, receive the value, and print it.
๐ View solutionGiven numbers := []int{1, 2, 3, 4, 5}, launch a goroutine per number that sends its square into a buffered channel sized to len(numbers). Receive all 5 results in a loop, summing them into a total printed at the end.
๐ View solutionWrite a function worker(id int, wg *sync.WaitGroup) that prints "Worker started" and "Worker finished" with the id, using defer wg.Done(). Launch 4 workers using a sync.WaitGroup, then print "All done" only after wg.Wait() returns.
๐ View solutionChapter 3 Quick Reference
- go functionCall() โ launches a goroutine; continues immediately without waiting
- make(chan Type) โ unbuffered channel; sending/receiving blocks until matched
- make(chan Type, n) โ buffered channel; can hold up to n values without blocking
- channel <- value โ send; <-channel โ receive (blocks until a value arrives)
- sync.WaitGroup โ Add(n) to register, Done() when finished, Wait() to block until all are done
- defer statement โ runs just before the surrounding function returns, however it returns
- main() does NOT wait for goroutines automatically โ use channels or WaitGroup, not Sleep, in real code
- This completes this Go course's planned chapters. Advanced topics (generics, context, testing) would follow in a Course 3.