Tutorial
General Principles
- Do not write Go code in the same way as other languages.
- Go is a compiled language: The toolchain converts source code and dependencies into native machine instructions.
- Unicode Support: Go natively handles Unicode, allowing text processing in all languages.
Running & Compiling Programs
- Run programs using:
go run
- Compile programs using:
go build
Packages
- A Go program is organized into packages, which consist of one or more
.go
files. - Every Go file starts with a package name that specifies which package it belongs to.
- Package
main
is special—it defines a standalone executable, not a library. - Within package
main
, themain
function is where program execution begins. - Use
import
statements to include only the required packages. import
declarations followpackage
declarations, then function, variable, and type declarations.
Functions
- Defined using
func
, followed by the function name, parameter list, result list, and function body. - The
main
function has an empty parameter and result list. - No semicolons (
;
) are required at the end of statements. - The
{
must be on the same line as the function declaration.
Formatting & Best Practices
- Go enforces formatting: Use
go fmt
to format code. - Use
goimports
to format imports automatically. - Command-line arguments are available via
os.Args
(a slice of strings). - Slices are dynamically sized sequences of array elements:
s[i]
accesses an element.s[m:n]
accesses a contiguous subsequence.
- Import multiple packages in list form rather than separate lines.
- Comments start with
//
and are ignored by the compiler.
Variables & Assignment
Uninitialized variables have zero values (
0
forint
,""
forstring
).Short variable declaration (
:=
) assigns types based on initializer values.Examples: Use the first two forms (preferably the first) for readability.
s := "" var s string var s = "" var s string = ""
+=
concatenates strings, but excessive use can be inefficient. Use standard library methods for large data.
Loops & Control Flow
for
loop structure:- Initialization - runs before the loop starts.
- Condition - evaluated at the start of each iteration.
- Post - executes after the loop body, before the next condition check.
- Any of these can be omitted. If all are omitted, use
break
orreturn
to prevent infinite loops. - The
range
keyword produces:- Index and value of elements.
- Use
_
to ignore the index (for _, v := range list {}
) since Go disallows unused variables.
Maps
A map stores key/value pairs with constant time operations.
Keys are typically of type
string
.Creating a map:
counts := make(map[string]int)
Maps are passed by reference, so modifications persist outside functions.
Input & Output
bufio.Scanner
reads input.input.Scan()
reads a line and removes the newline.input.Text()
retrieves the scanned text.fmt.Printf
formats output with verbs like:%d
(integer)%s
(string)%v
(any value in natural format)
Printf
does not add a newline by default, butPrintln
does.os.Open(filename)
opens a file and returns:- An open file (
os.File
) for reading. - An
error
if opening fails.
- Close the file with
f.Close()
to release resources.
- An open file (
Concurrency
- Go’s concurrency mechanisms: goroutines & channels.
- Useful for tasks like fetching multiple URLs concurrently.
- A goroutine executes a function concurrently.
- A channel is used for communication between goroutines.
main
runs in a goroutine, andgo
creates additional goroutines.- Channel properties:
- Unbuffered channels block sends until a corresponding receive occurs.
- A send blocks if no receive exists.
- A receive blocks if no send exists.
- Web servers & concurrency:
- Each request runs in a separate goroutine.
- To prevent concurrent updates to shared variables, use
mu.Lock
andmu.Unlock
.
Error Handling
Combine
if
anderr
checks to reduce variable scope:if err := r.ParseForm(); err != nil { log.Print(err) }
Output streams (
os.Stdout
,ioutil.Discard
,fmt.Printf
) share a common interface.
Functions & Types
- Anonymous functions defined inline are called function literals.
switch
acts as a multi-way branchif
statement.break
andcontinue
modify flow control.- Named types: The
type
keyword assigns a name to an existing type.
Pointers
&
operator yields a variable’s address.*
operator dereferences a pointer (retrieves the value it points to).- No pointer arithmetic in Go.
Standard Library & Documentation
- Go’s standard library is extensive and should be leveraged.
- Comments are essential as
godoc
extracts them for documentation.
Program Structure
Reserved Keywords and Naming Conventions
- Go has 25 reserved keywords like
if
andswitch
that can only be used where syntax permits, not as variable names. - Variable names must start with either a letter or an underscore.
- If a variable is declared within a function, it is local to that function.
- If a variable is declared outside a function, it is available to all files in that package.
- If a name begins with an uppercase letter, it is exported, meaning it can be accessed outside the package.
- Package names are always in lowercase.
- No limit on name length, but Go convention favors short names.
- Acronyms and initialisms (e.g., ASCII, HTML) maintain consistent casing: e.g.,
htmlEscape
,HTMLEscape
, orescapeHTML
, but notescapeHtml
.
Function Execution
- Execution of a function starts from the first statement and continues until it encounters a return statement or reaches the end of the function.
Variable Declaration
Each variable declaration follows the syntax:
var name type = expression
- Either the
type
or the= expression
part may be omitted, but not both. - There is no uninitialized variable in Go; the zero value mechanism ensures all variables hold a well-defined value.
Initialization Rules
- Package-level variables are initialized before
main()
begins. - Local variables are initialized as their declarations are encountered during function execution.
Alternative Initialization Example
var f, err = os.Open(name) // os.Open returns a file and an error
var
is typically used when:- Explicit type declaration is needed.
- A variable will be assigned a value later.
Short Variable Declarations
i := 100 // an int
var boiling float64 = 100 // a float64
- Multiple variable initialization in a single line:
i, j := 0, 1
- Use multiple initializers only if it improves readability.
Behavior of Short Variable Declarations
- A short variable declaration (
:=
) does not necessarily declare all variables on the left-hand side. - If a variable is already declared in the same lexical block,
:=
acts as assignment. - A short variable declaration must declare at least one new variable.
Pointers in Go
- A pointer stores the address of a variable.
- Not every value has an address, but every variable does.
- The zero value for a pointer is
nil
. - The test
p != nil
checks ifp
points to a variable. - Pointers are comparable; two pointers are equal if they point to the same variable or both are nil.
Flag Parsing
- Call
flag.Parse()
before using flags to update flag variables from default values. - Non-flag arguments are available from
flag.Args()
as a slice of strings. - If
flag.Parse()
encounters an error:- It prints a usage message.
- Calls
os.Exit(2)
to terminate the program.
Variable Lifetime and Scope
- Package-level variables exist throughout program execution.
- Local variables have dynamic lifetimes:
- A new instance is created each time the declaration statement executes.
- The variable exists until it becomes unreachable, after which its storage may be recycled.
- Function parameters and results are local variables created each time the enclosing function is called.
- Garbage collection helps manage memory but does not eliminate the need to consider variable lifetimes.
Type Declarations
- A
type
declaration defines a new named type with the same underlying type as an existing one.
type name underlying-type
- Named types help prevent unintended mixing of values.
Package Initialization
- Package-level variables are initialized in order of declaration.
- Dependencies are resolved first.
- If a package has multiple
.go
files, initialization follows the order of files sorted by name. - Special
init()
function:- Used to initialize variables.
- Cannot be explicitly called or referenced.
- Otherwise, behaves like a normal function.
Scope vs. Lifetime
- Scope: The region of code where a variable is visible and accessible.
- Determined by declaration location.
- Ends when execution moves out of the block.
- Lifetime: The duration a variable exists in memory.
- Determined by where it is allocated.
- Ends when the variable is no longer referenced.
Multiple Declarations
- A program may contain multiple declarations of the same name, provided each is in a different lexical block.
Basic Data Types
Types in Go
Go’s types fall into four categories:
- Basic Types
- Aggregate Types
- Reference Types
- Interface Types
Basic Types
Basic types include:
- Numbers
- Strings
- Booleans
Aggregate Types
Aggregate types are:
- Arrays
- Structs These types form more complicated types by combining values of several simpler ones.
Reference Types
Reference types are a diverse group, which includes:
- Pointers
- Slices
- Maps
- Functions
- Channels
What they have in common is that they all refer to program variables or state indirectly. This means that an operation applied to one reference affects all copies of that reference.
Special Type Aliases
rune
: A synonym forint32
, conventionally used to indicate a Unicode code point. The two names may be used interchangeably.byte
: A synonym foruint8
, emphasizing that the value represents raw data rather than a small numeric quantity.
Data Types in Go
- Unsigned Integers:
uint8
,uint16
,uint32
,uint64
- Signed Integers:
int8
,int16
,int32
,int64
- Floating Point Numbers:
float32
,float64
- Complex Numbers:
complex64
,complex128
- Boolean:
true / false
- String: Immutable sequence of bytes
- Constants: Defined using
const
Complex Numbers
Two complex numbers are equal if their real parts and imaginary parts are both equal.
Data Types in Go
- Arrays are homogeneous (contain elements of the same type) and have a fixed size.
- Structs are heterogeneous (can contain elements of different types) and also have a fixed size.
- Slices and maps are dynamic and can grow as values are added.
Arrays
- An array is a fixed-length sequence of zero or more elements of a particular type.
- Due to their fixed length, arrays are rarely used directly in Go.
- Slices, which can grow and shrink, are more commonly used.
- If an ellipsis (
...
) appears in place of the length, the array length is determined by the number of initializers.
q := [...]int{1, 2, 3}
fmt.Printf("%T\n", q) // "[3]int"
- The size of an array is part of its type, so
[3]int
and[4]int
are different types. - If an array’s element type is comparable, then the array type is comparable too, meaning we can use the
==
operator to compare arrays.
Slices
- Slices represent variable-length sequences whose elements have the same type.
- A slice type is written as
[]T
, whereT
is the type of elements. - Copying a slice creates an alias for the underlying array.
- Slices are not comparable, meaning we cannot use
==
to check for equality. - The zero value of a slice type is
nil
. Anil
slice has:- No underlying array.
- Length and capacity of zero.
- However, there are also non-nil slices of length and capacity zero, such as
[]int{}
ormake([]int, 3)[3:]
. - The built-in function
make
creates a slice with a specified element type, length, and capacity:
make([]T, len)
make([]T, len, cap) // same as make([]T, cap)[:len]
Maps
- A map is a reference to a hash table.
- A map type is written as
map[K]V
, whereK
is the key type andV
is the value type. - All keys in a map must be of the same type, and all values must be of the same type.
- The order of iteration in maps is random.
- The zero value of a map is
nil
, meaning it references no hash table at all.
Structs
- A struct is an aggregate data type that groups together zero or more named values (fields) of arbitrary types into a single entity.
- Fields are usually written one per line, but consecutive fields of the same type may be combined:
type Employee struct {
ID int
Name, Address string
DoB time.Time
Position string
Salary int
ManagerID int
}
- Field order is significant to type identity. Changing the order or combining fields differently defines a new struct type.
JSON Handling in Go
- JSON is a standard notation for sending and receiving structured information.
- Marshaling (converting Go data structures to JSON) is done using
json.Marshal
. Marshal
produces a compact JSON string without extra whitespace.- For neatly indented output, use
json.MarshalIndent
. - A field tag is a string of metadata associated at compile time with a struct field.
Example:
Year int `json:"released"`
Color bool `json:"color,omitempty"`
- Field tags are conventionally written as space-separated
key:"value"
pairs. - The
json
key controls behavior in theencoding/json
package. omitempty
indicates that a field should be omitted if it has a zero value (e.g.,false
for a boolean).
Unmarshaling JSON
- Unmarshaling (decoding JSON into Go data structures) is done using
json.Unmarshal
. - The JSON structure can be mapped to Go structs, allowing selective decoding of only necessary fields.
By understanding these core concepts, you can effectively work with Go’s data structures and JSON handling.
Functions
A function lets us wrap up a sequence of statements as a unit that can be called from elsewhere in a program, perhaps multiple times.
Function Declaration
func name(parameter-list) (result-list) {
body
}
Error Handling
An error may be nil
or not nil
.
When a function call returns an error, it’s the caller’s responsibility to check it and take appropriate action.
Because error messages are frequently chained together, message strings should not be capitalized and newlines should be avoided. When designing error messages:
- Be deliberate, ensuring each message provides a meaningful description of the problem with sufficient and relevant detail.
- Be consistent, so that errors returned by the same function or by a group of functions in the same package are similar in form and can be dealt with in the same way.
Approaches for Error Handling
- Check for
err ≠ nil
and log it. - Retry (e.g., for HTTP requests).
- Exit the program.
Error handling in Go follows a particular rhythm. After checking an error, failure is usually dealt with before success. If failure causes the function to return, the logic for success follows at the outer level instead of being indented within an else
block. Functions tend to have a common structure, with:
- A series of initial checks to reject errors.
- The core functionality of the function appearing at the end, minimally indented.
Function Literals and Anonymous Functions
Named functions can be declared only at the package level, but we can use a function literal to denote a function value within any expression. A function literal is written like a function declaration but without a name following the func
keyword. It is an expression, and its value is called an anonymous function.
Variadic Functions
A variadic function is one that can be called with varying numbers of arguments. The most familiar examples are fmt.Printf
and its variants. Printf
requires one fixed argument at the beginning, then accepts any number of subsequent arguments.
Example:
func sum(vals ...int) int {
total := 0
for _, val := range vals {
total += val
}
return total
}
Defer
The defer
function and argument expressions are evaluated when the statement is executed, but the actual call is deferred until the function that contains the defer
statement has finished—whether normally (by executing a return statement or falling off the end) or abnormally (by panicking). Any number of calls may be deferred; they are executed in the reverse order in which they were deferred.
A defer
statement is often used with paired operations like:
- Open and close.
- Connect and disconnect.
- Lock and unlock.
This ensures resources are released in all cases, no matter how complex the control flow. The right place for a defer
statement that releases a resource is immediately after the resource has been successfully acquired.
Panic and Recovery
Go’s type system catches many mistakes at compile time, but others, like an out-of-bounds array access or nil pointer dereference, require runtime checks. When the Go runtime detects these mistakes, it panics.
Behavior of a Panic
- Normal execution stops.
- All deferred function calls in that goroutine are executed.
- The program crashes with a log message.
This log message includes:
- The panic value (usually an error message).
- A stack trace showing the function calls active at the time of the panic.
This log often has enough information to diagnose the root cause of the problem without running the program again, so it should always be included in a bug report about a panicking program.
Not all panics come from the runtime. The built-in panic
function may be called directly, accepting any value as an argument. A panic is often the best thing to do when some “impossible” situation occurs, such as execution reaching a case that logically can’t happen.
Avoiding Panics
- Avoid panics as much as possible. Just deal with errors.
- Giving up is usually the right response to a panic, but not always.
- It might be possible to recover or at least clean up the mess before quitting.
- For example, a web server encountering an unexpected problem could close the connection rather than leave the client hanging. During development, it might also report the error to the client.
Recovery Considerations
Recovering indiscriminately from panics is a dubious practice because the state of a package’s variables after a panic is rarely well defined or documented. Possible issues include:
- A critical update to a data structure being incomplete.
- A file or network connection being opened but not closed.
- A lock being acquired but not released.
Furthermore, replacing a crash with a log entry may cause bugs to go unnoticed.
Methods
Objects and Methods
An object is simply a value or a variable that has methods, and a method is a function associated with a particular type. An object-oriented program is one that uses methods to express the properties and operations of each data structure so that clients need not access the object’s representation directly.
Calling a method is akin to sending a message to an object. The extra parameter p
is called the method’s receiver. Since the receiver name will be frequently used, it’s a good idea to choose something short and to be consistent across methods. A common choice is the first letter of the type name, like p
for Point
.
// Same thing, but as a method of the Point type
func (p Point) Distance(q Point) float64 {
return math.Hypot(q.X-p.X, q.Y-p.Y)
}
Pointer Receivers
Because calling a function makes a copy of each argument value, if a function needs to update a variable, or if an argument is so large that we wish to avoid copying it, we must pass the address of the variable using a pointer. The same goes for methods that need to update the receiver variable: we attach them to the pointer type, such as *Point
.
func (p *Point) ScaleBy(factor float64) {
p.X *= factor
p.Y *= factor
}
In a realistic program, convention dictates that if any method of Point
has a pointer receiver, then all methods of Point
should have a pointer receiver, even ones that don’t strictly need it.
Furthermore, to avoid ambiguities, method declarations are not permitted on named types that are themselves pointer types:
type P *int
func (P) f() { /* ... */ } // compile error: invalid receiver type
If the receiver p
is a variable of type Point
but the method requires a *Point
receiver, we can use this shorthand:
p.ScaleBy(2)
Struct Embedding
import "image/color"
type Point struct{ X, Y float64 }
type ColoredPoint struct {
Point
Color color.RGBA
}
Encapsulation
A variable or method of an object is said to be encapsulated if it is inaccessible to clients of the object. Encapsulation, sometimes called information hiding, is a key aspect of object-oriented programming.
Go has only one mechanism to control the visibility of names: capitalized identifiers are exported from the package in which they are defined, and uncapitalized names are not. The same mechanism that limits access to members of a package also limits access to the fields of a struct or the methods of a type. As a consequence, to encapsulate an object, we must make it a struct.
Benefits of Encapsulation
- Because clients cannot directly modify the object’s variables, one needs to inspect fewer statements to understand the possible values of those variables.
- Hiding implementation details prevents clients from depending on things that might change, which gives the designer greater freedom to evolve the implementation without breaking API compatibility.
- The third benefit of encapsulation, and in many cases the most important, is that it prevents clients from setting an object’s variables arbitrarily.
Getters and Setters
Functions that merely access or modify internal values of a type, such as the methods of the Logger
type from the log
package, are called getters and setters. However, when naming a getter method, we usually omit the Get
prefix.
Understanding Interface Types
Interface types express generalizations or abstractions about the behaviors of other types. By generalizing, interfaces allow us to write functions that are more flexible and adaptable because they are not tied to the details of a single implementation.
Many object-oriented languages have some notion of interfaces, but what makes Go’s interfaces distinctive is that they are satisfied implicitly.
Nature of Interfaces
An interface is an abstract type. It does not expose the representation or internal structure of its values, nor does it specify the set of basic operations they support. Instead, it only reveals some of their methods. When you have a value of an interface type, you know nothing about what it is; you only know what it can do, or more precisely, what behaviors are provided by its methods.
This freedom to substitute one type for another that satisfies the same interface is called substitutability, a hallmark of object-oriented programming.
Defining Interface Types
An interface type specifies a set of methods that a concrete type must possess to be considered an instance of that interface. The order in which the methods appear in an interface is immaterial; all that matters is the set of methods.
Type Assertions
A type assertion is an operation applied to an interface value. Syntactically, it looks like x.(T)
, where x
is an expression of an interface type and T
is a type, called the “asserted” type. A type assertion checks whether the dynamic type of its operand matches the asserted type.
When to Use Interfaces
Interfaces are only needed when there are two or more concrete types that must be dealt with in a uniform way.
Goroutines and Channels
Go enables two styles of concurrent programming. This chapter presents goroutines and channels, which support Communicating Sequential Processes (CSP), a model of concurrency where values are passed between independent activities (goroutines), while variables are largely confined to a single activity.
Goroutines
In Go, each concurrently executing activity is called a goroutine.
When a program starts, its only goroutine is the one that calls the main
function, referred to as the main goroutine. New goroutines are created using the go
statement, which precedes an ordinary function or method call. A go
statement causes the function to execute in a newly created goroutine, and the statement itself completes immediately.
f() // call f(); wait for it to return
go f() // create a new goroutine that calls f(); don't wait
Channels
If goroutines are the activities of a concurrent Go program, channels are the connections between them. A channel is a communication mechanism that allows one goroutine to send values to another. Each channel is associated with a specific element type.
ch := make(chan int) // ch has type 'chan int'
Channel Operations
A channel has two primary operations: send and receive, collectively known as communications.
- A send statement transmits a value from one goroutine through the channel to another goroutine.
- A receive statement fetches a value from the channel.
- The
<-
operator is used for both sending and receiving.
ch <- x // send statement
x = <-ch // receive expression in an assignment
<-ch // receive statement; result is discarded
Closing a Channel
Channels support a third operation, close, which indicates that no more values will be sent. Any further send operations on a closed channel will panic. Receive operations on a closed channel return the remaining sent values; once exhausted, they return the zero value of the channel’s element type.
close(ch)
Buffered vs Unbuffered Channels
A channel created with make
is an unbuffered channel unless a second argument (capacity) is provided, in which case it becomes a buffered channel.
ch = make(chan int) // unbuffered channel
ch = make(chan int, 0) // unbuffered channel
ch = make(chan int, 3) // buffered channel with capacity 3
Behavior Differences
- Unbuffered Channels: A send operation blocks the sender until a corresponding receive operation occurs.
- Buffered Channels: A send operation only blocks if the buffer is full. The receive operation blocks only if the buffer is empty.
Synchronization
Unbuffered channels synchronize the sending and receiving goroutines, so they are sometimes called synchronous channels.
Example: Closing a Channel in a Pipeline
func main() {
naturals := make(chan int)
squares := make(chan int)
// Counter
go func() {
for x := 0; x < 100; x++ {
naturals <- x
}
close(naturals)
}()
// Squarer
go func() {
for x := range naturals {
squares <- x * x
}
close(squares)
}()
// Printer (in main goroutine)
for x := range squares {
fmt.Println(x)
}
}
Send-Only and Receive-Only Channels
chan<- int
→ Send-only channel<-chan int
→ Receive-only channel
Since close
asserts that no more sends will occur, only the sending goroutine can call it. Attempting to close a receive-only channel results in a compile-time error.
Buffered Channel Queue
A buffered channel maintains a queue of elements, with its maximum size defined at creation.
ch = make(chan string, 3) // Buffered channel of capacity 3
- Send operation: Adds an element to the back of the queue.
- Receive operation: Removes an element from the front.
- If full: The send operation blocks until space is available.
- If empty: The receive operation blocks until a value is sent.
Checking Channel Capacity and Length
fmt.Println(cap(ch)) // Output: 3 (buffer capacity)
fmt.Println(len(ch)) // Output: 2 (number of buffered elements)
Avoiding Leaked Goroutines
Leaked goroutines are not automatically collected, so it’s crucial to ensure that goroutines terminate when no longer needed.
Choosing Between Unbuffered and Buffered Channels
- Unbuffered channels provide stronger synchronization guarantees since every send operation is paired with a receive.
- Buffered channels decouple send and receive operations, which can be beneficial when an upper bound on sent values is known.
- Deadlocks may occur if an insufficient buffer capacity is allocated, preventing the program from making progress.
Definition of Concurrency Safety
A function that works correctly in a sequential program is considered concurrency-safe if it continues to function correctly even when called concurrently—meaning from two or more goroutines—without requiring additional synchronization.
This notion can be extended to a set of collaborating functions, such as the methods and operations of a particular type. A type is concurrency-safe if all its accessible methods and operations are concurrency-safe.
Concurrency Safety in Programs
A program can be made concurrency-safe without requiring every concrete type within it to be concurrency-safe. In fact, concurrency-safe types are the exception rather than the rule. A variable should be accessed concurrently only if the documentation for its type explicitly states that it is safe to do so.
To avoid concurrent access to most variables, we use one of two approaches:
- Confinement: Restricting a variable’s use to a single goroutine.
- Mutual Exclusion: Maintaining a higher-level invariant that prevents simultaneous access by multiple goroutines.
Concurrency Safety in Exported Package-Level Functions
Exported package-level functions are generally expected to be concurrency-safe. Since package-level variables cannot be confined to a single goroutine, functions that modify them must enforce mutual exclusion to ensure safe concurrent access.
Compilation in Go
When we change a file, we must recompile the file’s package and potentially all the packages that depend on it. Go compilation is notably faster than most other compiled languages, even when building from scratch. There are three main reasons for the compiler’s speed:
- Explicit Imports: All imports must be explicitly listed at the beginning of each source file, so the compiler does not have to read and process an entire file to determine its dependencies.
- Dependency Graph: The dependencies of a package form a directed acyclic graph. Because there are no cycles, packages can be compiled separately and potentially in parallel.
- Efficient Object Files: The object file for a compiled Go package records export information not just for the package itself, but for its dependencies too. When compiling a package, the compiler must read one object file for each import but does not need to look beyond these files.
Import Paths
For packages you intend to share or publish, import paths should be globally unique. To avoid conflicts, the import paths of all packages, other than those from the standard library, should start with the Internet domain name of the organization that owns or hosts the package. This also makes it easier to locate packages.
Package Declaration
A package
declaration is required at the start of every Go source file. Its main purpose is to determine the default identifier for that package (called the package name) when it is imported by another package.
Package Naming Conventions
- A package defining a command (an executable Go program) must always have the name
main
, regardless of the package’s import path. This signals togo build
that it must invoke the linker to create an executable file. - Some files in the directory may have the suffix
_test
on their package name if the file name ends with_test.go
. Such a directory may define two packages: the usual one and another one called an external test package.
Suppressing Unused Import Errors
To suppress the “unused import” error, we must use a renaming import where the alternative name is _
, the blank identifier. As usual, the blank identifier can never be referenced.
GOPATH
Structure
GOPATH
has three subdirectories:
src
Directory: Holds source code. Each package resides in a directory whose name relative to$GOPATH/src
is the package’s import path, such asgopl.io/ch1/helloworld
. A singleGOPATH
workspace can contain multiple version-control repositories beneathsrc
, such asgopl.io
orgolang.org
.pkg
Directory: Stores compiled packages.bin
Directory: Holds executable programs likehelloworld
.
Testing
Special Functions in _test.go
Files
Within *_test.go
files, three kinds of functions are treated specially: tests, benchmarks, and examples.
- A test function, whose name begins with
Test
, exercises some program logic for correct behavior.go test
calls the test function and reports the result as eitherPASS
orFAIL
. - A benchmark function, with a name beginning with
Benchmark
, measures the performance of some operation.go test
reports the mean execution time of the operation. - An example function, whose name starts with
Example
, provides machine-checked documentation.
Testing Philosophy
Black-box testing > White-box testing
Go’s approach to testing is unique. It expects test authors to handle most of the work themselves, defining functions to avoid repetition, just as they would for ordinary programs. Testing is not a mere form-filling exercise; it has a user interface too, where the users are its maintainers.
A good test:
- Does not explode on failure but provides a clear and succinct description of the problem, along with relevant context.
- Should not require maintainers to read the source code to decipher a failure.
- Should not give up after one failure but instead report multiple errors in a single run, as patterns of failures can be informative.
Buggy vs. Brittle Tests
- A program that frequently fails when encountering valid inputs is buggy.
- A test that fails due to a sound change in the program is brittle.
Just as a buggy program frustrates its users, brittle tests exasperate maintainers. The most brittle tests, which fail for almost any change (good or bad), are called change detector or status quo tests. These consume more time than they save.
How to avoid brittle tests:
- Test only the properties that matter.
- Prefer testing stable interfaces over internal functions.
- Be selective in assertions.
- Avoid exact string matches; instead, check for stable substrings.
- Write helper functions to simplify complex outputs so assertions remain reliable.
“Testing shows the presence, not the absence, of bugs.”
Benchmark Functions
Example Benchmark Function
import "testing"
func BenchmarkIsPalindrome(b *testing.B) {
for i := 0; i < b.N; i++ {
IsPalindrome("A man, a plan, a canal: Panama")
}
}
Running Benchmarks
go test -bench=.
Premature optimization is the root of all evil.
Programmers often waste time optimizing noncritical parts of their programs. This not only reduces maintainability but also increases debugging time. We should ignore minor efficiencies 97% of the time and focus on writing clear, maintainable code instead.
Special Functions in go test
- Benchmark
- Coverage
- Example
Example Function
func ExampleIsPalindrome() {
fmt.Println(IsPalindrome("A man, a plan, a canal: Panama"))
fmt.Println(IsPalindrome("palindrome"))
// Output:
// true
// false
}
Benefits of Example Functions
- Documentation - Examples serve as self-explanatory documentation.
- Executable Tests - Examples are automatically run by
go test
. - Hands-on Experimentation - Examples can be edited and tested interactively in the Go Playground.