Skip to content
gohacks.dev
Basics

Go Basics for Developers Coming From Another Language

A learning path for Go if you already program in another language: what to learn in what order, what to skip at first, and the traps that catch newcomers.

23 min read
Summarize in AI
Flat illustration of an editor window full of code.

You already know how to program, so what you need from a Go guide is not an explanation of variables. It is a map: what is different here, in what order to learn it, what to ignore until later, and which mistakes everyone makes in the first month. This page is that map, and it is also the hub for the basics section of this site, so each stop on the route links to a short article that goes deeper on one thing.

Tested with Go 1.25.6, macOS on Apple Silicon (arm64)

I have written Go professionally for years, after a long time with PHP, Java and JavaScript, and I have watched developers from all three pick it up. The pattern is consistent. The syntax takes a day. The standard library takes a few weeks. What takes months is unlearning habits, such as reaching for inheritance, throwing exceptions, or treating a slice as if it were a list. The route below is arranged so those habits break early and cheaply.

A note on how to use it. Do not read this top to bottom and call it learned. Read a section, open the linked article, type the code, change it until it fails, and move on. Everything here was run on Go 1.25.6, and the articles show real output. If you are coming from a particular language, there are also dedicated guides for Python developers, Java developers, JavaScript developers, PHP developers and C developers, and I recommend reading yours alongside this one.

What Go is, and what it is for

Go is a compiled, statically typed language with garbage collection, built at Google and released as open source in 2009. It aims at one thing: making it cheap to write, read, build and deploy servers and command line tools that stay understandable as teams and codebases grow. That aim explains most design choices, including the ones that look like omissions. There is one way to format code. There is no inheritance. The language has few keywords, and the specification is short enough to read in an evening. The compiler is fast, the output is a single static binary, and cross-compiling to another operating system is an environment variable.

That makes Go a good fit for network services, APIs, command line tools, infrastructure software (Docker, Kubernetes and Terraform are written in it), data pipelines and anything that benefits from cheap concurrency. It is a weaker fit for code that leans on a rich ecosystem of numerical libraries, for graphical applications, and for work where the expressiveness of a language with macros or pattern matching is the point. Knowing that up front saves you from judging Go by the wrong task.

The trade-offs are worth stating plainly, because the language does not hide them. You get simplicity and predictable performance. You pay with verbosity: no exceptions, no ternary operator, few abstractions, and a style that rewards writing the obvious loop over the clever one-liner. Generics arrived in 1.18 and are deliberately modest. If a language with more features is what you want, Go will feel like a tight shoe for a while. Most developers tell me the feeling passes once they have read their first large Go codebase written by someone else and found it easy to follow.

Step one: install it and run something

The setup is short. Installing Go covers macOS, Linux and Windows, how to verify the install, and what the toolchain manages for you, including downloading a newer Go version automatically when a module asks for it. Then write your first program and read every line of it, because the three lines of boilerplate (a package clause, an import and a main function) introduce ideas the rest of the language builds on.

Before you write more than one file, understand how code is organised. GOPATH and Go modules explains the history and the present. Today a project is a module, a directory tree with a go.mod file at the root that names the module and lists its dependencies, and you create one with go mod init. You do not need to think about GOPATH any more, but old tutorials mention it and you should know why you can ignore them. The commands you will use daily are few: go run to compile and run, go build to produce a binary, go test to run tests, go vet to catch suspicious code, and go fmt to format.

Run gofmt from day one and never argue with it. gofmt explains what it does and how to make your editor run it on save. Formatting disputes are the biggest time sink in many teams, and Go removes them by having exactly one answer.

Step two: values, types and the zero value

Go is statically typed, but you rarely write the types out, because := infers them. The first idea to absorb is the zero value: every variable has a defined value from the moment it exists, 0 for numbers, the empty string for strings, false for booleans and nil for pointers, slices, maps, channels, functions and interfaces. There is no "uninitialised" state to guard against. Zero values shows the full table and why the design makes so much code shorter, and the useful zero value shows how to design your own types so that the zero value is ready to use, as sync.Mutex and bytes.Buffer are.

Two small things catch people early. The first is shadowing: a := inside a block creates a new variable that hides the outer one, and the classic bug is an err that is quietly redeclared in an if and never reaches the code that checks it. Variable shadowing shows the cases and the linter that catches them. The second is constants. Iota is how Go writes enumerated constants, and untyped constants explains why const x = 1 << 40 can be used as a float or an int64 without a conversion, which is surprising until you know the rule.

Numbers deserve more care than they get. Integers wrap silently on overflow, with no panic and no warning, and integer overflow shows when that bites. Floating point behaves as in every other language, and float precision explains why 0.1 + 0.2 is not 0.3 and what to use for money (integers of the smallest unit, or a decimal library). Conversion between numeric types is always explicit, so type conversion is worth reading once; the compiler error you will see when you forget is explained in mismatched types int and float64. For bit manipulation, there is a short guide to the bitwise operators.

Step three: strings, bytes and runes

If you take one topic slowly, make it strings. A Go string is an immutable sequence of bytes, usually holding UTF-8 text. That single sentence has consequences. len(s) counts bytes, not characters. Indexing a string gives a byte. Ranging over a string gives runes (Unicode code points) and decodes UTF-8 as it goes. Strings and bytes and runes and bytes show each of these with output, including the length of a string with an accented letter or an emoji, and they save you from the bug where truncating a string at a byte offset cuts a character in half.

The practical rules I follow: use strings.Builder when you build a string in a loop, use []rune only when you need to index by character, and use the unicode/utf8 package to count them. The standard library has a function for almost everything else, and the habit of looking in strings, bytes, strconv and unicode before writing your own will pay back all year.

Step four: arrays, slices and maps

This is the part of the language where intuition from other languages misleads most. An array has a fixed length that is part of its type, and it is copied when assigned or passed. You will rarely use one directly, but arrays explains them because slices are built on top. A slice is a small descriptor, a pointer, a length and a capacity, that views part of an array, and understanding that one sentence resolves most of the confusion. Slices explained is the article to read twice.

The consequences come in a handful of well-known traps:

  • Appending may or may not reallocate, so two slices can share storage or not, depending on capacity. The append gotcha shows the case where changing one slice changes another.
  • Copying a slice variable copies the descriptor, not the elements. Copying slices shows copy and slices.Clone and when each is the right tool.
  • Removing an element is not a built-in operation. Removing an element from a slice gives the idioms, including slices.Delete and the version that preserves order.
  • A sub-slice keeps the whole backing array alive, and can be appended into by accident. The three-index slice is the tool that limits its capacity.

A map is a hash table. Reading a missing key returns the zero value, so you need the two-value form to tell absent from zero; checking whether a key exists shows it. Iteration order is deliberately randomised, and map iteration order explains the reason and how to iterate in sorted order. A nil map can be read but not written, which produces a panic that every Go developer meets once; the nil map assignment panic explains how to read it. The full overview is maps explained. Also note that maps are not safe for concurrent use, which becomes important in the concurrency section.

For sorting and searching, learn sort.Slice and the newer slices package early. Between the slices and maps packages that came with Go 1.21 and the built-in min, max and clear (the newer built-ins), a lot of code that needed a helper function a few years ago is now one call.

Step five: structs, methods and pointers

Go has no classes. A struct groups fields, you attach methods to any named type, and that is the entire object model. Structs covers declaration and literals, struct tags explains the annotations that encoding/json and others read, and struct embedding shows Go's answer to inheritance: composition, where the fields and methods of an embedded type are promoted to the outer one. It looks like inheritance and is not, and the difference matters when you try to use an embedded type as a base class.

Pointers are simpler than in C, with no arithmetic, and still need a clear head. A function receives copies of its arguments, so changing a struct inside a function changes the copy unless you pass a pointer. Pointers explained is the start. Methods can have a value receiver or a pointer receiver, and the choice has rules: pointer versus value receivers gives the ones I use, and the compile error about a pointer receiver is what you see when you get one wrong. Whether a function should return a struct or a pointer to it is covered in returning a pointer or a value, with benchmarks that usually show the difference is smaller than the folklore says. Also useful: comparing structs, comparable types and shallow copies of structs.

For creating values properly, constructor functions shows the NewX convention, and functional options shows the pattern for configurable constructors that do not need a dozen parameters. You will see it in many libraries.

Step six: interfaces

Interfaces are where Go most differs from Java and C#, and learning them well is the highest-value hour of this guide. An interface is a set of method signatures, and a type satisfies it by having those methods, with no implements declaration anywhere. Interfaces explained covers the mechanics.

The idiom that follows from that is to define small interfaces where they are consumed, not where they are implemented. A function that needs to read bytes takes an io.Reader, one method, and then files, network connections, buffers and test fixtures all work without any of them knowing about the function. Accept interfaces, return structs explains the proverb and when it does not apply. The standard library's io, fmt and sort packages are the best teachers: small interfaces, many implementations.

Three traps accompany them. An interface value holding a nil pointer is not itself nil, which breaks if err != nil in a notorious way; the nil interface that is not nil is essential reading. You use type assertions and type switches to get back to a concrete type, and the empty interface and any to hold anything, which is a tool you should reach for rarely now that generics exist. For the error-handling side of interfaces, the built-in error is just an interface with one method, which is the subject of the error handling guide.

Step seven: functions, closures and defer

Functions are values. They can be assigned, passed and returned, and a function literal that refers to variables of its enclosing function is a closure. Closures shows the idiom and the pitfall that the loop-variable change in Go 1.22 mostly removed; the loop variable change explains what the old behaviour was, because you will meet it in older code. Multiple return values are the basis of error handling, and multiple return values shows the convention and the named-result variant. Variadic functions covers ... in both directions.

defer schedules a call to run when the surrounding function returns, and it is the idiomatic way to release a resource right next to the line that acquired it. Defer order shows the last-in, first-out behaviour and when the arguments are evaluated, and defer in a loop shows the memory leak that appears when you defer inside a long loop and the fix.

Step eight: control flow

Go has one loop keyword, for, in several shapes. Range covers iterating over slices, maps, strings, channels and integers, and range over functions covers the iterator support added in Go 1.23, which lets a type offer its own sequence. The switch statement has no fall-through by default and no need for break, which surprises C programmers; switch goes through the forms. Labelled breaks and continues for nested loops are in break with labels, and the rarely used goto has a short article of its own, mostly so you can decide it is not for you.

Step nine: packages, names and style

A package is a directory of files that share a name, and the unit of reuse and of visibility. A name that starts with an uppercase letter is exported; anything else is private to the package. That is the whole access-control system, explained in packages and exported names. Naming matters more in Go than in most languages because it is the main documentation tool: package naming gives the rules (short, lowercase, no underscores, no util), and naming conventions covers variables, receivers and acronyms. A function called init runs automatically before main, and the init function explains when that is fine and when it is a trap. Comments become documentation when you follow the format in doc comments, and they show up on pkg.go.dev with no extra work.

There is a body of accumulated style guidance, and the short versions are worth reading together: idiomatic Go describes what experienced Go developers mean by the word, and Go proverbs collects the sayings with an explanation of each. Do not memorise them. Read them again after you have written a few thousand lines, and they will make a different kind of sense.

Step ten: formatting, printing and the standard library

Output and input are the first library skills. Printf verbs covers the formatting directives, and printing structs with %v shows %v, %+v and %#v, which are the three you will use for debugging. Reading user input, command line arguments, reading files and environment variables are the building blocks of any command line tool. For dates and durations, time.Duration and time formatting are the two with the most surprises, including the reference date that replaces strftime patterns. For random numbers, random numbers in Go explains math/rand/v2 and when you need crypto/rand instead. And the blank identifier and shadowing a package name explain two small things that appear in the first day of reading other people's code, along with the list of built-in functions.

A few of those pages teach more than their size suggests. The standard library is the best Go code you can read, and the habit of opening the source of a function you are about to use, which is one click away on pkg.go.dev, is the quickest way to absorb the idioms. Reading Go code describes how I approach an unfamiliar codebase, with the specific tools (go doc, go list, the references view of your editor) that make it fast.

Step eleven: the compiler errors you will meet

Beginners are told to read the error message, and the advice is only as good as the messages. Go's are usually clear, but a handful look odd until you have seen them once. Declared and not used is the first (the compiler refuses unused variables and imports, which annoys everyone for exactly a week). Cannot use as type in assignment is the type mismatch in its many forms. Missing return is the compiler failing to see that every path returns, and undefined is the one you get for a typo, a missing import or a file in the wrong package. When the message is not enough, the pattern is the same: find the line, find what the compiler thinks the type is, and compare it with what you think it is.

Step twelve: idioms that make code Go

Once the basics work, a second layer of idiom makes code look like Go. Enums shows that the language has none and how a typed constant with iota plays the part, including a String method through the Stringer interface. Type aliases versus type definitions clears up a confusion that bites during refactors. Build tags and file name suffixes for OS and architecture are how a codebase has platform-specific files without preprocessor macros.

Step thirteen: how the runtime behaves

You can write Go for a long time without understanding the runtime, and you will write it better once you do. The Go memory model states what concurrent code may rely on. Garbage collector basics describes the concurrent mark and sweep collector at the level a programmer needs, including the GOGC and GOMEMLIMIT knobs. Stack and heap explains escape analysis, and how to see the compiler's decisions with -gcflags=-m, which is the quickest way to learn why an allocation happens. Runtime basics ties the pieces together. None of it is needed on day one, and all of it is helpful by month three.

A program that uses the core of the language

Reading about features is not the same as using them together. This small inventory program uses a struct, a map, a slice, methods, an interface, sentinel errors, error wrapping and sorted output, which is most of what the sections above describe:

inventory.goGo
package main

import (
	"errors"
	"fmt"
	"sort"
)

var (
	ErrNotFound     = errors.New("item not found")
	ErrInsufficient = errors.New("not enough stock")
)

// Item is one product line. Prices are kept in cents to avoid float rounding.
type Item struct {
	Name       string
	Qty        int
	PriceCents int
}

// Describer is satisfied by anything that can describe itself in one line.
type Describer interface {
	Describe() string
}

func (i Item) Describe() string {
	return fmt.Sprintf("%-6s x%-2d $%d.%02d", i.Name, i.Qty, i.PriceCents/100, i.PriceCents%100)
}

// Store keeps items by name. The zero value is not ready, so use NewStore.
type Store struct {
	items map[string]Item
}

func NewStore() *Store { return &Store{items: make(map[string]Item)} }

func (s *Store) Add(name string, qty, cents int) {
	it := s.items[name] // a missing key yields the zero Item
	it.Name = name
	it.Qty += qty
	it.PriceCents = cents
	s.items[name] = it
}

func (s *Store) Remove(name string, qty int) error {
	it, ok := s.items[name]
	if !ok {
		return fmt.Errorf("remove %q: %w", name, ErrNotFound)
	}
	if it.Qty < qty {
		return fmt.Errorf("remove %d of %q (have %d): %w", qty, name, it.Qty, ErrInsufficient)
	}
	it.Qty -= qty
	s.items[name] = it
	return nil
}

// Report lists the items sorted by name, because map order is random.
func (s *Store) Report() []string {
	names := make([]string, 0, len(s.items))
	for name := range s.items {
		names = append(names, name)
	}
	sort.Strings(names)

	lines := make([]string, 0, len(names))
	for _, name := range names {
		var d Describer = s.items[name]
		lines = append(lines, d.Describe())
	}
	return lines
}
main.goGo
package main

import (
	"errors"
	"fmt"
	"strings"
)

func main() {
	s := NewStore()
	s.Add("apple", 10, 50)
	s.Add("pear", 4, 75)
	s.Add("apple", 5, 55) // adds to the existing line and updates the price

	for _, step := range []struct {
		name string
		qty  int
	}{{"apple", 3}, {"kiwi", 1}, {"pear", 9}} {
		if err := s.Remove(step.name, step.qty); err != nil {
			switch {
			case errors.Is(err, ErrNotFound):
				fmt.Println("missing:", err)
			case errors.Is(err, ErrInsufficient):
				fmt.Println("short:  ", err)
			}
		}
	}

	fmt.Println(strings.Join(s.Report(), "\n"))
}
Text
missing: remove "kiwi": item not found
short:   remove 9 of "pear" (have 4): not enough stock
apple  x12 $0.55
pear   x4  $0.75
store_test.goGo
package main

import (
	"errors"
	"testing"
)

func TestRemove(t *testing.T) {
	tests := []struct {
		name    string
		item    string
		qty     int
		wantErr error
		wantQty int
	}{
		{"enough stock", "apple", 3, nil, 7},
		{"exactly all", "apple", 10, nil, 0},
		{"too many", "apple", 11, ErrInsufficient, 10},
		{"unknown item", "kiwi", 1, ErrNotFound, 0},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			s := NewStore()
			s.Add("apple", 10, 50)
			err := s.Remove(tt.item, tt.qty)
			if !errors.Is(err, tt.wantErr) {
				t.Fatalf("err = %v, want %v", err, tt.wantErr)
			}
			if tt.item == "apple" && s.items["apple"].Qty != tt.wantQty {
				t.Errorf("qty = %d, want %d", s.items["apple"].Qty, tt.wantQty)
			}
		})
	}
}
Text
=== RUN   TestRemove
=== RUN   TestRemove/enough_stock
=== RUN   TestRemove/exactly_all
=== RUN   TestRemove/too_many
=== RUN   TestRemove/unknown_item
--- PASS: TestRemove (0.00s)
    --- PASS: TestRemove/enough_stock (0.00s)
    --- PASS: TestRemove/exactly_all (0.00s)
    --- PASS: TestRemove/too_many (0.00s)
    --- PASS: TestRemove/unknown_item (0.00s)
PASS
ok  	example.test/lab	1.266s

Read it for the decisions, not the syntax. The money is in cents because a float cannot hold 0.10 exactly. The sentinel errors are wrapped with %w, so the caller tests with errors.Is and still gets a message that says which item and how many. The method that reports sorts the keys, since ranging over a map would print in a different order on each run. Describer has a single method and is satisfied by Item without any declaration. The test is table-driven, the Go idiom for covering many cases with one loop and one failure message. Change one thing at a time (make Remove use a pointer map value, delete the sort, drop the %w) and watch what breaks, because that is the quickest way to feel the language.

Step fourteen: errors, tests and then concurrency

The two pieces that turn exercises into real programs are error handling and testing. Errors in Go are values that you check, wrap and return, and the guide is Go error handling, with the cluster of articles behind it on wrapping, sentinel errors, custom error types and the runtime panics you will meet as a beginner. If the repetition of if err != nil bothers you, you are not alone, and the honest answer is that it stops bothering you when you start treating each check as the place where you decide what the failure means.

Tests are in the standard library: a file ending in _test.go, a function starting with Test, and go test. The table-driven style above is the default, and -race adds the race detector for concurrent code. Write tests as you learn; they make experiments repeatable.

Only after those two should you learn goroutines and channels. The concurrency model is Go's best-known feature and the most dangerous to learn early, because a goroutine multiplies every misunderstanding about slices, maps and errors that you still have. When you are ready, start with Go concurrency patterns, the hub for that section, then goroutines and channels.

What to skip at first

Part of learning a language quickly is knowing what to leave alone. These are real parts of Go that you will meet, and none of them belongs in your first month.

Generics. Type parameters arrived in Go 1.18 and are useful for container types and for helpers like Map and Filter, but most application code needs few of them. Write the concrete version first. If you find yourself copying a function three times for three types, that is the signal, and the cost of waiting is zero.

The reflect package. It powers encoding/json and similar libraries, and application code should almost never call it. If you are reaching for reflection, a plain interface or a code generator is usually the cleaner answer.

unsafe and cgo. They exist for interop and for the rare performance hack. A beginner using either is working around a misunderstanding of the type system.

Struct embedding for everything. Embedding is attractive to developers from inheritance languages, who use it to rebuild class hierarchies. Prefer a named field and an explicit method until you have a concrete reason to promote the inner type's methods.

Clever concurrency. Start with sequential code that works, measure, and add goroutines where a measurement says they help. A program that is correct and single-threaded beats one that is parallel and racy, and it is usually fast enough.

Frameworks. The net/http package in the standard library is enough to build a real service, and since Go 1.22 its router handles methods and path parameters. Learn it before you learn a framework, and you will understand what the framework is doing for you.

The tooling worth learning in week one

Go ships with an unusually complete toolchain, and the habit of using it is worth building early.

  • go vet finds code that compiles and is almost certainly wrong: misused Printf verbs, copied locks, unreachable code, and a long list of others. Run it before every commit; go test runs a subset automatically.
  • go test -race and go test -cover cover the two questions every test run should answer: are there data races, and what did the tests execute?
  • go doc prints documentation in the terminal, as in go doc strings.Builder, which is quicker than a browser when you only need a signature.
  • go mod tidy adds the dependencies your code imports and removes the ones it no longer does. Run it after you change imports and commit the go.mod and go.sum changes together.
  • pprof profiles CPU and memory, and go tool trace shows the execution timeline. You will not need them in week one, but know they exist, because the day you need to find out why something is slow they save hours of guessing.
  • A debugger, dlv, is available if you prefer stepping to printing. Many Go developers print with %+v and rarely need it, and both approaches are fine.

An editor with gopls, the official language server, gives you completion, jump to definition, rename and automatic imports on save. Whatever editor you use, make it run gofmt and the vet checks on save, and you will have most of the benefit of a heavyweight IDE.

Practise with projects, not tutorials

Reading carries you only so far. Pick small projects that exercise what you just learned, and finish them. Project ideas for beginners lists a graded set of projects that each cover a different part of the language. Two I would build early are a guessing game, which covers input, loops, random numbers and parsing, and a command line to-do app, which adds structs, JSON files and testable functions. For drills, Go exercises points to the sources I found most useful, the Go Tour, Go by Example and Exercism among them. After a project, read the code again a week later and rewrite the part that embarrasses you. That second pass is where the learning happens.

The mistakes everyone makes

Every developer new to Go makes about the same dozen mistakes, and common beginner mistakes collects them with the fix for each. The ones I would flag now: ignoring errors with _, using a slice after appending to a copy, writing to a nil map, taking the address of a loop variable on an old Go version, starting goroutines with no way to stop them, and defining an interface before you have two implementations. A good linter set (go vet ships with the toolchain, and staticcheck or golangci-lint add more) catches several of them for free, so wire one into your editor on the first day.

A study plan, if you want one

A schedule helps some people and constrains others, so treat this as a default to adapt, assuming an hour or two a day.

  1. Days 1 to 2: install, hello world, modules, the zero value, constants, basic types. Write ten tiny programs.
  2. Days 3 to 5: strings, slices and maps in depth. Do not skip the traps. Build the guessing game.
  3. Days 6 to 8: structs, methods, pointers, interfaces. Rewrite one program from your first language, using interfaces where it used inheritance.
  4. Days 9 to 10: errors, defer, packages, the standard library tour (os, io, bufio, encoding/json, net/http, time). Build the to-do app with file storage.
  5. Days 11 to 12: tests, benchmarks, go vet, formatting, and reading a good open source package from end to end.
  6. Week 3 onwards: goroutines, channels, context, and a project that you would use yourself.

By the end of the second week most developers I have worked with are productive on a codebase with review support. By the end of the second month they are writing code that reads as Go. The difference is almost entirely reading and being corrected, so find a codebase you respect and a reviewer who will be direct.

Where the official material helps

The official sources are unusually good, and I recommend them over third-party summaries. A Tour of Go is an interactive introduction that takes a few hours. Effective Go is older but still the clearest statement of idiom, and the Go language specification is short and readable. How to Write Go Code walks through modules and packages, the standard library index is the reference you will use daily, and the Go blog has the design explanations behind generics, errors, contexts and more. Read them in that order, and come back to this page as an index when you want a deeper look at a single thing.

Share

Keep reading

Get new articles by email

We send one email when a new article is published. No spam.

Unsubscribe in one click. No spam, ever.