Go Programming Notes
Go Programming Notes
These are my notes from the Go: The Complete Developer's Guide (Golang)
course. You can find the actual codes at my GitHub repo
.
Getting Started with Go
Five Important Questions
Even a simple Go program reveals five fundamental aspects of the language:
|
|
Looking at this program raises five essential questions that form the foundation of understanding Go:
- How do programmers run code in a Go project?
- What does
package main
signify? - What purpose does
import "fmt"
serve? - What is the
func
keyword’s role? - How should code be organized in Go files?
The answers to these questions reveal how Go programs are structured and executed.
Go Packages
Every Go file must declare its package membership. Two distinct types of packages exist in Go:
Executable Packages:
1 2 3 4 5
package main func main() { // Entry point for executable programs }
The word “main” is special - it tells Go to create a program that can be run directly. An executable package must contain a
main()
function.Reusable Packages:
1 2 3 4 5
package cards func newDeck() { // Helper code for other packages }
Any package name besides “main” creates a reusable package, meant to provide helper code and functionality to other packages.
Import Statements
Imports give access to code from other packages. For example:
|
|
Or using the grouped syntax:
|
|
Each import makes that package’s functionality available in the current file. The fmt
package, short for “format”, provides functions for formatting and printing output.
File Organization
Go enforces a specific file structure:
- Package declaration at the top
- Import statements
- Code/function declarations
This organization isn’t merely conventional - it’s required by the Go compiler:
|
|
Working with Variables
Variable Declarations
Go provides multiple ways to declare variables, each serving different purposes:
|
|
The :=
operator both declares and initializes a variable, but it can only be used inside functions. The var keyword works anywhere. Go assigns zero values to variables that aren’t explicitly initialized:
- string
""
- int
0
- bool
false
Functions and Return Types
Functions in Go must declare both their parameter types and return types:
|
|
The return type appears after the parameter list. If a function declares a return type, the Go compiler ensures it always returns that type of value.
Slices and For Loops
Slices provide dynamic arrays in Go. The for loop with range makes iteration straightforward:
|
|
The range keyword provides both the index and value of each element. Using _
ignores values you don’t need:
|
|
Object-Oriented Programming vs Go Approach
When creating a deck of cards, an object-oriented language might use a class with properties and methods:
|
|
Go takes a fundamentally different approach. Instead of creating classes with properties and methods, Go extends existing types and adds behavior to them:
|
|
This difference is significant. Go favors composition over inheritance, and its type system reflects this philosophy. Rather than creating complex hierarchies of classes, Go programmers combine simple types to build more complex ones.
Custom Type Declarations
Creating a new type in Go involves extending an existing type:
|
|
This line creates a new type called deck
that extends the behavior of a slice of strings. This new type inherits all the capabilities of a string slice but can also have its own specific methods. This approach demonstrates Go’s focus on simplicity and explicit behavior.
Receiver Functions
Receiver functions add methods to custom types:
|
|
The (d deck)
before the function name is called a receiver. It means any variable of type deck
now has access to the print method. This can be called like:
|
|
By convention, the receiver variable uses a one or two letter abbreviation that matches the type name (d for deck). This keeps the code concise while maintaining readability.
Creating a New Deck
Creating a New Deck
The newDeck function demonstrates how to create and initialize a custom type:
|
|
This function shows several important Go concepts:
- Creating an empty deck using the custom type
- Using nested loops to generate combinations
- Building strings by concatenation
- Returning the custom type
Slice Range Syntax
Go provides special syntax for working with parts of a slice:
|
|
The range syntax [low:high]
creates a slice from index low
up to (but not including) index high
. Omitting a number means “start” or “end” respectively:
cards[0:5]
- first 5 cardscards[5:]
- everything from index 5 onwardscards[:5]
- first 5 cards (same as [0:5])
Multiple Return Values
Functions in Go can return multiple values:
|
|
This enables functions to return related pieces of data together. When calling such functions, you must receive all return values:
|
|
This feature eliminates the need for special container objects or complex return types, making the code more straightforward and explicit.
Working with Files and Data
Byte Slices
Working with files in Go requires understanding byte slices. A byte slice represents a sequence of raw binary data:
|
|
Converting between strings and byte slices is common when working with files because file operations deal with raw binary data. The conversion is explicit, reflecting Go’s philosophy of making operations visible and clear.
Deck to String
Before saving a deck to a file, it needs to be converted to a string:
|
|
This method:
- Converts the deck to a slice of strings using type conversion
- Joins all cards with commas between them
- Returns a single string representation
Saving Data to Hard Drive
Writing data to files uses the ioutil
package:
|
|
The process involves:
- Converting the deck to a string
- Converting the string to a byte slice
- Writing the bytes to a file
- Using permissions (0666) to set file access rights
Error Handling
In Go, error handling is explicit and straightforward. When reading from files or performing operations that might fail, Go requires direct handling of potential errors. Consider this example from the cards project:
|
|
This error handling pattern appears throughout Go programs. The if err != nil
check is crucial because it allows programs to respond appropriately when things go wrong. In this case, if the file can’t be read, the program prints the error and exits rather than continuing with invalid data.
Random Number Generation and Shuffling
Implementing the shuffle functionality for our deck reveals important aspects of Go’s random number generation. A naive implementation might look like this:
|
|
However, this implementation has a subtle problem. Go’s random number generator needs a seed value to generate truly random numbers. Without setting a seed, the same sequence of random numbers appears every time. Here’s the correct implementation:
|
|
This version uses the current time as a seed, ensuring different random sequences on each program run. The difference illustrates how Go encourages explicit handling of program state.
Testing
Go includes a testing framework in its standard library. Test files are identified by the _test.go
suffix. Here’s an example testing the deck creation:
|
|
This test demonstrates several testing principles in Go:
- Test functions start with “Test”
- They take a single parameter of type
*testing.T
- Errors are reported using the
t.Errorf
function - Tests verify specific, expected behaviors
Testing file operations requires cleanup to ensure tests remain reliable:
|
|
This test demonstrates proper cleanup by removing test files both before and after the test runs. This practice prevents test files from accumulating and ensures each test starts with a clean state.
Understanding Structs
Structs provide a way to create composite types that group related data together. Unlike the deck type, which extended a slice, structs combine different types of data:
|
|
This structure shows how Go allows composition of types. Instead of inheritance, Go encourages building complex types by combining simpler ones. When creating instances of structs, Go provides several syntax options:
|
|
The second form demonstrates Go’s zero value concept - each field gets a default value appropriate to its type. This predictable initialization is a key feature of Go’s design.
Maps
Maps provide a way to create collections of key-value pairs. Unlike structs, which have a fixed set of fields determined at compile time, maps allow dynamic key-value associations:
|
|
The syntax map[keyType]valueType
specifies that all keys must be of one type and all values must be of another type. This type consistency is enforced by the compiler, preventing type-related errors at runtime.
Maps can also be created using the make function:
|
|
This approach creates an empty map ready for use. Unlike structs, maps must be initialized before use - the zero value of a map is nil, and attempting to add to a nil map causes a runtime panic.
Adding and removing values from maps uses straightforward syntax:
|
|
Maps differ from structs in several key ways:
- Maps require all keys/values to be the same type
- Maps are reference types (passed by reference)
- Map keys are indexed and can be iterated over
- Map keys don’t need to be known at compile time
Interfaces
Interfaces solve a common problem in programming: how to make code reusable across different types.
|
|
Instead of writing separate functions to handle each type of bot, interfaces allow writing a single function that works with any type implementing the required methods:
|
|
This demonstrates a fundamental principle in Go: interface satisfaction is implicit. Any type that implements the required methods automatically satisfies the interface - no explicit declaration is needed.
The HTTP package demonstrates practical interface usage through the Reader and Writer interfaces:
|
|
The response body implements the Reader interface, which specifies a Read method:
|
|
This standard interface allows different types (files, network connections, strings) to provide data in a consistent way. The Writer interface serves a similar purpose for output:
|
|
These interfaces enable powerful composition, as seen in the io.Copy function:
|
|
This function works with any Reader as input and any Writer as output, demonstrating Go’s approach to code reuse through interfaces.
Concurrency
Go provides powerful tools for concurrent programming through goroutines and channels. A goroutine is a lightweight thread managed by Go’s runtime:
|
|
The go
keyword launches a function in a new goroutine. The Go scheduler manages these goroutines, multiplexing them onto operating system threads. This makes concurrent programming more manageable than direct thread manipulation.
Channels provide safe communication between goroutines:
|
|
The channel operations block until both sender and receiver are ready, providing synchronization without explicit locks. This blocking behavior helps prevent race conditions and makes concurrent code easier to reason about.
Function Literal
Function literals (anonymous functions) are often used with goroutines:
|
|
When using function literals with goroutines, variables should be passed as parameters rather than accessed from the enclosing scope. This prevents issues with shared variable access:
|
|
Channel iteration provides a clean way to process a stream of values:
|
|
This pattern creates continuous monitoring, with each check launching after a delay. The for-range loop continues until the channel is closed, and each iteration spawns a new goroutine to handle the check independently.
Go’s concurrency features work together to enable clear, efficient concurrent programs. Goroutines provide lightweight concurrency, channels ensure safe communication, and the Go runtime manages the actual execution efficiently.