A half-hour to learn Rust, except it's Go instead of Rust

I enjoyed seeing the A half-hour to learn Rust blog post by Amos recently. For someone like me who doesn't know Rust but is curious about it, it felt like a very accessible introduction that I could use to learn Rust.

One of my absolutely favorite properties of Go is how short and simple the language is. My previous primary language was C++. Go has comparatively few keywords and language features, and yet I never feel something is missing when I'm writing Go code. On the contrary, I feel very productive as a programmer when getting things done using Go, and I really appreciate that it's simple to learn and teach. After using it for non-stop for over 7 years, writing Go still brings me joy like no other programming language I've ever used. I certainly wouldn't recommend Go to someone who wants a complicated programming language, but I would if one wants a simple and productive one.

So, reading that blog post made me very curious to try a quick experiment. What would the same post look like if it were written about Go instead?

I figured I'd give it a shot.

Ready? Go.

var declares a variable of a given type:

var x int // Declare x of type int.
x = 42    // Assign 42 to x.

This can also be written as a single line:

var x int = 42

You can also specify the variable's type implicitly:

var x = 42 // Type int is inferred from 42.

This can also be written using a short form:

x := 42

You can declare many variables at the same time:

var a, b, c int
a, b, c = 1, 2, 3
x, y := 40, 80

All declared variables are always initialized to the zero value. If you declare a variable and use it right away, you can be sure it'll have the zero value of that type.

var x int
foo(x) // Same as foo(0).

The underscore _ is a special identifier. It's used to indicate "no identifier" and can be used to discard something, or mark it as "used":

// This does nothing because 42 is a constant.
_ = 42

// This calls fetchThing but discards its result.
_ = fetchThing()

You can import a package for side-effects only by using _:

import _ "image/png"

A variable with the same name can be re-declared as long as it's in a different block. That causes it to shadow an existing variable:

x := 13
if true {
	x := x + 3
	// Using x after that line only refers to the second x,
	// the first x is no longer accessible in this scope.
}

Go does not have a concept of "tuples" like Rust, but it has struct types and functions can return multiple result values.

Semicolons can be used to mark the end of a statement. But they are optional and automatically inserted at the end of a line, so they're only needed if you're placing multiple statements in one line:

var x = 3; var y = 5; var z = y + x

All Go code is formatted with gofmt, the standard Go code formatting tool. It pretty prints Go code so each statement is on a single line, without semicolons:

var x = 3
var y = 5
var z = y + x

It aligns fields and makes Go code look consistent, no matter who wrote it:

header := component.Header{
	CurrentUser:       authenticatedUser,
	NotificationCount: nc,
	ReturnURL:         returnURL,
}

Statements can span multiple lines:

notifications := initNotifications(
	http.DefaultServeMux,
	webdav.Dir(filepath.Join(storeDir, "notifications")),
	gerritActivity,
	users,
	githubRouter,
)

func declares a function.

Here's a simple function:

func Greet() {
	fmt.Println("Hi there!")
}

Here's a function that returns a 32-bit signed integer:

func FairDiceRoll() int32 {
	return 4
}

A pair of brackets declare a block, which has its own scope:

func main() {
	x := "out"
	{
		// This is a different x.
		x := "in"
		fmt.Println(x)
	}
	fmt.Println(x)
}

Dots are typically used to access fields of a struct:

var x = struct{ a, b int }{10, 20}
x.a // This is 10.

dmitshur := fetchSomeStruct()
dmitshur.WebsiteURL // This is "https://dmitri.shuralyov.com".

Or call a method on a value:

w.Header().Set("Content-Type", "text/html; charset=utf-8")

Dots are also used to access exported identifiers from other Go packages that are imported:

import "fmt"

func main() {
	fmt.Println("Println is a function in the fmt package")
}

Dots work for accessing struct fields regardless whether they're a value or a pointer, which makes refactoring code result in a smaller diff.

Named types are declared with the type keyword.

type MyInt int

Struct types are defined with the struct keyword.

type Vec2 struct {
	X float64
	Y float64
}

They can be initialized using struct literals:

var v1 = Vec2{X: 1.0, Y: 3.0}
var v2 = Vec2{Y: 2.0, X: 4.0 /* The order does not matter, only the names do. */}

One value can be assigned to another:

var v3 = v2
v3.X = 14

You can declare methods on named structs that you defined:

type Number struct {
	Odd   bool
	Value int32
}

// IsStrictlyPositive reports whether Number n is strictly positive.
func (n Number) IsStrictlyPositive() bool {
	return n.Value > 0
}

And use them like usual:

func main() {
	minusTwo := Number{
		Odd:   false,
		Value: -2,
	}
	fmt.Println("positive?", minusTwo.IsStrictlyPositive())

	// Output:
	// positive? false
}

Values are mutable.

n := Number{
	Odd:   true,
	Value: 17,
}
n.Odd = false // This modifies n.

Functions cannot be generic. They can accept parameters that have interface types. All values implement the blank interface interface{}, so that's a way to pass a parameter of arbitrary type.

func Println(arg interface{}) {
	// Do something with arg.
}

An interface can have 0 or more methods in its method set. When you have a value of type interface, you can call any of the methods in the method set of said interface.

type Stringer interface {
	String() string
}

func Println(s Stringer) {
	s.String() // s has a String method, you can call it.
}

Structs cannot be generic either.

Go has slices. They're a reference to multiple contiguous elements in an underlying array.

v := []int{1, 2, 3, 4, 5}
v2 := v[2:4]
fmt.Printf("v2 = %v\n", v2)

// Output:
// v2 = [3, 4]

The v[2:4] syntax is a slice operation. It makes a new slice that references the same underlying array as the v slice, but includes elements starting with index 2 and ending with index 4.

Functions that can fail typically return two result values, the last being of type error:

u1, err := url.Parse("https://example.com/")
fmt.Println(u1.Host, err)
// Prints "example.com <nil>".

u2, err := url.Parse(":")
fmt.Println(u2, err)
// Prints "<nil> parse ":": missing protocol scheme".

If you want to panic in case of a non-nil error, you can use panic:

u, err := url.Parse(someURL)
if err != nil {
	panic(err)
}
fmt.Println(u.Host)

Or write any custom code to handle the non-nil error:

u, err := url.Parse(someURL)
if err != nil {
	// Custom behavior to handle the parsing error...
}
fmt.Println(u.Host)

Or you can return the error to the caller, so it can deal with the failure appropriately:

u, err := url.Parse(someURL)
if err != nil {
	return nil, err
}
return u.Host, nil

Or annotate the error with relevant context:

u, err := url.Parse(someURL)
if err != nil {
	return nil, fmt.Errorf("failed to parse URL %q, so cannot determine its host: %v", someURL, err)
}
return u.Host, nil

The * operator can be used to dereference pointers, but it's done implicitly by Go when accessing fields or calling methods:

type Point struct {
	X, Y float64
}

func main() {
	p := Point{1, 3}
	pp := &p
	fmt.Println(pp.X, pp.Y)
	// Output: 1 3
}

A function literal represents an anonymous function. It can be assigned to a variable of function type.

var f func(string) = func(planet string) {
	fmt.Println("Hello", planet)
}

Function literals are closures: they may refer to variables defined in a surrounding function. Variables with function type can be passed around and executed.

func main() {
	greeting := "Hello"
	greet := func(planet string) {
		fmt.Println(greeting, planet) // Variable greeting is captured.
	}

	forEachPlanet(greet)

	// Output:
	// Hello Earth
	// Hello Mars
	// Hello Jupiter
}

// forEachPlanet calls f for each planet.
func forEachPlanet(f func(string)) {
	f("Earth")
	f("Mars")
	f("Jupiter")
}

Anything that can be iterated over can be used in a for loop.

for i := 0; i < 3; i++ {
	fmt.Println("index", i)
}

// Output:
// index 0
// index 1
// index 2

There is also a for range loop.

s := []int{52, 49, 21}
for i, v := range s {
	fmt.Printf("index=%v value=%v\n", i, v)
}

// Output:
// index=0 value=52
// index=1 value=49
// index=2 value=21

And with that, I've finished going over the A half-hour to learn Rust post and translating the parts that applied to Go, and skipping over many things that just aren't a part of the Go language (I really enjoying skipping things).

I don't actually think this post is very necessary for anyone who wants to read Go code. One of Go's design goals was to prioritize readability over writability, and it has resulted in a language that is very easy to jump in and read for anyone already familiar with the C-family of languages.

One outcome from that is the joke website https://godecl.org. It was inspired by an actually useful website https://cdecl.org for C declarations. In Go, declarations can be read simply from left to right, and so https://godecl.org doesn't really need to exist other than for fun.

Writing this did take me more than half an hour, and I have not covered all aspects of the Go programming language. For that, I would recommend existing resources such as:

  • The Go tour at https://tour.golang.org. It can be read online and goes over Go language features in a more complete manner.

  • The Go specification at https://golang.org/ref/spec. It's surprisingly small for a language specification, and reading it is a fantastic way to answer questions about the Go language.

There are even more resources at https://golang.org/doc, https://learn.go.dev, and https://golang.org/wiki/Learn.

Finally, I'll say my personal favorite way to learn Go was to read the Go standard library. It's open source, high quality, and very educational about what idiomatic Go code tends to look like. Find your favorite package and see what it looks like on the inside!

If you got this far, I hope you find a programming language you enjoy using for the tasks you want to complete. There are many great choices in the year 2020. Special thanks to Amos for making this experiment possible and inspiring me to write a blog post after 2 years of quiet. 😅

Comments

j3s.sh commented 5 months ago

this is honestly so sick.

Write Preview Markdown
to comment.