Go Introduction

Go, often called Golang, is an open-source, compiled, statically typed programming language created at Google to make building reliable, fast software simple at scale. It combines the quick compile times and readability of a scripting language with the performance and safety of a compiled one, and it treats concurrency as a first-class feature through goroutines and channels. Tools you may already know — Docker, Kubernetes, Terraform — are written in Go. This lesson explains what Go is, how its compiler and toolchain actually work, and gets you writing real, running Go code.

Overview: How Go Works

Go was designed starting in 2007 by Robert Griesemer, Rob Pike, and Ken Thompson at Google, open-sourced in 2009, and reached a stable 1.0 release in 2012 with a strong backward-compatibility promise that still holds today. The team was frustrated with slow C++ build times, tangled dependency graphs, and languages that made safe concurrent programming hard. The result is a deliberately small language — around 25 keywords — that compiles fast, produces a single self-contained binary, and bakes concurrency into the language itself.

Go is a compiled language, not interpreted or bytecode-based like Python or Java. Running go build invokes the Go toolchain’s compiler, which translates your source directly into native machine code for your target operating system and CPU, links in everything the program needs — including the Go runtime that manages memory allocation, garbage collection, and goroutine scheduling — and produces one standalone executable. There is no separate virtual machine, and no runtime needs to be installed on the machine that eventually runs the program; you just copy the binary over. That is a major reason Go is popular for command-line tools and backend services.

Go is also statically typed: every variable’s type is checked at compile time, catching a whole class of bugs before the program ever runs. Type inference via the := operator (covered below) means you rarely write types out explicitly, so it feels almost dynamically typed day to day. Memory is managed automatically by a concurrent garbage collector, so you never call free() like in C, but you also skip the overhead of a slow interpreter loop.

Modern Go code is organized into modules. A module is described by a go.mod file at your project root, created with go mod init <module-path>, which records the module’s name and the versions of any external packages it depends on. This replaced the older GOPATH workflow, where every project had to live inside one global workspace; modules let a project live anywhere on disk. Every program is built from packages: a package is a directory of .go files that all start with the same package declaration. The special package main marks an executable, and it must contain a function named main with no parameters and no return value — that is where execution begins.

You will use a handful of command-line tools constantly:

Command What it does
go run file.go Compiles and immediately runs a program, without leaving a binary behind
go build Compiles the package into a standalone executable
go fmt Rewrites source files into Go’s one canonical formatting style
go mod init Creates a go.mod file to start a new module
go vet Statically analyzes code for likely mistakes beyond what the compiler checks
go test Runs tests in files named *_test.go

Formatting is not a matter of taste in Go: gofmt rewrites every file into one shape — tabs for indentation, opening braces on the same line as the statement that introduces them. That brace rule is not just style: Go’s compiler automatically inserts semicolons at the end of certain lines based on the final token, so writing an opening brace on its own line actually breaks compilation, because a semicolon gets inserted right before it. Because of this rule, virtually every Go codebase you will ever read has the same shape.

Syntax

Every Go source file follows the same basic pattern:

package main

import "fmt"

func main() {
	// statements go here
	fmt.Println("Hello, Go!")
}
  • package main — declares which package this file belongs to; main is the special name that marks a runnable program.
  • import "fmt" — pulls in the standard library’s formatting package so you can call fmt.Println. Every import must be used, or the compiler rejects the file.
  • func main() — the entry point. When you run the compiled binary, execution starts here.
  • No semicolons — Go statements do not end with ; in source; the compiler inserts them automatically at line breaks.

Examples

Example 1: Hello, Go!

package main

import "fmt"

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

Save this as hello.go and run go run hello.go. The compiler builds a temporary binary, runs it, and fmt.Println writes the string followed by a newline to standard output.

Example 2: Variables and Types

package main

import "fmt"

func main() {
	var name string = "Gopher"
	age := 15
	height := 3.5

	fmt.Println("Name:", name)
	fmt.Println("Age:", age)
	fmt.Println("Height:", height)
	fmt.Println("Age next year:", age+1)
}
Name: Gopher
Age: 15
Height: 3.5
Age next year: 16

This shows Go’s two ways to declare a variable. var name string = "Gopher" spells out the type explicitly. age := 15 uses the short declaration operator, which is only legal inside a function body; the compiler infers the type (int) from the value on the right. Both forms produce statically typed variables — age cannot later be assigned a string.

Example 3: Multiple Return Values and Errors

package main

import (
	"errors"
	"fmt"
)

func divide(a, b float64) (float64, error) {
	if b == 0 {
		return 0, errors.New("division by zero")
	}
	return a / b, nil
}

func main() {
	result, err := divide(10, 2)
	if err != nil {
		fmt.Println("Error:", err)
	} else {
		fmt.Println("10 / 2 =", result)
	}

	result, err = divide(5, 0)
	if err != nil {
		fmt.Println("Error:", err)
	} else {
		fmt.Println("5 / 0 =", result)
	}
}
10 / 2 = 5
Error: division by zero

This is the idiomatic Go error pattern: a function that can fail returns its normal result plus a value of the built-in error interface, which is nil when nothing went wrong. There are no exceptions to throw or catch — the caller is required to look at err and decide what to do. The second call reuses the existing result and err variables with = instead of :=, since they were already declared above.

How Go Programs Start and Run

Understanding the path from source file to running program clarifies a lot of Go’s behavior:

  • Compile: go build (or go run, which does this and then executes the result) parses every .go file in the package, type-checks it, and translates it straight into native machine code — there is no intermediate bytecode.
  • Link: the compiler links in the Go runtime (the garbage collector and goroutine scheduler) and any imported packages, producing one self-contained binary with no external dependencies to install.
  • Package-level initialization: when the binary runs, package-level variables are initialized first, in dependency order.
  • init functions: if a package defines one or more func init() functions, they run next, in the order they appear in the source.
  • main.main: only after all of that does the runtime call your func main(). In Example 3, this is the point where divide(10, 2) is actually called.
  • Exit: when main returns, the program exits immediately — Go does not wait for any goroutines you may have started elsewhere to finish, which is why real programs coordinate goroutine shutdown explicitly (covered in the concurrency lessons).

Common Mistakes

Mistake 1: Discarding an error with _

Beginners often throw away the error return because the extra if feels like boilerplate:

value, _ := strconv.Atoi("abc")
fmt.Println(value * 2)

Here strconv.Atoi fails because "abc" is not a number, so value silently becomes 0 and the program prints 0 as if nothing went wrong — the real problem is swallowed. Always check the error:

package main

import (
	"fmt"
	"strconv"
)

func main() {
	value, err := strconv.Atoi("abc")
	if err != nil {
		fmt.Println("Conversion failed:", err)
		return
	}
	fmt.Println(value * 2)
}
Conversion failed: strconv.Atoi: parsing "abc": invalid syntax

Mistake 2: Accidental shadowing with :=

Using := inside a nested block creates a brand-new variable that only exists in that block, even if an outer variable has the same name:

package main

import "fmt"

func main() {
	x := 10
	if true {
		x := 20
		fmt.Println("inner x:", x)
	}
	fmt.Println("outer x:", x)
}
inner x: 20
outer x: 10

A programmer expecting x to become 20 everywhere is surprised: the inner x := 20 shadowed the outer variable instead of updating it. To modify the existing variable, use plain assignment (=) instead of a new short declaration:

package main

import "fmt"

func main() {
	x := 10
	if true {
		x = 20
		fmt.Println("inner x:", x)
	}
	fmt.Println("outer x:", x)
}
inner x: 20
outer x: 20

Best Practices

  • Run go fmt (or let your editor run it on save) so every file matches the language’s one canonical style.
  • Check every error where it occurs; don’t defer error handling to “later” or discard it with _ outside of a deliberate, documented reason.
  • Keep package names short, lowercase, and free of underscores — the package name plus its exported identifiers should read naturally, like strings.Split.
  • Run go vet alongside the compiler; it catches likely mistakes the type checker doesn’t, like malformed Printf format verbs.
  • Prefer small, focused functions and packages over large ones — Go’s fast compiler rewards splitting code into well-scoped units.
  • Use go mod init at the start of every new project instead of the legacy GOPATH layout.
  • Write tests in *_test.go files and run them with go test as you go, rather than only at the end.

Practice Exercises

  1. Write a complete program that declares a string, an int, and a bool variable (using whichever declaration form you like), then prints all three on one line with fmt.Println.
  2. Write a function square(n int) int that returns n * n, then call it from main inside a for loop for the numbers 1 through 5, printing each result. Expected output is five lines: 1, 4, 9, 16, 25.
  3. Write a function safeDivide(a, b int) (int, error) that returns an error instead of panicking when b is 0, and a normal quotient otherwise. Call it twice from main — once with a valid divisor and once with 0 — and print either the result or the error message.

Summary

  • Go is a compiled, statically typed language built at Google for fast builds, simple deployment, and safe concurrency.
  • go build/go run compile straight to native machine code and link in the Go runtime — no virtual machine, no separate runtime install.
  • Every program needs a package main and a parameterless, return-less func main() as its entry point.
  • Modules (go.mod, created with go mod init) are the modern way to organize dependencies, replacing the old GOPATH workflow.
  • gofmt enforces one formatting style for the whole language, and Go’s automatic semicolon insertion is why opening braces must stay on the same line.
  • Go has no exceptions: functions that can fail return an error value that callers are expected to check with if err != nil.
  • := inside a nested block declares a new, shadowed variable rather than reusing an outer one — use = when you mean to modify the existing variable.