The "tiny" systems language with a punch!
Don't let Go's simplicity fool you; it's a beast! Look at infra. Docker? That's Go.
After 10 years of coding, here’s one golden rule I’ve learned:
Don’t memorize syntax. That’s what Google’s for.
Instead, focus on the core ideas of a language, in its natural habitat..
For Python, that’s ML.
For Golang? Backend & Infra.
So that's what we’re doing here: core Golang concepts by building a server app.
I'll guide you step-by-step, from the basics to mutexes and concurrency.
This tutorial is for intermediate to advanced beginners.
If you’re already comfortable with Go fundamentals, you can skip ahead to Interface-Driven Design.
A TCP Server in Go
TCP is a reliable server connection. Let's get straight into it:
package main
import (
"fmt"
"net"
)
func OnConnect(conn net.Conn) {}
func main() {
ln, err := net.Listen("tcp", ":9000")
if err != nil {
panic(err)
}
fmt.Println("Server listening on :9000")
for {
conn, err := ln.Accept()
if err != nil {
fmt.Println("Accept error:", err)
continue
}
OnConnect(conn)
}
}
We bind a new TCP server on port 9000
:
ln, err := net.Listen("tcp", ":9000")
The line above encodes one of Go’s most important idioms: explicit error handling.
First Lesson: Go Error Handling
In Go, errors are values: you have to deal with them directly.
Almost every function returns a tuple: (value, error)
.
func CustFunc() (string, error) {
return "value", nil
}
To handle the error:
val, err := CustFunc()
if err != nil {
// handle error
}
// continue safely
fmt.Println(val)
If you want to be a gopher, you need to live this pattern.
Second Idiom: Short Variable Declarations
:= // declares & assigns in one shot
Third Idiom: Strong Typing
func OnConnect(conn net.Conn) {}
net.Conn
is your TCP socket. In backend systems, we usually buffer these connections, toss them into a queue or array for processing.
But we don’t store raw sockets. We wrap them into predictable, structured formats, aka objects.
User-Defined Types ("Objects" in Go)
In Go, that’s a struct
:
type Client struct {
Conn net.Conn
ID string
}
On connection:
func OnConnect(conn net.Conn) {
client := Client{Conn: conn, ID: conn.RemoteAddr().String()}
fmt.Println("New client:", client.ID)
// next: read from client
}
Now your connection now has a descriptive type we can easily pass around.
Reading From Connections
import "bufio"
func process(input string) string {}
func OnConnect(conn net.Conn) {
client := Client{Conn: conn, ID: conn.RemoteAddr().String()}
reader := bufio.NewReader(conn)
for {
line, err := reader.ReadString('\n')
if err != nil {
fmt.Println("Read error:", err)
break
}
response := process(line)
conn.Write([]byte(response + "\n"))
}
}
The Takeaway: The Go Standard Library is incredible. Learn it well.
-
bufio
helps us read the raw TCP stream. - We read strings, handle errors, process input, and write back responses.
Designing a Protocol
Since strings can represent anything (JSON, plain text, whatever), we need a protocol.
We’ll support just two commands: ECHO
and TIME
.
parts := strings.Fields(strings.TrimSpace(input)) // trim
cmd := strings.ToUpper(parts[0]) // split
switch cmd { // match
case "ECHO":
return strings.Join(parts[1:], " ")
case "TIME":
return time.Now().Format(time.RFC3339)
default:
return "ERR unknown command"
}
We trim, split, and pattern match.
Now the client request has meaning, and we can respond accordingly.
But Wait… It’s Blocking!
Concurrency: Goroutines
Right now, we can only handle one client at a time.
Solution? Concurrency.
In Go, it's this easy:
go OnConnect(conn)
Goroutines are lightweight "green threads" managed by Go's runtime. You can spawn hundreds of thousands of them.
But with concurrency comes great danger.
Synchronization: Locks & Mutexes
Imagine a busy intersection without traffic light. There's bound to be an accident.
Concurrency means multiple readers and writers, which brings race conditions.
Mutexes are traffic lights.
var (
clients = make([]Client, 0)
clientsMu sync.Mutex
)
func register(c Client) {
clientsMu.Lock()
clients = append(clients, c)
clientsMu.Unlock()
}
Now we safely register new clients.
Update OnConnect
:
func OnConnect(conn net.Conn) {
client := Client{Conn: conn, ID: conn.RemoteAddr().String()}
register(client)
// handle I/O
}
Broadcasting
Now that we store clients, we can broadcast messages:
func broadcast(msg string) {
clientsMu.Lock()
defer clientsMu.Unlock()
for _, c := range clients {
go c.Conn.Write([]byte("BROADCAST: " + msg + "\n"))
}
}
defer
ensures the mutex unlocks even if the function panics or returns early.
Clean Up After Yourself
Always close I/O devices in systems code:
func closeAll() {
clientsMu.Lock()
defer clientsMu.Unlock()
for _, c := range clients {
c.Conn.Close()
}
}
Wrapping Up
Now look at how much you’ve touched in just a short time:
- Imports & modules
- Functions & signatures
- Idiomatic error handling
- User-defined types (
struct
) - Bonus: Methods on structs:
func (c *Client) Print() {
fmt.Println(c.ID)
}
- Reading buffered data with
bufio
- String manipulation (
strings.TrimSpace
,strings.Fields
,strings.ToUpper
) - Goroutines for concurrency
- Slices
- Mutexes for synchronization
And notice, we never wasted time on variables, loops, numbers, or data types. Those are just syntax.
The fundamentals don’t change between languages, it's just syntax and the language's idioms
If you want to learn a language fast, skip the first 100 pages of the book and figure out what makes it tick.
I didn’t explain conditionals or loops, but you understood:
"Ahh, that’s a for
loop."
Top comments (2)
Your series is great ! Thanks for sharing , really interesting topics not mentioned in basic tutorials
Thank you! I am glad you enjoyed it! It was fun making it.🔥
Some comments may only be visible to logged-in visitors. Sign in to view all comments.