tutorialGoBackendProgramming

Go Series #1: Packages, Variables, Slices & Maps

Day 1, Session 1. How Go organizes code, the rules every Go file follows, and the data types you'll use in every program.

7 min read

Go was built at Google to solve a specific problem: large teams writing software that needs to compile fast, run fast, and stay readable years later. To do that, it made some strong choices — no classes, no exceptions, no implicit anything. This session covers the foundation: how Go organizes code, and the data structures you'll use constantly.

Packages — How Go Organizes Code

Every Go file starts with a package declaration. No exceptions.

package main

import "fmt"

func main() {
    fmt.Println("Hello, Go")
}

Run it: go run main.go

A packagePackageA named collection of Go files that share the same namespace. is a named group of .go files in the same directory. Two rules you'll feel immediately:

package main is the entry point. Every executable must have exactly one package main with exactly one func main(). Library packages use any other name.

Unused imports are a compile error. Go refuses to build if you import something you don't use. This keeps codebases clean by force, not convention.

import (
    "fmt"
    "math"
)
// Both must be used — or it won't compile

Grouped imports use parentheses. gofmt (Go's formatter) enforces import order: stdlib first, third-party next, your own packages last, separated by blank lines. You don't think about this — you just run gofmt.

Exported Names — Go's Visibility Rule

In other languages, you write keywords to control visibility. Go does it differently: a name is exported (public) if it starts with a capital letter.

Bandingkan dengan:
Python
class User: def __init__(self, name: str, email: str): self.name = name # public (convention only) self.email = email # public self._age = 0 # "private" by convention only @classmethod def new_user(cls, name: str, email: str): return cls(name, email) def _validate(self, email: str) -> bool: # "private" convention return len(email) > 0
Go
package user type User struct { Name string // exported — readable from other packages Email string // exported age int // unexported — private to this package } func NewUser(name, email string) User { // exported return User{Name: name, Email: email} } func validate(email string) bool { // unexported return len(email) > 0 }

The compiler enforces this — accessing an unexportedExported NameAny identifier starting with a capital letter is public — accessible from other packages. name from outside its package is a compile error, not a runtime warning. Capital letter = public. Lowercase = private. That's the entire system.

Variables & Types

Go is statically typedStatically TypedVariable types are checked at compile time, not while the program runs.. Every variable has a type the compiler knows at compile timeCompile TimeWhen your code is being translated into a runnable program — before it runs.. No runtime type surprises.

Bandingkan dengan:
Python
# Python name = "Nanda" age = 28 is_engineer = True count = None # no zero value — None until assigned label = None active = None
Go
// Short declaration — idiomatic inside functions name := "Nanda" age := 28 isEngineer := true // Explicit — used at package level or when declaring without value var count int // 0 var label string // "" var active bool // false

Two things that stand out: := is the idiomaticIdiomaticThe 'natural' or conventional way of writing code in a given language. way inside functions — Go infers the type, so you rarely write explicit types. And unlike most languages, Go has zero valuesZero ValuesGo's default values for variables — 0, "", false — instead of null. — every declared variable is automatically initialized: int0, string"", boolfalse. No null, no undefined.

Basic types:

var i int     = 42
var f float64 = 3.14
var s string  = "hello"
var b bool    = true

// Type conversion is always explicit
var x int = 10
var y float64 = float64(x) // no implicit casting

Control Flow

Go's control flow looks familiar, with a few deliberate differences.

for is the only loop — there's no while or do-while. for covers all three forms:

// Classic
for i := 0; i < 5; i++ { fmt.Println(i) }

// While-style
n := 1
for n < 100 { n *= 2 }

// Infinite
for { break }

if has no parentheses, and can have a short init statement:

if score >= 90 {
    fmt.Println("A")
} else if score >= 80 {
    fmt.Println("B")
}

// Init statement — x is scoped to the if block
if x := compute(); x > 0 {
    fmt.Println("positive:", x)
}

switch doesn't fall through by default — no break needed between cases:

switch day {
case "Saturday", "Sunday":
    fmt.Println("Weekend")
default:
    fmt.Println("Weekday")
}

Slices — Go's Everyday List

A sliceSliceA dynamic, resizable view into an array — Go's everyday list type. is Go's dynamic list. If you use Python lists, JS arrays, or Java's ArrayList, this is your equivalent — and the most-used data structure in Go.

langs := []string{"Python", "TypeScript", "Go"}
langs  = append(langs, "Rust") // always assign append back

fmt.Println(langs)       // [Python TypeScript Go Rust]
fmt.Println(len(langs))  // 4
fmt.Println(langs[1:3])  // [TypeScript Go]

Under the hood, a slice is a view into an array with a length and a capacity. append grows the capacity automatically when needed. Use make when you know the expected size upfront:

scores := make([]int, 0, 10) // length 0, capacity 10 — avoids reallocation
for i := 1; i <= 5; i++ {
    scores = append(scores, i*10)
}

range is the idiomatic way to iterate — it works on slices, maps, strings, and channels:

for i, lang := range langs {
    fmt.Printf("%d: %s\n", i, lang)
}

for _, lang := range langs { // _ discards the index
    fmt.Println(lang)
}

Maps — Key-Value Data

A mapMapA built-in key-value store — Go's equivalent of a dictionary or object. is Go's built-in key-value type. No imports needed.

roles := map[string]string{
    "nanda": "engineer",
    "alice": "designer",
}

roles["bob"] = "manager"   // add / update
delete(roles, "alice")     // remove

// Safe key check — always use the two-value form
role, ok := roles["unknown"]
if !ok {
    fmt.Println("not found")
}

The two-value value, ok := m[key] is important: accessing a missing key with just value := m[key] silently returns the zero value ("" for strings) — which can hide bugs. Always use ok when the key might not exist.

Maps must be initialized before writing:

var counts map[string]int  // nil — reads ok, writes panic
counts = make(map[string]int)
counts["visits"]++

Functions & Error Handling

This is where Go looks most different from languages you've used before.

Bandingkan dengan:
Python
def divide(a: float, b: float) -> float: if b == 0: raise ValueError("cannot divide by zero") return a / b try: result = divide(10, 2) print(result) # 5.0 except ValueError as e: print("Error:", e)
Go
func divide(a, b float64) (float64, error) { if b == 0 { return 0, errors.New("cannot divide by zero") } return a / b, nil } result, err := divide(10, 2) if err != nil { fmt.Println("Error:", err) return } fmt.Println(result) // 5

Go has no exceptions. Errors are plain return values — the convention is (result, error). The caller must handle the error explicitly; you can't silently ignore it (assigning to _ is the only way to discard it, and it's obvious when you do).

In real code, wrap errors with context using fmt.Errorf:

func getUser(id int) (*User, error) {
    user, err := db.Find(id)
    if err != nil {
        return nil, fmt.Errorf("getUser %d: %w", id, err)
    }
    return user, nil
}

%w wraps the original error so callers can inspect the chain with errors.Is() and errors.As(). At first this feels verbose. After a while, you realize you always know exactly where an error came from.

Go Ships With

Before any third-party library, check the standard library — it covers a lot:

| Package | What it does | |---|---| | fmt | Formatting and printing | | os | File system, env variables, exit | | net/http | HTTP server and client | | encoding/json | JSON encode/decode | | strings | String manipulation | | strconv | String ↔ number conversion | | sync | Mutex, WaitGroup, Once | | time | Time, duration, formatting | | testing | Unit test runner (built-in) | | errors | Error creation and wrapping |

No npm install, no pip install. These are available in every Go project by default.

Practice

Build this before Session 2:

// Role manager:
// 1. map[string]string for username → role
// 2. assign(m, user, role string) error — error if user already has a role
// 3. promote(m, user, newRole string) error — error if user doesn't exist
// 4. list(m) — print all users and their roles
//
// Stretch: rewrite using []struct{ Name, Role string } instead of a map

Key Takeaways

  • Every file belongs to a packagePackageA named collection of Go files that share the same namespace.. package main = executable entry point
  • Unused imports → compile error (not a warning)
  • Capital letter = exportedExported NameAny identifier starting with a capital letter is public — accessible from other packages. (public). Lowercase = unexported. No keywords
  • := is idiomatic — Go infers the type. Zero valuesZero ValuesGo's default values for variables — 0, "", false — instead of null. replace null
  • for is the only loop. switch doesn't fall through
  • SlicesSliceA dynamic, resizable view into an array — Go's everyday list type.: append(), range, make() for pre-allocation
  • MapsMapA built-in key-value store — Go's equivalent of a dictionary or object.: make() before writing, value, ok for safe reads
  • Errors are return values — no exceptions, no hidden control flow
  • The standard library covers HTTP, JSON, testing, and more out of the box

Next: Go Series #2 — Defer, Structs & Interfaces