Skip to content

Instantly share code, notes, and snippets.

What would you like to do?
Go language overview for experienced programmers

The Go language for experienced programmers

Why use Go?

  • Like C, but with garbage collection, memory safety, and special mechanisms for concurrency
  • Pointers but no pointer arithmetic
  • No header files
  • Simple, clean syntax
  • Very fast native compilation (about as quick to edit code and restart as a dynamic language)
  • Easy-to-distribute executables
  • No implicit type coercions
  • Simple built-in package system
  • Simple tools
  • Inferred types on variable declarations
  • Slices and maps (feel like arrays and hashmaps in dynamic languages)
  • Explicit error handling with error values and multiple return
  • Interface-based polymorphism
  • Interfaces implicitly implemented (allowing post-hoc interfaces for imported types)
  • Goroutines (runtime managed lightweight threads)
  • Channels for coordinating goroutines and sharing data between them (based on the theory of CSP)

Reasons not to use Go

  • Immature debugger
  • No generics
  • Garbage collection overhead / pauses (though the pauses are very short)
  • Executable size (the runtime embedded in the output executables is itself ~1MB)

Base types

int8           // 8-bit signed int
int16          // 16-bit signed int
int32          // 32-bit signed int
int64          // 64-bit signed int

uint8          // 8-bit unsigned int
uint16         // 16-bit unsigned int
uint32         // 32-bit unsigned int
uint64         // 64-bit unsigned int

float32       // 32-bit float
float64       // 64-bit float

complex64      // two 32-bit floats
complex128     // two 64-bit floats

int        // 32- or 64-bit signed int (depends upon compilation target)
uint       // 32- or 64-bit unsigned int (depends upon compiliation target)

uintptr    // unsigned int large enough to store an address on compilation target

string     // a string value is an address referencing UTF-8 data elsewhere in memory

byte    // alias for uint8
rune    // alias for int 32 (used for representing Unicode code points)

Semi-colon insertion

The semi-colons used in most C-like syntaxes are generally left implicit in Go. Semi-colons are implicit at the end of any line ending with:

  • a number, string, or boolean literal
  • an identifier
  • the reserved words break continue fallthrough return
  • the operators ++ --
  • the end delimiters } ] )

Variable declarations and type inference

The full syntax for declaring a variable:

var NAME TYPE;                   // declare without initialization
var NAME TYPE = EXPRESSION;      // declare with initialization

An uninitialized variable defaults to the 'zero value' for its type. The zero values are:

  • numbers: 0
  • strings: ""
  • bools: false
  • pointers: nil
  • structs: (each element is the zero value of its type)
  • arrays: (each element is the zero value of its type)
  • slices: (a null reference with capacity 0 and length 0)
  • interfaces: nil

When a declaration is initialized, we can leave the type inferred from the value's type:

var foo = "hello"    // foo inferred to be a string variable
var bar = ack()      // bar inferred to be whatever type ack() is declared to return

(Keep in mind that all Go operations and functions return a fixed type, so the compiler always knows the type of every expression.)

Go programmers normally use this shorthand for inferred declarations:

foo := "hello"       // declare foo, which is inferred to be a string variable
bar := ack()         // declare bar, which is inferred to be whatever type ack() is declared to return

Type casting

Unlike many other statically-typed languages, Go is strict about types:

  • a variable can only be assigned values of the exact same type
  • each argument to a function must exactly match the corresponding parameter's type
  • the operand types of a binary operation must exactly match

The only place where types need not match exactly is when using interface types (discussed later).

We can cast between certain types:

var x int = 35
var y float32 = float32(x)    // cast int to float32

Casting between some types preserves the precise value; in other cases, a cast may distort the original value, e.g. casts from floats to integers.

Named types

We can define our own types with a type statement. For example:

type alice int    // create a new type 'alice' which is represented as an int

These new types are not aliases: the compiler considers them to be separate, non-interchangeable types. We can however explicitly cast between a named type and the type it is based on (and vice versa) without distoring the value (because the types have the same underlying representation as bits):

var x int = 3    // an int variable called 'x'
var y alice      // an alice variable called 'y'
y = 5            // OK! integer literals are considered to have no specific type, and so 5 is a valid alice value
y = x            // compile error! an alice variable cannot be assigned an int value 
                 // (even though an alice is really just an int)
y = alice(x)     // OK! cast the int value to an alice value
x = int(y)       // OK! cast the alice value to an int


A Go package is a namespace and unit of compilation. One source directory constitutes one package, and all of the source files in the directory make up the package.

Source file names must end with .go. Certain suffixes starting with underscore can be used to specify that a file should only be compiled for certain platforms, e.g. the file foo_linux.go will only be included when compiling for Linux. Otherwise, source file names can be anything you want.

The first line of code in each source file must be a package statement declaring the name of the package:

package foobar       // declare that this file is part of the package named foobar

All source files in a directory must declare the same package name.

The package name main is special. A package named main is compiled into an executable. Any package not named main is compiled into an object file.


In each source file, you can use by name anything defined in the current package. To use names from another package, that other package must be imported into the current file with an import statement:

package whatever
// all imports must be at top of the file but after the package statement
import "otherpackage" 

The package to import is specified by its import path as a string. An import path tells the Go compiler where to find a package relative to the GOPATH/src directory (GOPATH is an environment variable pointing to your chosen Go working directory):

import "foo/bar/ack"   // import the package in GOPATH/src/foo/bar/ack

A package's import path and its name can be completely different, but by convention the last segment of the import path matches the name, e.g. the package at import path "foo/bar/ack" would have the name ack.

Standard library packages are kept in a subdirectory where the Go tool itself is installed, and they generally have short import paths, like "fmt".

When a package is imported, names of that package are prefixed by the package name and a dot:

mary.David()  // invoke a function David from a package named mary

Only names starting with uppercase letters are public, i.e. visible to other packages. Names starting with lowercase letters are private to their package.

Packages named main cannot be imported by other packages.

Package imports cannot be cyclical, e.g. if package A imports package B which imports package C, B cannot import A, and C cannot import either A or B.

Escape analysis

Unlike C, Go is garbage collected and memory safe. Anything we create in a function call is safe to use even after the call returns. To make this possible, the Go compiler performs escape analysis: it determines which things created in a function might be referenced outside the function; only things which the compiler knows for sure will not escape the call in which they are created get stack allocated; everything else is heap allocated.

For example, it is dangerous in a C function to return a pointer to a local variable because the pointed-to-variable no longer exists after the call returns. In Go, however, this is no problem: the compiler detects that the local variable may be used beyond its scope, and so the compiler will allocate the variable on the heap.


Arrays in Go are fixed in size and homogenous:

var foo [3]int           // local variable foo is an array of 3 ints
var bar [7 + 2]float32   // local variable bar is an array of 9 ints
foo[0] = 98              // assign 98 to first slot of foo
bar[8] = -4.21           // assign -4.21 to last slot of bar
var i int = foo[4]       // assign 0 to i (the uninitialized elements of an array start out as zero-values)

The size must be specified by a constant expression. (For dynamically-sized arrays, use slices, as discussed later).

Arrays of the same element type but different sizes are considered different types of array.

Arrays of the same type can be assigned to each other and compared with == and !=:

var foo [3]int
var bar [3]int
foo = bar          // copy all values of bar to foo
foo == bar         // true if all respective elements are equal 

An array can be created as a literal:

// an array with 3 ints: 8, 900, -21
[3]int{8, 900, -21}             

// an array with 7 ints (... tells the compiler to infer the size from the number of elements)
[...]int{1, 2, 3, 4, 5, 6, 7}   

These array literals are most commonly passed directly to other functions or assigned to array variables. For example:

foo([3]byte{9, 100, 30})       // pass an array of 3 bytes to foo()

bar := [5]int{1, 2, 3, 4, 5}   // create [5]int variable bar with initial elements 1, 2, 3, 4, 5

We can create arrays of any type, including arrays of arrays. Multi-dimensional arrays have a special literal syntax:

var foo [3][2]int
foo = [3][2]int{{1, 2}, {3, 4}, {5, 6}}

Multiple-return functions

A Go function can return multiple values. The return types are listed in parens:

// a function with no params returning an int, a byte, and a string
func foo() (int, byte, string) {
    return 300, 4, "hi"

A multiple-return function can only be called as a stand-alone statement or as the value of an assignment with the right number of variables:

foo()              // the return values are discarded
a, b, c := foo()   // assign 300 to a, 4 to b, and "hi" to c
a, b := foo()      // compile error! no receiver for the returned string
bar(foo())         // compile error! cannot call multi-return function in single-value context

If you don't want to use one or more returned values, you can assign them to the special blank identifier (a single underscore):

a, _, b, _ = ack()   // ack returns 4 values, but we only want the first and third

Be careful using := with multiple return: any target variables which don't already exist in the current scope will be implicitly declared:

var x int
    x, y := foo()    // create a new x in this scope rather than assign to x of the enclosing scope

Avoiding this gotcha requires explicitly declaring the variables with var:

var x int
    var y int
    x, y = foo()   // assign to x of enclosing scope and y of this scope


A slice is a value representing a logical range of indices of an array. For example, given an array of 10 ints, a slice of ints could represent the range from index 3 up to (and including) index 8; this slice would have a length of 5 and a capacity of 6 (because there are 6 elements in the array from index 3 of the array to its end).

Each slice value is made up of three components:

  • a reference to a starting element within an array
  • a length (the number of elements represented by the slice)
  • a capacity (length + the remaining number of elements in the array)

A slice's type is determined just by the type of its elements, not by its length or capacity:

var foo []int       // a variable foo that stores int slice values
var bar []string    // a variable bar that stores string slice values

Given an X array, we can create an X slice with the slice operator. We can get the length and capacity of slices with the built-in len() and cap() functions:

foo := [7]int{10, 20, 30, 40, 50, 60, 70}
var bar []int
bar = foo[2:5]    // a slice representing index 2 up to (but not including) index 5

len(bar)          // 3
cap(bar)          // 5

foo[2]            // 30
bar[0]            // 30

foo[3]            // 40
bar[1]            // 40

foo[4]            // 50
bar[2]            // 50

foo[5]            // 60
bar[3]            // panic! exceeded bounds of the slice

// create a slice from a slice
ack := bar[1:3]
len(ack)           // 2
cap(ack)           // 4
ack[0]             // 40

// the ranges represented by ack and bar overlap in memory
ack[0] = 41
bar[1]             // 41
bar[1] = 42
ack[0]             // 42
// changes through the slice modify the underlying array
foo[3]             // 42

// assign the slice value (reference, length, capacity) of ack to the variable bar
bar = ack

In the slice operator, the number before the colon defaults to 0, and the number after the colon defaults to the length of the array or slice. Effectively, foo[:] returns a slice representing the whole range of foo.

The special built-in function make() returns a slice with a new underlying array:

make([]int, 7, 10)    // create an int array of size 10 and return a slice of indexes 0:7 of this new array

(Notice that make() takes a type as its first argument, so it is clearly not an ordinary function.)

Variadic functions

If we want a function to take a varying number of inputs, we could simply make a parameter a slice, to which callers would then pass a slice with 0 or more elements:

func foo(a string, b []int) { /* do stuff */ }

foo("hi", []int{6, 2, -11})
foo("yo", []int{5})
foo("bye", []int{})    

This certainly works, but the []type{} syntax makes these calls a little cluttered. To clean up this pattern, the last parameter of a Go function can be a slice denoted by ... (elipses) instead of []; the slice passed to this parameter is assembled from 0 or more values not enclosed in the []type{} syntax:

func foo(a string, b { /* do stuff */ }

foo("hi", 6, 2, -11)    // []int{6, 2, -11} is passed to b
foo("yo", 5)            // []int{5} is passed to b
foo("bye")              // []int{} is passed to b

// explicitly pass an actual slice by suffixing ...
nums := []int{8, 3, 4}
foo("aloha", nums...)   // nums is passed to b

Appending to slices

To append ints to an int slice no matter its capacity, we can define an append function:

func append(sl []int, vals []int {
    newLen := len(sl) + len(vals)
    if cap(sl) >= newLen {
        // original underlying array was large enough for appending the new values
        copy(vals, sl[len(sl):cap(sl)])
        return sl[:newLen]  // same slice range, but extended
    } else {
        // original underlying array was not large enough
        newSl := make([]int, newLen)  // create new, larger underlying array
        copy(sl, newSl)               // copy existing values
        copy(vals, newSl[len(sl):])   // append the new values
        return newSl

Go has no mechanism for defining generic functions, but a few generic functions are provided as built-ins. The built-in append() is generic: it takes slices of any type and returns the slice type passed to it, e.g. passing a slice of bytes to append() returns a slice of bytes.


A map is a hashmap of key-value pairs. The values can be any type; the keys cannot be functions, slices, or maps.

A map variable is just a reference. To create an actual map, use make():

var foo map[string]int
foo = make(map[string]int)    // create a new empty map
foo["hello"] = 10             // assign value 10 to key "hello"

We can create a map with literal syntax:

foo := map[string]int{"hi": 9, "bye": 11}

The built-in delete() removes a key from a map:

foo := map[string]int{"hi": 9, "bye": 11}
delete(foo, "hi")  // remove the key "hi"

For-range loops

A for-range loop is Go's equivalent of for-in/foreach in other languages:

for i := range arr {
    // i iterates from 0 up to (but not including) len(arr)

// when range is assigned to two variables, the first is the index, the second is the value
for i, v := range arr {
    // i is the index, v is the value

We can use for-range to iterate through the elements of a map:

// the iteration order is random
for k := range m {
    // k is a key from m

for k, v := range m {
    // k is a key from m and v is its corresponding value


A struct (short for structure) is a programmer-defined data type made up of other types:

type Cat struct {
    Lives int
    Age float32
    Weight float32
    Name string

var c Cat   // fields start out as 'zero' values
c.Name = "Mittens"
c.Lives = 9
c.Age = 4.3

Structs cannot be directly recursive:

type Cat struct {
    Lives int
    Age float32
    Weight float32
    Name string
    Mother Cat     // compile error! every Cat would contain an infinite number of other Cats

A struct can however be indirectly recursive through some kind of reference:

type Cat struct {
    Lives int
    Age float32
    Weight float32
    Name string
    Mother *Cat     // a pointer to Cat is OK!


Go pointers are basically just like C pointers except Go has no pointer arithmetic. A pointer represents a typed address, e.g. an address of an int, an address of a string, an address of a byte, etc. We can get a pointer to a variable using & (the reference operator); using & on a variable of type X returns an X pointer.

var i int
var b byte
var ip *int     // variable ip stores int pointers
var bp *byte    // variable bp stores byte pointers
ip = &i         // assign ip the address of i
bp = &b         // assign bp the address of b
ip = &b         // compile error! &b returns a byte pointer, not an int pointer

We can also use & to get a pointer to a struct field:

var c Cat
var fp *float32
fp = &c.Age

We can get the value pointed to by a pointer using * (the dereference operator), and we can modify the storage pointed to by a pointer using * on the target of assignment:

var f float32 = 8.2
var fp *float32 = &f
*fp                    // 8.2
*fp = 14.7             // assign 14.7 to storage pointed to by fp
f                      // 14.2

The zero-value of a pointer is nil. Dereferencing a nil pointer triggers a panic.

We can create pointers of any type, even pointers to pointers. However, multi-degree pointers tend to be used much less in Go code than in C code, largely because slices and interfaces fill many of the same use cases as pointers.

Anonymous functions

A function variable stores the address of a function:

function bar (a int, b string) (float32, []int) { /* body */ }
function ack (a int) { /* body */ }

var foo func (int, string) (float32, []int)
foo = bar     // OK
foo = ack     // compile error! wrong type of function

We can create nested functions using expression syntax:

foo := func (a int) int {
    return a * 5

// OK if bar takes a function taking a string and returning nothing
bar(func (a string) {

Just like in Javascript, a nested function in Go is a closure over its containing function(s), meaning that any variables it uses from the enclosing call(s) persist, even after the enclosing function(s) return:

func foo() {
    a := 3      // 'a' is used by the nested function and so persists even after the call to foo() returns
    return func () {
        a += 2           

f := foo()
f()  // 5
f()  // 7
f()  // 9


A Go method is like a function but with a special receiver parameter. Only named types and pointers to named types can be method receivers. Each of these types has its own table of methods (and effectively its own namespace of methods).

type alice int

// 'a' is the receiver
func (a alice) foo() int {
    return int(a) + 3

a := alice(5)       // 8

If a named type has a method foo, the pointer to that named type cannot have its own method foo, and vice versa: either type X has a method foo, or *X has a method foo, but never both.

We can invoke methods of *X on instances of X, in which case the reference with & is implicit:

func (x *X) foo() { /* body */ }

var bar X    // (&bar).foo()

Likewise, we can invoke methods of X on pointers to X, in which case the dereference with * is implicit:

func (x X) foo() { /* body */ }

var bar *X    // (*bar).foo()


Go interfaces are very much like Java and C# interfaces: an interface is a type defined by a set of method signatures rather than any concrete code or data.

Unlike in Java and C#, which types implement which interfaces is not stated by the programmer explicitly: if a type has methods matching all the signatures listed in an interface, that type implicitly implements the interface.

If an interface method is implemented on X, then *X is considered to implement the interface method as well. Effectively, *X is considered to implement an interface if the methods are all implemented on X, or on *X, or some mix thereof. However, X is considered to implement an interface only if the methods are all implemented on X itself.

An interface value is made up of two references:

  • A reference to a value of a type implementing the interface
  • A reference to the type of the value itself (the type is represented in memory as the type's table of methods)

All types which implement an interface are considered valid subtypes of the interface. So given two types Cat and Dog which both implement interface Animal, we can assign Dog and Cat values to an Animal variable:

var c Cat
var d Dog
var a Animal
a = c  // OK!
a = d  // OK!

No matter what type of value we assign to the Animal variable, we can only use that variable as an Animal, not as a Dog or Cat:

var d Dog
var a Animal = d
a.sleep()               // OK (assuming sleep is a method of Animal)
a.bark()                // compile error! (assuming bark is not a method of Animal)
var f float32 = a.age   // compile error! (interfaces do not have fields)

We can branch over the possible types of an interface value with a type switch:

var x Animal;
// ... assume x is assigned a concrete type of Animal
switch v := x.(type) {
case Cat:    // if x references a Cat
    // v is of type Cat
case Dog:    // if x references a Dog
    // v is of type Dog
    // v is of type Animal


A goroutine is a thread of execution managed by the Go runtime rather than directly by the OS. The Go runtime manages a pool of OS threads and schedules the goroutines to run in the OS threads.

Creating a program with many OS threads is generally impractical because each OS thread incurs significant overhead; creating a program with many thousands of goroutines is generally practical because goroutines are relatively cheap.

A go statement spawns a new goroutine:

go foo()      // kick off new thread that starts with a call to foo()
bar()         // executed in the current thread (so does not wait for foo() to return)


Select statements

Panic and recover

Most programming languages introduced in the last few decades are designed around exceptions for error handling. Go does have a mechanism nearly like exceptions called 'panics', but panics are reserved for backing out of code in the event of out-right bugs, such as exceeding array bounds. It's generally improper to catch panics (or recover them, as we say in Go), except as last-ditch opportunities for our programs to fail gracefully.

Error interface

For eventualities like failures to read files or open network connections, Go favors explicitly returned error values rather than panics. Very simply, the caller of a function must check the return value for error conditions. Thanks to multiple return, this is not as clunky as it is in C.

Go also makes error values easier to handle by defining an Error interface. By convention, all error values in Go implement the Error interface, and the declared return type for an error value is (almost) always Error. Thus, a function can return different types of error values from different branches. The caller is expected to know which kind of error values the function might return and branch accordingly.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.