Skip to main content

Introduction to Go

This article is merely meant as a brief introduction that I hope will encourage you to further research Go and its principles. It is far from exhaustive but does capture the core ideas of Go and some of the features that I find most interesting. I will cover Go’s: Design , Syntax , Ecosystem , Standard Library , Concurrency Model , and approach to Error Handling . At the end of each section I also add resources that I found useful when I was learning Go.

The only expectation is that you are familiar with programming basics – though if you have less experience, I think you will still find most of the article digestible.

Note: examples target Go 1.25

Go’s Design  §

Go is a programming language built around the core principles of simplicity, efficiency, and concurrency. Many programming languages prioritize expressiveness or control. But Go often makes sacrifices in these areas in exchange for clarity and predictable performance.

In my experience, simplicity is the greatest indicator of an effective and maintainable code base. The simpler a project is – both architecturally and line-by-line – the easier it is for developers to work with it effectively. Go’s design encourages code that is more readable and maintainable, almost by default.

Resources: Go Proverbs , Effective Go

Go’s Syntax  §

If you’re familiar with general programming concepts, then Go shouldn’t be too difficult to pick up. Its syntax is relatively basic, with just a few key differences from other C-like languages.

package main

import "fmt"

// In Go, types come *after* the variable or function name
var test int

// Capitalized identifiers (functions, variables, types,
// etc.) are *exported* (visible outside of the package)
func Sum(x int, y int) int {
	return x + y
}

func main() {
	// `range` can be used to iterate (only as of Go 1.22+)
	for i := range 5 {
		// Using formatted output
		fmt.Printf("%d ", Sum(i, i-1))
	}

	// Traditional loop syntax also works:
	for i := 0; i < 5; i++ {
		fmt.Printf("%d ", Sum(i, i-1))
	}

	// Explicit type declaration
	var a bool = true
	// Short variable declaration which infers the type
	b := false

	// No parentheses around conditions
	if a || b {
		fmt.Println("a or b")
	}

	x := 0
	// Infinite loop
	for {
		if x > 10 {
			break
		}
		x++
	}
}

The main syntactic differences from other languages:

  • No semicolons at the ends of lines
  • Types after identifiers in declarations (x int)
  • Export via capitalization, rather than a keyword
  • No parentheses are present around conditions

Go doesn’t have classes or inheritance. Instead, it uses structs for grouping data and methods (i.e. functions that operate on types) for behaviour.

Go’s methods look different from methods in many other mainstream languages. They are defined outside the struct and use a receiver to specify which type they operate on.

package main

import "fmt"

type Example struct {
	name           string // unexported (private)
	isCool         bool
	FavoriteNumber int // exported (public)
	SomeArray      []int
}

// Method with value receiver which receives a *copy* of
// `Example`
func (e Example) PrintName() {
	// unexported `name` still available inside the method
	fmt.Println(e.name)
}

func (e Example) PrintInfo() {
	fmt.Printf("%d %t\n", e.FavoriteNumber, e.isCool)
}

// Method with pointer receiver which allows modification of
// the original
func (e *Example) MakeCool() {
	e.FavoriteNumber = 42
	e.isCool = true
}

// Go doesn't require constructors, but a common convention
// is to use `NewType`
func NewExample(name string) *Example {
	return &Example{
		name:           name,
		isCool:         false,
		FavoriteNumber: 67,
		// Because `SomeArray` is omitted, it will default
		// to `nil`
	}
}

// The main function runs when a package is executed
func main() {
	ex := NewExample("Wyatt")

	ex.PrintName() // Outputs: Wyatt
	ex.PrintInfo() // Outputs: 67 false

	ex.MakeCool()  // Modify through pointer receiver
	ex.PrintInfo() // Outputs: 42 true
}

Go automatically handles pointer/value method calls; so you can call both ex.PrintName() and ex.MakeCool() the same way, and Go will handle the reference conversion for you.

Resources: Tour of Go , Go by Example

Go’s Error Handling  §

I’ve made this its own section, as I often find a language’s approach to error handling to be highly indicative of its general principles. Go’s approach is ‘errors as values.’

package main

import (
	"errors"
	"fmt"
)

// Go supports multiple return values
func canFail(x int) (int, error) {
	if x <= 5 {
		return 0, errors.New("x should be greater than 5")
	} else {
		return x, nil
	}
}

func main() {
	value, err := canFail(4)
	// You'll see this check quite often in Go code
	if err != nil {
		fmt.Printf("Uh oh: %v\n", err)
		return
	}
	fmt.Printf("Success: %d was big enough!\n", value)
}

Resources: Working with Errors , Error Handling in Go

Go’s Ecosystem  §

The pillars that Go is built on also affect the package system and community. Go developers tend to avoid dependencies if possible and prefer straightforward designs. There’s a preference for ‘obvious’ designs, and composition as opposed to inheritance.

In most Go codebases, you’ll find:

  • Shallow directory structures
  • Minimal abstraction
  • Composition over inheritance
  • A preference for plain interfaces and simple tooling

To start a new Go project:

# First, install Go from https://go.dev/doc/install
mkdir my-project
cd my-project
go mod init github.com/your-username/my-project

To add a dependency:

go get github.com/dependency-owner/dependency-name@latest

To clean up unused or redundant dependencies:

go mod tidy

And finally, to run your project:

go run .

go.mod defines the module path and your direct dependencies and the hashes of those dependencies are stored in go.sum.

Some other tools you may see:

  • gofmt / go fmt: formatting (Go culture strongly relies on auto-formatting)
  • go vet and staticcheck: static analysis
  • go test: testing
  • go install pkg@version: install a binary from a module

Resources: Awesome Go , Go Modules Reference

Go’s Concurrency Model  §

In Go, concurrency is first class: it’s built right into the syntax of the language. The primary tool for concurrency in Go is the goroutine.

package main

import (
	"fmt"
	"sync"
	"time"
)

func DoSomething(x int, wg *sync.WaitGroup) {
	// `defer` performs the action at the end of the function
	defer wg.Done()
	time.Sleep(time.Duration(x) * time.Second)
	fmt.Printf("Waited for %d seconds\n", x)
}

func main() {
	var wg sync.WaitGroup

	for i := range 4 {
		wg.Add(1)
		// Simply put `go` in front of a function to run
		// it as a goroutine
		go DoSomething(i, &wg)
	}

	// Wait for all goroutines to finish
	wg.Wait()

	// Both output:
	// Waited for 0 seconds
	// Waited for 1 seconds
	// Waited for 2 seconds
	// Waited for 3 seconds
}

The other core concurrency tool is the channel. Channels are used for communicating between goroutines, and they come with some of their own syntax.

package main

import (
	"fmt"
	"time"
)

func DoWork(id string, delay int, ch chan string) {
	time.Sleep(time.Duration(delay) * time.Second)
	// Put this message into the provided channel
	ch <- fmt.Sprintf(
		"%s finished after %d seconds",
		id,
		delay,
	)
}

func main() {
	// `make` is used to create channels
	ch1 := make(chan string)
	ch2 := make(chan string)

	// Goroutine 1 will finish first and 2 will finish around
	// three seconds after
	go DoWork("Goroutine 1", 2, ch1)
	go DoWork("Goroutine 2", 5, ch2)

	for range 2 {
		// `select` matches on whichever channel is ready
		select {
		// `<-` used to receive value from channel
		case msg1 := <-ch1:
			fmt.Println(msg1)
		case msg2 := <-ch2:
			fmt.Println(msg2)
		}
	}

	// Outputs:
	// Goroutine 1 finished after 2 seconds
	// Goroutine 2 finished after 5 seconds

	// Capacity is specified as last argument to `make`
	buffered := make(chan int, 2)
	buffered <- 10
	buffered <- 20
	// closing signals no more values will be sent
	close(buffered)

	// 'range' can be used to receive values until the
	// channel closes
	for val := range buffered {
		fmt.Printf("Buffered value: %d\n", val)
	}

	// Outptuts:
	// Buffered value: 10
	// Buffered value: 20
}

Resources: Go 101 , Go Concurrency Patterns (video) , The Go Memory Model

Go’s Standard Library  §

Given the Go community’s general opposition to unnecessary dependencies, Go’s standard library is quite comprehensive – particularly in the areas of I/O and networking. It is very often not necessary to look outside the standard library. Before adding a dependency, always check Go’s Standard Library and see if it provides what you need.

Some of the big packages:

  • net: TCP, UDP, HTTP, DNS, Unix sockets, and more (including net/http).
  • io / os: file I/O, environment, io.Reader/io.Writer patterns.
  • encoding: JSON, XML, gob, base64, etc.
  • crypto: common cryptographic tools.
  • testing: unit tests, benchmarks, and examples.

Long story short: if you need it, Go often has a solid foundation in the standard library.

Further Resources  §

There’s still a lot I didn’t cover, and much left to explore. But I hope that what I did show impressed you enough to continue looking into it. Go is one of my favourite programming languages; I find the trade-offs it makes appealing, and I love how simple it is to pick up.

Tour of Go is a hands-on tour of the syntax and standard library of Go. It walks you through all of its critical components with a built-in editor.

Effective Go provides idioms for common patterns. This was probably the resource I found most useful in my journey learning more advanced Go.

pkg.go.dev/std is the standard library documentation.

Go Blog covers particular topics. Some topics I didn’t mention or only briefly mentioned, such as generics, channels, concurrency, and advanced error handling, are also worth exploring on the Go Blog.

If you have any questions or enjoyed the article, feel free to reach out! You can find me on LinkedIn .

October 15, 2025 · by Lucas McClean
reviewed by Lucas Velasco

© 2025 Lucas McClean. All rights reserved.

//