Golang
Created: 2020-06-11 08:51:39 -0700 Modified: 2022-01-30 10:26:22 -0800
- Resources
- There’s a Visual Studio Code extension here. Note that it used to be run by Microsoft, but it’s since changed hands to the Go team (reference).
- As you go, you’ll be prompted to install other extensions like go-outline or dlv. These get installed in GOBIN if it exists).
- If you’re not prompted to install these, then do ctrl+shift+P → Go: Install/Update Tools → check all of the ones that you want (and then reload the window). For example, without go-outline, you can’t use ctrl+R to navigate to different functions (although maybe this is just about needing to save the file first).
- As you go, you’ll be prompted to install other extensions like go-outline or dlv. These get installed in GOBIN if it exists).
- golang.org resources
- Their getting-started guide is all located here. Part of this guide is their tour, which covers methods, concurrency, etc.
- Effective Go - tips for writing clear, idiomatic Go.
- Playground (V2 playground)
- The Go playground can be used for testing code quickly. Code run there is deterministic, meaning you’ll get the same result every time (e.g. random numbers or even time.Now()). More information about this is here.
- goplay.space has a dark theme, help look-up by double-clicking, and some other features if you prefer that.
- Books
- “The Go Programming Language” by Donovan and Kernighan is apparently considered to be the best.
- There’s a Visual Studio Code extension here. Note that it used to be run by Microsoft, but it’s since changed hands to the Go team (reference).
- Selecting a specific version
- Go has built-in (reference)
- Properties of the language
- Great talk by Rob Pike, one of the creators of the language, on the simplicity of the language
- There’s a garbage collector, but it isn’t exposed to the programmer
- Go is fast because it’s compiled (so there’s no REPL).
- Interfaces are just methods; there won’t be any extra data with them.
- Rarely will you find multiple ways to program the same thing.
- Great talk by Rob Pike, one of the creators of the language, on the simplicity of the language
- Go is camelCased (although PascalCase is required for exporting names).
- Game engines/rendering:
- From my relatively basic research, these seem like the best options if you’re going to do anything with game development:
- SDL bindings: GitHub, example code. This isn’t exactly a game engine, but it has input-handling, rendering, audio, text rendering, and some other features.
- raylib-go: GitHub: input handling, sound, shaders, physics.
- From my relatively basic research, these seem like the best options if you’re going to do anything with game development:
- Terminology
- A package is a collection of source files in the same directory that get compiled together
- A module is a collection of packages that are distributed together.
- An import path is a string used to import a package.
- Modules
- Modules are relatively new (2018-2019) (reference).
- There is no centralized repo for Go modules like there is for Node with NPM.
- Getting started: the first thing you’ll do for most Golang projects is something like “go mod init yourmodulename”. This will create a “go.mod” file which will you install dependencies. After that, you can paste in a “hello world”-style program, do “go run main.go” and it’ll automatically install what’s imported.
- When you use “go install”, “go build”, or “go run”, it will automatically download the remote module into $GOPATH/pkg/mod. These downloaded modules are versioned and will be reused by all modules that require the same version. This saves a lot of disk space!
- “go get” vs. “go install”: see this StackOverflow post
- “go get” is like “npm install”, but it doesn’t go into a local “node_modules” equivalent, which is fine due to how Go tries to reuse versioned packages across multiple folders.
- “go mod” will install them into a “vendor/” folder (e.g. “go mod init adamlearns.live/m”). Doing so will also create a “go.mod” file to keep track of the dependencies just like package.json would for Node.
- By running “go install <module path>”, you will get a binary in GOPATH/bin.
- The Go programmers in chat said that they don’t actually use “go install” or an IDE handles it for them. Instead, people use “go build” or “go run” when developing a single module. “go get” and “go build” will both also get dependencies.
- Testing
- Make a file ending in “_test.go” (note that it does not need to match the name of an existing file), then prefix all functions with “Test”, e.g. “TestReverseString”. They need to take in a “t *testing.T”, which is essentially the test framework and gives you functions like t.Error and t.Fail.
- Using the command “go test ./…” will test everything underneath the current directory.
- Packages
- Capitalized names are automatically exported from a package.
- Packages start running the code in “package main”.
- Every package can define its own “init” function (no args, no return value) that will get called after the package is imported (reference). This is as opposed to the main function which is only called for the executable’s entrypoint (reference).
- The convention is that the package name matches the last element of the import path.
- Return values
- You can specify multiple return values by separating them with a comma, e.g. “return num1, num2”. The function signature then needs to look like “(int, int)” with parentheses.
- You can name return values and then use a “naked” return to automatically return them (reference). This is only recommended for short functions apparently.
- “if” statement (reference)
- Parentheses are optional, but curly braces are required.
- You can have a statement before the condition of the “if” that will only be in-scope for the “if” statement itself (and its body and “else” statement) (reference). Here’s a common example of why you may want this:
“err” won’t be in scope after the closing curly brace, meaning nothing else can use it, and it can be redefined.
- Deferring function execution (reference)
- The “defer” keyword lets you defer execution of a function until the surrounding function returns. The arguments to the deferred function will still be evaluated before its execution. This is typically used to clean up state, e.g. acquiring locks, closing connections, etc. This is especially helpful if a function has multiple “return” statements in it.
- Deferred functions execute in inverse order (reference):
- “switch” statements (reference)
- No “break” is needed. It’s assumed by default, and if you want to fall through to other cases, you explicitly type “fallthrough”.
- The values you’re comparing against don’t need to be constants and don’t need to be integers.
- The individual cases stop executing when a match is found (reference). This means that if you have “case functionWithSideEffects()” after a match, then that function will never be hit.
- Switch can be used without a condition at all, in which case it’s assumed to be “switch true”. At that point, it’s a cleaner-looking if-else chain and is apparently frequently used in some scenarios (reference).
- “for” loop (reference)
- There’s only one construct for this, and it’s in the form “for init; cond; post { body }” (don’t use parentheses, curly braces are required). Each part is optional, so an infinite loop could be expressed as “for ;; { body }“. This is how you would accomplish a “while” loop, although you’d even omit the semicolons (reference).
- Garbage collection
- Escape analysis (reference): this is a function of the garbage collector where it can decide whether a variable should be allocated on the heap or the stack (and remember that heap-allocated variables later need to be garbage-collected).
- Types
- Basic types are bool, string, byte, rune, and a bunch of numbers like int32, float64, complex128 (reference).
- A rune is an alias for int32 (reference). Saying something like “const a = ‘a’;” (notice the single quotes) will make a rune whose value is 97. When you do something like reflect.TypeOf(someRune), you’ll see “int32”.
- They suggest using “int” whenever you need an integer value unless you have a specific reason for a size or for an unsigned value (reference)
- If you don’t initialize a primitive, it will get its “zero” value (0 for numeric types, false for bool, and an empty string for strings).
- Type conversions can be done via TYPE(value), where “type” matches the primitive’s name exactly, e.g. “float64(5)” (reference)
- For consecutive named function parameters or variable declarations of the same type, you only have to specify the type on the final param/var (reference):
- Basic types are bool, string, byte, rune, and a bunch of numbers like int32, float64, complex128 (reference).
- Types can be inferred at initialization time (shown below is the initialization of two separate variables on a single line):
- You can use ”:=” inside of a function only for implicitly typed variables, e.g.
- You can alias your own types with the “type” keyword (reference): “type MyFloat float64”
- Empty interfaces are like the “any” type in TypeScript in that they’ll accept any type. They look like “interface{}” (i.e. literally the word “interface” with curly braces after it). This is how functions like fmt.Printf accept arguments of any type (and how you’d write your own pretty-print library, for example).
- You can get at the underlying type with “.(type)” (reference)
- For maps, you’ll need to use reflection (reference) to get at individual key/value types, typically with reflect.TypeOf. For example:
Output:
- To drill into this more, you would do something like this to programmatically figure out whether you even have a map:
-
reflect.Indirect is probably easiest to understand by looking at its simple source code.
-
Go V2 will apparently have generics (reference).
-
You can do type assertions to test or ensure that a type matches a particular interface (reference). The syntax is something like this:
- You can use a type switch to allow for several different types. This uses empty interfaces and takes the form:
- Pointers (reference)
- Syntax is with * and & just like in C except that there’s no pointer arithmetic (reference).
- A pointer’s default value is nil.
- Pointers are essentially used the same way they are in C—you may want to pass something by reference instead of by value. However, maps and slices are automatically passed by reference without even needing an explicit pointer (reference), so this works:
-
In fact, passing a map by a pointer results in a value being
-
To access a member of a struct through a pointer, you would typically have to write ”(*p).X”, but that’s so common (just like in C++ with the arrow (”->”)) that Go allows you to just type “p.X” (reference).
-
Structs (reference)
- Syntax example
- You can name parameters when initializing (reference):
-
Why use pointers in structs? Good reasons listed here.
-
Arrays (reference)
- Sample definition: var a [10]int
- Arrays cannot be resized.
- An uninitialized array will get all of the “zero” values of the elements, e.g. “var s [3]bool” will be [false false false].
- Array literals (reference) - use these to build an array at the same time that you say its size, e.g.:
[3]bool{true, true, false}
- Slices (reference)
- Slices are dynamically-sized, flexible views into the elements of an array. Sample definition:
- A slice does not actually store any data. It’s like a reference to the array (reference). Modifying the elements of a slice will also modify the underlying array and any other slices that reference those elements.
- Slice literals - very similar to array literals in that it creates an array, but then you get a slice that references the whole array:
- You can omit either of the bounds for a slice (reference) (just like you can in Python)
- Slices have a length and a capacity (reference). The length refers to the size of the slice itself (i.e. how many elements it contains), whereas the capacity is the size of the underlying array.
- An uninitialized slice will be nil (reference)
- You can use the make function to create dynamically-sized arrays (reference). “make” has three parameters (reference): the array type, the length, and the optional capacity.
- Slices can contain any type, including other slices (reference). This is like having an array of arrays.
- You can append to a slice using the “append” function (reference). This will not change the slice that you pass in; instead, you have to use the return value of “append”.
- Range (reference)
- You can use a “for” loop to iterate over a slice:
-
If you only want the index, just omit the second variable (“v” above).
-
If you only want the value, specify ”_” instead of an alphanumeric name.
-
Map (reference)
- Sample definition: var m map[string]Vertex
- The key type is “string” and the value type is “Vertex”
- Map literals (reference):
- Sample definition: var m map[string]Vertex
Note that you could have omitted “Vertex” in the initialization (i.e. you remove 2 of the 3 instances of “Vertex”) because the type is known.
- Examples of making empty maps rather than nil maps:
-
CRUD operations on maps: see this link
-
Maps are key/value pairs. They start as nil and, like slices, can be made via “make”.
-
Functions (reference)
- Functions are first-class, meaning they can be used as values, e.g. being passed to other functions.
- Anonymous functions can be declared like this (reference)
- Closures (reference) - functions can refer to variables outside of their defining scope.
- There is no concept of function overloading. Go’s internals accomplish something like this apparently with type assertions (reference); you’ll see code like this:
The first one doesn’t cause a panic because Go sees that you’re using both return values of the type assertion. However, if you wanted to get this logic from your own functions, apparently you wouldn’t be able to.
- Methods (reference)
- Methods are functions with a special receiver argument. They’re essentially Go’s concept of classes. Syntax (reference):
- Notes
- Vertex is just a struct. You can make methods on any type as long as it’s defined in the same package, meaning you’ll have to alias built-in types like “float” using the “type” keyword if you want a float method.
- To call this Abs function, you would do something like this
-
You don’t have to capitalize method names, but you probably want them to be exported.
-
This is pretty much exactly the same as just writing Abs to take in a Vertex rather than using the special receiver argument.
-
It’s more common to see pointer receivers (i.e. receivers that are pointers to a type) than value receivers since the pointer would allow modification of the “class instance” (these aren’t actually classes, but that’s the behavior this is similar to).
- When you have a pointer receiver and call it with something like v.Abs(), Go will automatically translate it for you into (&v).Abs() (reference). Likewise, when you have a value receiver and call it with a pointer, it will turn it into (*v).Abs() (reference).
- Pointer receivers are necessary if you want to modify the value that the receiver points to. However, they’re also good for avoiding unnecessary copying since the arguments are passed by reference. Whatever you choose (pointer vs. value), don’t mix the two for methods on a particular type (reference).
-
You can call methods with nil values and not get something like a NullPointerException. E.g.
- Interfaces (reference)
- An interface is a set of method signatures.
- Golang uses structural typing (like TypeScript) (reference). This means that a type essentially just has to satisfy the right shape as opposed to matching in name (like in “nominative”/“nominal” systems which are based on the name, e.g. a function that expects a width and height wouldn’t allow a Rectangle in a nominative system because it doesn’t match exactly in name).
- As a result, there is no “implements” keyword in Go; if your type matches the interface’s signature, then it implicitly implements the interface.
- Here’s an example from the tour. This example is a little bit difficult to understand if you’re new to Go since you’re fighting against syntax and their unhelpful one-letter names:
This is a method named “M” that has a receiver named “t” of type “T”, which they declare is a struct. They use this to show that “T” implements their interface since the interface only requires the type to have an “M” function.
- Interface values (reference) have a concrete type since there’s no explicit implementation of an interface. Just like with any variable, there’s a value and a type, but you’ll never see the type match the name of an interface.
Calling any function/method “i.foo” will be a runtime error since there is no concrete type.
-
I wrote notes about empty interfaces (i.e. “interface{}”) under “Types”.
-
To get something like toString() in other languages, you’ll implement the Stringer interface (reference). Thanks to structural typing, this just requires that you make a “String” method that returns a string.
-
Readers (reference) - common interfaces are io.Reader and io.Writer. Find out more at the reference link.
- It’s common for a Reader to wrap another Reader to modify the stream in some way (reference).
-
Errors (reference)
- Just like with the fmt.Stringer interface, there’s a built-in “error” interface that has a single method “Error()” that returns a string. This link from the tour has an example of defining your own error and returning it from a function.
- The convention is to always return the error last from a function that may error. Thus, as a caller of such a function, you’ll typically do something like “returnValueYouWant, err := someFunctionCallThatMayError()” and then check “err” against nil.
- Making a new error: just use errors.New(“some message here”) for an unformatted error or fmt.Errorf(“Wanted %d apples”, 5) for a formatted one. Some style guides recommend always using fmt.Errorf so that you don’t need to change constructions based on whether or not you have formatting arguments.
- Go’s errors vs. exceptions (reference): there’s a whole conversation online about Go’s errors vs. checked exceptions (like Java). The reference link has plenty of information; what I added below is just to clarify.
- Checked vs. unchecked exceptions: first of all, the “checked” part refers to the fact that the exception is checked by the compiler (i.e. the method must annotate that it throws the exception and the caller must do something about catching it). Java also has unchecked exceptions (reference), which are exceptions that explicitly inherit from RuntimeException or Error; these exceptions are not checked by the compiler, so any programmer can write code without bothering to specify or catch these exceptions. One common unchecked exception is a NullPointerException, meaning you can call a function defined like “public void greet(String message)” like “greet(null)” and your program will compile just fine.
- Comparison between the languages: Go only forces you to follow a function’s contract. E.g. if a function returns an error, then callers will need to store that return value (even if it’s just in the blank identifier (the underscore)). In that sense, it’s similar to Java since you could always catch an exception and just do nothing about it. However, if f() calls g() and g() calls h(), then f() doesn’t need to handle h()‘s potential errors (like it might in Java). Also, there’s no stack unwinding, so there’s no performance overhead for the runtime. Go doesn’t have a need for another set of control structures in the form of try/catch/finally.
-
Goroutines (reference)
- Goroutines are lightweight threads managed by the Go runtime. Sample syntax: go f(x, y, z) - this starts a new thread running the function specified (“f” in this case).
- The evaluations of each individual component (“f”, “x”, “y”, and “z”) are all done in the current thread, but the function execution itself will take place in the new thread.
- All threads will share the same address space, so you’ll need to synchronize access (reference).
- sync.Mutex (reference)
- Goroutines are lightweight threads managed by the Go runtime. Sample syntax: go f(x, y, z) - this starts a new thread running the function specified (“f” in this case).
-
Channels (reference)
- You can send and receive values through channels. By default, both operations block until the other side is ready (see “select statements” below for information on non-blocking communication). Sample syntax
-
Buffered channels will block based on a buffer size that you pass to “make” (sends block when full, receives block when empty).
-
Range and close (reference) - a sender can close a channel to indicate that no more data should be sent over it. By using “range CHANNEL”, where CHANNEL is a specific channel, you will receive values from the channel until it’s closed.
-
Select statements (reference) - select lets you wait on multiple communication operations. It blocks until any of the given channels have data on them. If multiple have data, it chooses one at random. This is sort of like “Promise.race” in JavaScript. The default case is run if no other case is ready (reference).
- Select + default (reference) - if your “select” has a “default” case and none of the non-default cases are matched, then communication will not block.
-
Sets
- Sets don’t exist in Go, so if you want to test inclusion, use a map of bools:
Exercises
Section titled ExercisesThis is the code I ended up with for the various exercises in the documentation. I don’t know if any of it is good since I didn’t know Go before doing this.
Exercise: Sqrt (reference)
Section titled Exercise: Sqrt (reference)Exercise: Slices (reference)
Section titled Exercise: Slices (reference)Exercise: Errors (reference)
Section titled Exercise: Errors (reference)Exercise: Readers (reference)
Section titled Exercise: Readers (reference)Exercise: rot13Reader (reference)
Section titled Exercise: rot13Reader (reference)(I cut a corner by only doing rot13 for lowercase ASCII letters)
Exercise: Images (reference)
Section titled Exercise: Images (reference)Exercise: Equivalent Binary Trees (reference)
Section titled Exercise: Equivalent Binary Trees (reference)Note: this code doesn’t need to sort and doesn’t actually solve the problem, but it has the foundation for it.
Troubleshooting
Section titled TroubleshootingCan’t build on Windows - “cc1.exe: sorry, unimplemented: 64-bit mode not compiled in”
Section titled Can’t build on Windows - “cc1.exe: sorry, unimplemented: 64-bit mode not compiled in”You need mingw-w64, which for some reason was annoyingly difficult to figure out how to install (at least an hour), so that’s why I write these troubleshooting steps:
- Download mingw-w64-install.exe from here
- Run it
- Version: choose latest
- Architecture: x86_64
- Threads: posix
- Exception: seh
- Build revision: 0
- Add “C:Program Filesmingw-w64x86_64-8.1.0-posix-seh-rt_v6-rev0mingw64bin” to your PATH
(I know those steps are easy and relatively fast, but finding them took forever)