br3ndonland / golearn

Resources from learning Go

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Go learn

Resources

Get going

Make a repo with the GitHub CLI:

gh repo create golearn \
  --clone \
  --description "Resources from learning Go" \
  --disable-issues \
  --disable-wiki \
  --gitignore "Go" \
  --license "CC0-1.0" \
  --public

Go through these resources:

  • A Tour of Go
  • Learn Go with Tests
    • Most of the directory names in this repo match Learn Go with Tests.
    • Package names have been changed from package main to specific package names.
    • Source code is only included if it is 100% covered with tests. Learn Go with Tests includes uncovered code and that code is not included here.

Go deeper

Syntax

Names

Effective Go has general info on names. Key points:

By convention, packages are given lower case, single-word names; there should be no need for underscores or mixedCaps... Another convention is that the package name is the base name of its source directory; the package in src/encoding/base64 is imported as "encoding/base64" but has name base64, not encoding_base64 and not encodingBase64... Finally, the convention in Go is to use MixedCaps or mixedCaps rather than underscores to write multiword names.

The comments on not using underscores apply to package names, not file names. Test files are written with names ending in _test.go.

A Tour of Go explains exported names:

In Go, a name is exported if it begins with a capital letter. For example, Pizza is an exported name, as is Pi, which is exported from the math package.

Go files can be written with names out-of-order, and Go is able to handle this when determining order of evaluation of the expressions. Python resolves names approximately top-to-bottom (from the top of a file to the bottom), so in Python, the order of names is more important.

Command-line applications

As described in the "Get started with Go" tutorial and Go language spec, func main() is used to determine behavior when running the program from the command-line, like if __name__ == "__main__": in Python.

Iteration

for is Go's while:

package main

import "fmt"

func main() {
	sum := 1
	for sum < 10 {
		sum += sum
	}
	fmt.Println(sum)
}

An infinite loop is simply for:

package main

func main() {
	for {
	}
}

Strings and runes

Strings must be double-quoted. A single-quoted character is called a rune and has its own data type.

Arrays and slices

Go's arrays are fixed-length. Variable-length arrays are called "slices" and, like "package" and "module," the term "slice" is used differently in Go than it may be elsewhere. In Python, a "slice" is a part of a list, whereas in Go a slice is the list itself. Slices can be sliced.

Maps

Go map syntax is intuitive and similar to Python dictionaries.

Map keys and values can be set using square bracket syntax, like mapInstance[key] = value.

Map lookups (getting values with square bracket syntax) have some important differences from Python. Python dictionary lookups return a single value if the key is found, or a KeyError exception if the key is not found. Go map lookups return two values: the value if found, and a Boolean indicating whether or not the key is in the map. Assignment of these values would therefore look like value, inMap := mapInstance[key].

Structs

A Tour of Go says, "A struct is a collection of fields. Struct fields are accessed using a dot. Struct fields can be accessed through a struct pointer." Hmm, not very helpful. The "How to Write Go Code" docs are not particularly helpful either - the example code introduces a struct without explaining what it is. The Go language spec section on struct types is esoteric and doesn't get us much further. So let's just look at an example from the structs chapter of "Learn Go with Tests" (in shapes.go in this repo).

type Rectangle struct {
	Width  float64
	Height float64
}

Methods

The Go language spec section on method declarations says, "A method is a function with a receiver." The "receiver" can be any type (built-in types or structs). When methods are called, they must be called on an instance of the type. In this sense, it seems like Go methods are somewhat like Python class instance methods, which accept a class instance (self) as their receiver.

The receiver variable is conventionally named with the first letter of the type, like r Rectangle.

Methods are not nested or indented inside their receiver types, so the func syntax itself is the only syntactic indication that a method is associated with a receiver.

Adding methods to the struct example would look like this:

type Rectangle struct {
	Width  float64
	Height float64
}

func (r Rectangle) Perimeter() float64 {
	return 2 * (r.Width + r.Height)
}

func (r Rectangle) Area() float64 {
	return r.Width * r.Height
}

Interfaces

Interfaces are somewhat like abstract base classes in Python. One difference from other programming languages is that interface resolution is implicit in Go. In the example below, Circle is automatically a Shape because it implements an Area() method that returns a float64.

import "math"

type Shape interface {
	Area() float64
}

type Circle struct {
	Radius float64
}

func (c Circle) Area() float64 {
	return math.Pi * c.Radius * c.Radius
}

Pointers

Go copies values when they are passed in to functions or methods. Pointers can be used to access values directly instead of copying them. Pointers are indicated with an asterisk (*) before the name of the value. Within functions that accept pointers as arguments, the pointers are automatically "dereferenced," meaning that the value at the memory address is identified rather than the memory address itself. See the example from the pointers chapter of "Learn Go with Tests" in wallet.go.

func (w *Wallet) Deposit(amount Bitcoin) {
	w.balance += amount
}

Formatting

Go has a built-in formatter gofmt that formats indentations with tabs and does not enforce a maximum line length.

Troubleshooting

Go workspaces

Initially, I had a tour.go file from working on A Tour of Go and then I started Learn Go with Tests. I thought I might put the files into subdirectories to keep them separated.

  • golearn/
    • helloworld/
      • hello_test.go
      • hello.go
    • tour/
      • tour.go
The contents of each file looked like this (expand).

helloworld/hello_test.go

package main

import "testing"

func TestHello(t *testing.T) {
	got := Hello()
	want := "Hello, world"
	if got != want {
		t.Errorf("got %q want %q", got, want)
	}
}

helloworld/hello.go

package main

import "fmt"

func Hello() string {
	return "Hello, world"
}

func main() {
	fmt.Println(Hello())
}

tour/tour.go

package main

import (
	"fmt"
	"time"
)

func add(x, y int) int {
	return x + y
}

func addAndPrint(x, y int) string {
	return fmt.Sprintf("The sum of %d and %d is %d", x, y, add(x, y))
}

func main() {
	var x, y int = 42, 13
	fmt.Println("Welcome to the playground!")
	fmt.Println("The time is", time.Now())
	fmt.Println(addAndPrint(x, y))
}

With this directory configuration, I can run each module like go run helloworld/hello.go or go run tour/tour.go, but I can't run tests.

If I try to run go test helloworld/hello_test.go, I see an error:

golearn on  main [?⇡] via 🐹 v1.20.4
❯ go test helloworld/hello_test.go
# command-line-arguments [command-line-arguments.test]
helloworld/hello_test.go:6:9: undefined: Hello
FAIL    command-line-arguments [build failed]
FAIL

If I try to change into the helloworld directory and run go test, I see a completely different error:

golearn on  main [?⇡] via 🐹 v1.20.4
❯ go test
go: go.mod file not found in current directory or any parent directory; see 'go help modules'

The "Hello, World" chapter of Learn Go with Tests suggested running go mod init.

After running go mod init in the subfolder and then hovering over the module or package name in a Go file with the VSCode Go extension enabled (extension ID golang.go), I see an error:

gopls was not able to find modules in your workspace. When outside of GOPATH, gopls needs to know which modules you are working on. You can fix this by opening your workspace to a folder inside a Go module, or by using a go.work file to specify multiple modules. See the documentation for more information on setting up your workspace: https://github.com/golang/tools/blob/master/gopls/doc/workspace.md. go list

Screenshot of VSCode showing gopls error

The gopls docs (the Go language server) say:

Go workspaces (Go 1.18+)

Starting with Go 1.18, the go command has native support for multi-module workspaces, via go.work files. These files are recognized by gopls starting with gopls@v0.8.0.

The easiest way to work on multiple modules in Go 1.18 and later is therefore to create a go.work file containing the modules you wish to work on, and set your workspace root to the directory containing the go.work file.

For example, suppose this repo is checked out into the $WORK/tools directory. We can work on both golang.org/x/tools and golang.org/x/tools/gopls simultaneously by creating a go.work file using go work init, followed by go work use MODULE_DIRECTORIES... to add directories containing go.mod files to the workspace:

cd $WORK
go work init
go work use ./tools/ ./tools/gopls/

...followed by opening the $WORK directory in our editor.

The Go docs on workspaces provide further details.

At this point, I ran:

golearn on  main [?⇡] via 🐹 v1.20.4
❯ go work init

golearn on  main [?⇡] via 🐹 v1.20.4
❯ go work use helloworld tour

The go.work file came out like this:

go 1.20

use (
	./helloworld
	./tour
)

Note that the GitHub Go .gitignore template is configured to ignore go.work files.

I guess a simpler way of doing this would have been running go mod init golearn in the top-level directory to have a single go.mod file, then adding subdirectories without their own go.mod files. I figured this out after learning more about Go modules. Read on.

Go packages and modules

Package is not a main package errors

After moving helloworld to a subdirectory and running go mod init to establish the module, I first tried changing the package lines in hello.go and hello_test.go to say package helloworld, but that led to errors like package command-line-arguments is not a main package (in the top-level directory) and package helloworld is not in GOROOT (/opt/homebrew/Cellar/go/1.20.4/libexec/src/helloworld) (in the package directory). I also saw a notice in hello.go that func main is unused.

To resolve the errors, the module name in go.mod should say module helloworld, but the package lines in hello.go and hello_test.go should say package main. The Go CLI and VSCode extension could provide some clearer instructions here.

Cannot import "main" errors

Tutorials commonly include all source code in package main examples, which allows the examples to be run from the command-line, but this is not how a larger module would be built. The "Learn Go with Tests" chapter on arrays and slices explains:

If you had initialized go mod with go mod init main you will be presented with an error _testmain.go:13:2: cannot import "main". This is because according to common practice, package main will only contain integration of other packages and not unit-testable code and hence Go will not allow you to import a package with name main.

To fix this, you can rename the main module in go.mod to any other name.

Did they mean "Go will not allow you to import a package module with name main"? Tests seem to work on package main examples.

Definitions of package and module

So what's the difference between a package and a module?

The Go docs on "How to write Go code" explain,

Go programs are organized into packages. A package is a collection of source files in the same directory that are compiled together. Functions, types, variables, and constants defined in one source file are visible to all other source files within the same package.

A repository contains one or more modules. A module is a collection of related Go packages that are released together. A Go repository typically contains only one module, located at the root of the repository. A file named go.mod there declares the module path: the import path prefix for all packages within the module. The module contains the packages in the directory containing its go.mod file as well as subdirectories of that directory, up to the next subdirectory containing another go.mod file (if any).

...

The first statement in a Go source file must be package name. Executable commands must always use package main.

The Go modules docs provide more in-depth documentation.

This is effectively the inverse of Python - a Python module is a .py file, whereas a Python package is a collection of modules in a directory with a __init__.py file in it. Packages can have subpackages, which are additional directories inside the package directory with __init__.py files in them.

Most other real-world definitions of "package" appear to follow the Python convention and not the Go convention. For example, GitHub calls their service "GitHub Packages" not "GitHub Modules."

Setting up the Go module

At this point, I realized I didn't need Go workspaces. I deleted the go.mod files from each subdirectory, and added a single go.mod file to the top-level directory with go mod init github.com/br3ndonland/golearn, following the naming suggested in the Go modules docs.

About

Resources from learning Go

License:Creative Commons Zero v1.0 Universal


Languages

Language:Go 100.0%