Go (Golang) Complete Beginners Guide: Simple, Fast, and Production-Ready
Learn Go from scratch — variables, functions, structs, interfaces, goroutines, channels, and building a REST API. A practical guide to the language that powers Docker and Kubernetes.
Go was built at Google by people who were tired of waiting for C++ to compile. They wanted a language that was simple enough for large teams, fast enough for systems work, and had concurrency built in from day one. The result is a language that powers Docker, Kubernetes, Terraform, and a massive chunk of cloud infrastructure.
Here's the thing about Go — it's deliberately boring. No generics (well, they added them in 1.18, reluctantly). No exceptions. No inheritance. No operator overloading. Go has about 25 keywords. Python has 35. C++ has over 90. That simplicity is the feature.
Setup and Hello World
# Install Go from https://go.dev/dl/
# Verify installation
go version
# Create a new project
mkdir myproject && cd myproject
go mod init myproject
package main
import "fmt"
func main() {
fmt.Println("Hello, World!")
}
go run main.go
Every Go file belongs to a package. The main package with a main function is the entry point. fmt is the formatting package from the standard library. That's it. No build configuration, no bundler, no transpiler.
Variables and Types
Go is strongly typed with type inference. You declare variables two ways:
// Explicit type
var name string = "Alice"
var age int = 30
// Short declaration (inferred type) — used 90% of the time
name := "Alice"
age := 30
active := true
scores := []int{95, 87, 92}
Go has the types you'd expect — string, int, float64, bool, byte, rune (Unicode character). Arrays have fixed sizes; slices are the dynamic version you'll actually use.
// Slices — dynamic arrays
names := []string{"Alice", "Bob", "Charlie"}
names = append(names, "Diana")
// Maps — key-value pairs
ages := map[string]int{
"Alice": 30,
"Bob": 25,
}
ages["Charlie"] = 35
One thing that surprises newcomers: Go has zero values. An uninitialized int is 0, a string is "", a bool is false, a pointer is nil. No undefined, no null surprises — every variable has a known default state.
Functions — Multiple Return Values
Go functions can return multiple values. This is used everywhere, especially for error handling.
func divide(a, b float64) (float64, error) {
if b == 0 {
return 0, fmt.Errorf("division by zero")
}
return a / b, nil
}
result, err := divide(10, 3)
if err != nil {
log.Fatal(err)
}
fmt.Println(result) // 3.3333...
The (float64, error) return signature is the most common pattern in Go. A result plus an error. If the error is nil, the result is valid. If it's not nil, something went wrong. This replaces try/catch and it's everywhere.
Error Handling — The if err != nil Pattern
Let's be honest — this is the most controversial thing about Go:
file, err := os.Open("config.json")
if err != nil {
return fmt.Errorf("opening config: %w", err)
}
defer file.Close()
data, err := io.ReadAll(file)
if err != nil {
return fmt.Errorf("reading config: %w", err)
}
var config Config
err = json.Unmarshal(data, &config)
if err != nil {
return fmt.Errorf("parsing config: %w", err)
}
Yes, it's verbose. Yes, you'll write if err != nil hundreds of times. But there's a benefit — error handling is explicit and visible. You never wonder "can this function throw?" because the answer is always right there in the return type. After a while, you stop fighting it and start appreciating that error paths are never hidden.
Structs and Methods
Go doesn't have classes. It has structs with methods attached to them.
type User struct {
ID int
Name string
Email string
}
// Method on User (value receiver)
func (u User) DisplayName() string {
return fmt.Sprintf("%s (%s)", u.Name, u.Email)
}
// Method that modifies the struct (pointer receiver)
func (u *User) UpdateEmail(email string) {
u.Email = email
}
user := User{ID: 1, Name: "Alice", Email: "alice@example.com"}
fmt.Println(user.DisplayName())
user.UpdateEmail("alice@newdomain.com")
No inheritance. If you want shared behavior, use composition:
type Admin struct {
User // Embedded struct — Admin "has a" User
Permissions []string
}
admin := Admin{
User: User{ID: 1, Name: "Alice", Email: "alice@example.com"},
Permissions: []string{"read", "write", "admin"},
}
fmt.Println(admin.DisplayName()) // Works — promoted from embedded User
Interfaces — Implicit Implementation
This is Go's most elegant feature. Interfaces are satisfied implicitly — you don't declare that a type implements an interface. If it has the right methods, it qualifies.
type Writer interface {
Write(data []byte) (int, error)
}
// File implements Writer (has a Write method)
// Buffer implements Writer (has a Write method)
// Your custom type can too — no "implements" keyword needed
type Logger struct {
prefix string
}
func (l Logger) Write(data []byte) (int, error) {
fmt.Printf("[%s] %s", l.prefix, string(data))
return len(data), nil
}
// Logger is now a Writer — the compiler knows this without being told
func logMessage(w Writer, msg string) {
w.Write([]byte(msg))
}
This means you can write functions that accept interfaces, and any type that matches will work — even types from other packages that don't know your interface exists. It's powerful and keeps code decoupled.
Goroutines and Channels — Concurrency Made Simple
This is Go's killer feature. Goroutines are lightweight threads (thousands cost almost nothing). Channels let them communicate safely.
// Launch a goroutine — just add "go" before a function call
go func() {
fmt.Println("running concurrently")
}()
// Channels — typed pipes for goroutine communication
ch := make(chan string)
go func() {
time.Sleep(time.Second)
ch <- "done" // Send a value into the channel
}()
result := <-ch // Receive — blocks until a value arrives
fmt.Println(result)
Real example — parallel HTTP requests:
func fetchAll(urls []string) []string {
ch := make(chan string, len(urls))
for _, url := range urls {
go func(u string) {
resp, err := http.Get(u)
if err != nil {
ch <- fmt.Sprintf("error: %s", err)
return
}
defer resp.Body.Close()
ch <- fmt.Sprintf("%s: %d", u, resp.StatusCode)
}(url)
}
results := make([]string, len(urls))
for i := range urls {
results[i] = <-ch
}
return results
}
Worker pool pattern:
func workerPool(jobs <-chan int, results chan<- int, workers int) {
var wg sync.WaitGroup
for i := 0; i < workers; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for job := range jobs {
results <- process(job)
}
}()
}
wg.Wait()
close(results)
}
The combination of goroutines and channels makes concurrency patterns that would be painful in other languages almost trivial in Go. No thread pools, no mutexes (usually), no callback hell.
Building a REST API — Standard Library Only
Go's standard library is powerful enough that many production APIs use net/http without a framework:
package main
import (
"encoding/json"
"net/http"
)
type User struct {
ID int json:"id"
Name string json:"name"
}
var users = []User{
{ID: 1, Name: "Alice"},
{ID: 2, Name: "Bob"},
}
func getUsers(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(users)
}
func main() {
http.HandleFunc("/users", getUsers)
http.ListenAndServe(":8080", nil)
}
That's a working JSON API in 25 lines with zero dependencies. In my experience, the standard library handles most use cases. If you need routing with path parameters, chi or gorilla/mux are lightweight additions — but you don't need a full framework.
When Go Shines (and When It Doesn't)
Go shines for: CLI tools, REST APIs, microservices, DevOps tooling, network servers, anything that needs concurrency, anything where fast compilation matters (CI/CD pipelines). Go doesn't shine for: data science and ML (Python owns this), rapid prototyping (too much boilerplate for throwaway code), GUI applications, problems that need complex type systems (Rust or Haskell), heavy string processing and text manipulation.The sweet spot is backend services and infrastructure tools. If you're building something that receives requests, processes data, and returns responses — Go is hard to beat on the combination of performance, simplicity, and developer productivity.
Where to Go From Here
Go rewards practice more than theory. The language is small enough that you can learn the entire spec in a week. The real learning is in patterns — how to structure concurrent programs, how to handle errors cleanly, how to write idiomatic Go that other Go developers can read.
Build something real on CodeUp — interactive Go exercises where you write, compile, and run code in the browser. Start with the basics, work up to goroutines and channels. The concurrency model clicks much faster when you see it execute than when you read about it.