On 19 August 2026 I wrote my first Go program. It printed three lines to a terminal. Three weeks later there are 83 files and about 2,300 lines of Go on my machine, and this post is the start of writing down what happened in between.
Everything here is real and public: github.com/shxwat/golang. Every snippet below is copied from that repo as it is, mistakes included.
Why Go
I wanted to understand what sits behind an application. Not the part a user sees, but the part that takes a request, talks to a database, does several things at once, and stays up while doing it.
Go pulled me in for a few reasons:
- The syntax is small enough to hold in your head
- Concurrency is built into the language, not bolted on
- It compiles fast enough that you stay in the loop
- The standard library covers most of what a backend needs
- It is what a lot of infrastructure is actually written in
What I did not expect to like as much as I do: Go pushes you toward the plain solution. There is usually one obvious way to write something, and it is usually the boring one.
Day 1: the first program
Here it is, exactly as I wrote it:
package main
import "fmt"
func main() {
fmt.Println("Hello Shashwat")
fmt.Println("Today 19Aug i started learning Go")
fmt.Println("I think it is Cool")
}
The actual file. Line three still has the space-indent I never went back to fix.
Nothing clever. But it taught me four things I have used in every file since:
- An executable program starts with
package main main()is where it begins- Packages like
fmtare imported and reused go runcompiles and runs in one step, fast enough that you keep trying things
The third line is indented with spaces while the others use a tab. I have left
it that way. gofmt would fix it in a second, and that is roughly how long it
took me to learn that gofmt exists.
Three weeks, in six files
The table at the end is the full count. These are the six files where something actually changed in my head, in the order I wrote them.
21 Aug: a pointer changes the original
Before this, every function I wrote got a copy. Then I passed an address instead of a value:
func updatedChocolateBox(chocolate *int) {
*chocolate = 50
fmt.Println("number of chocolate in updatedChocolateBox is:", *chocolate)
}
func main() {
chocolate := 10
updatedChocolateBox(&chocolate)
fmt.Println("number of chocolate in main is:", chocolate)
}number of chocolate in updatedChocolateBox is: 50
number of chocolate in main is: 50main started with 10 and ended with 50, and it never assigned anything
itself. &chocolate hands over where the value lives, and *chocolate = 50
writes there. The file still has two earlier attempts commented out above
this one, changeNum and updateName. This was the fourth try. It was the
one where it clicked.
22 Aug: the goroutine that never printed
My first goroutine:
func printHello() {
for i := 1; i <= 5; i++ {
fmt.Println("Hello from Goroutine -", i)
}
}
func main() {
go printHello()
for i := 1; i <= 5; i++ {
fmt.Println("Hello from main -", i)
}
}Hello from main - 1
Hello from main - 2
Hello from main - 3
Hello from main - 4
Hello from main - 5The goroutine did not print a single line. go printHello() starts it and
moves on immediately, and when main returns the program ends, along with
every goroutine still running. Nothing waits by default. Three days later the
folder had three files with waitgroup in the name.
24 Aug: defer runs backwards
func main() {
fmt.Println("1. Main starts")
defer fmt.Println("2. First Defer")
defer fmt.Println("3. second Defer")
defer fmt.Println("4. Third Defer")
fmt.Println("5. Main Ends!!")
}1. Main starts
5. Main Ends!!
4. Third Defer
3. second Defer
2. First DeferDeferred calls stack up and run last-in, first-out when the function returns.
It looked like a party trick. The next day it was the reason defer wg.Done()
sits at the top of every goroutine I write, and by 7 September defer mu.Unlock()
sat right under its Lock(). You write the cleanup next to the setup, and Go runs it at the end.
27 Aug: the interface I did not use
type Storage interface {
SaveUser(name string)
}
type MemoryDB struct {
Users []string
}
func (m *MemoryDB) SaveUser(name string) {
m.Users = append(m.Users, name)
fmt.Println("User saved safely in memory:", name)
}
func RegisterNewUser(db *MemoryDB, name string) {
db.SaveUser(name)
}It runs. It saves both users. And the interface does nothing at all.
Reading it again for this post, RegisterNewUser takes a *MemoryDB, not a
Storage. I defined the contract and then asked for the concrete type anyway.
The whole point is the signature RegisterNewUser(db Storage, name string):
then a Postgres store, a mock in a test, or anything else with a SaveUser
method can be passed in without touching the function. I did not see that on
27 August. I see it now, and that is the reason to keep the file as it is.
2 Sep: errors that keep their cause
I wrote ten files in the errors folder in one day. This is the one I
keep coming back to:
var ErrOutOfStock = errors.New("Item is out of stock")
func checkInventory(item string) error {
return ErrOutOfStock
}
func buyItem(item string) error {
err := checkInventory(item)
if err != nil {
return fmt.Errorf("cart operation failed: %w", err)
}
return nil
}
func main() {
err := buyItem("iphone")
if errors.Is(err, ErrOutOfStock) {
fmt.Println("The product is out of stock. Please try again later.")
} else {
fmt.Println("Unexpected error:", err)
}
}The product is out of stock. Please try again later.%w wraps the original error inside a new message instead of flattening it
into a string. Each layer can add its own context (cart operation failed: ...)
and the caller can still ask errors.Is what actually went wrong underneath.
Without the %w, that check fails and the out-of-stock case looks like any
other crash.
7 Sep: a program that simply stopped
func main() {
ch := make(chan string)
fmt.Println("Sender: preparing to send an item.")
ch <- "Here are the noodles."
go func() {
fmt.Println("Receiver: ready to receive the item.")
msg := <-ch
fmt.Println("Receiver: received ->", msg)
}()
fmt.Println("Sender: task completed.")
}Sender: preparing to send an item.
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan send]:
main.main()
/Users/shashwat/Golang/channels/unbuffered_channel_deadlock.go:30 +0x70
exit status 2An unbuffered channel has no room. A send blocks until someone is there to
receive, at that exact moment. Here main sends on line 30, but the receiver
is only started on the line after, which main never reaches. Nobody else is
running, so Go gives up and says so.
The fix is one move: start the receiver first, then send. The file next to it,
unbuffered_channel_handoff.go, does exactly that and works. I kept the broken
one because the broken one is what taught me something. (A few days later I
did it again in a real project, and
wrote a whole post about it.)
Where I have got to
Three weeks in, sorted by folder:
| Topic | Files |
|---|---|
| Basics: variables, types, loops, defer | 13 |
| Functions, multiple returns, variadics | 8 |
| Pointers and pointer receivers | 8 |
| Structs and methods | 4 |
| Interfaces | 4 |
| Maps | 2 |
Errors, wrapping, errors.Is |
11 |
| Goroutines, WaitGroup, Mutex | 10 |
Channels, buffered and unbuffered, select |
8 |
HTTP APIs with net/http, and PostgreSQL |
13 |
Some of these took several attempts. Pointers made sense on the fourth try. Unbuffered channels made sense only after I wrote a deadlock and had to sit and work out why the program had simply stopped.
Why write any of this down
Because the useful part is not the working code. It is the twenty minutes before it worked.
Look at the six files above. One is a goroutine that never ran, one an interface that is never used, one a program that dies on line 30. I have another where I built an error, forgot to return it, and let a bank account go to minus a thousand rupees. Those are the parts worth keeping, and they are exactly the parts that vanish once something works.
So this blog is a log, not a tutorial. Posts will cover:
- A concept I have just understood, and what finally made it click
- The code that got me there
- The bug I hit, and why it happened
- Backend projects as I build them
I am not writing these as an expert. I am writing them as someone three weeks in, who would rather have a record than a clean-looking résumé.
Next
The goal now is to stop writing standalone files and build one real thing.
What I want to learn next:
- Structuring a Go application into packages
context.Context: cancellation and timeouts- Testing, properly
- REST API design beyond a single handler
- PostgreSQL with transactions
- Authentication
- Docker, and getting it deployed
That is the plan. This is entry one.