Go in a nutshell
By design, Go has a simple syntax. Its designers wanted to create a language that is clear, concise, and consistent with few syntactic surprises. When reading Go code, keep this mantra in mind: what you see is what it is. Go shies away from a clever and terse coding style in favor of code that is clear and readable as exemplified by the following program:
// This program prints molecular information for known metalloids // including atomic number, mass, and atom count found // in 100 grams of each element using the mole unit. // See http://en.wikipedia.org/wiki/Mole_(unit) package main import "fmt" const avogadro float64 = 6.0221413e+23 const grams = 100.0 type amu float64 func (mass amu) float() float64 { return float64(mass) } type metalloid struct { name string number int32 weight amu } var metalloids = []metalloid{ metalloid{"Boron", 5, 10.81}, metalloid{"Silicon", 14, 28.085}, metalloid{"Germanium", 32, 74.63}, metalloid{"Arsenic", 33, 74.921}, metalloid{"Antimony", 51, 121.760}, metalloid{"Tellerium", 52, 127.60}, metalloid{"Polonium", 84, 209.0}, } // finds # of moles func moles(mass amu) float64 { return float64(mass) / grams } // returns # of atoms moles func atoms(moles float64) float64 { return moles * avogadro } // return column headers func headers() string { return fmt.Sprintf( "%-10s %-10s %-10s Atoms in %.2f Grams\n", "Element", "Number", "AMU", grams, ) } func main() { fmt.Print(headers()) for _, m := range metalloids { fmt.Printf( "%-10s %-10d %-10.3f %e\n", m.name, m.number, m.weight.float(), atoms(moles(m.weight)), ) } }
golang.fyi/ch01/metalloids.go
When the code is executed, it will give the following output:
$> go run metalloids.go
Element Number AMU Atoms in 100.00 Grams
Boron 5 10.810 6.509935e+22
Silicon 14 28.085 1.691318e+23
Germanium 32 74.630 4.494324e+23
Arsenic 33 74.921 4.511848e+23
Antimony 51 121.760 7.332559e+23
Tellerium 52 127.600 7.684252e+23
Polonium 84 209.000 1.258628e+24
If you have never seen Go before, you may not understand some of the details of the syntax and idioms used in the previous program. Nevertheless, when you read the code, there is a good chance you will be able to follow the logic and form a mental model of the program's flow. That is the beauty of Go's simplicity and the reason why so many programmers use it. If you are completely lost, no need to worry, as the subsequent chapters will cover all aspects of the language to get you going.
Functions
Go programs are composed of functions, the smallest callable code unit in the language. In Go, functions are typed entities that can either be named (as shown in the previous example) or be assigned to a variable as a value:
// a simple Go function func moles(mass amu) float64 { return float64(mass) / grams }
Another interesting feature about Go functions is their ability to return multiple values as a result of a call. For instance, the previous function could be re-written to return a value of type error
in addition to the calculated float64
value:
func moles(mass amu) (float64, error) { if mass < 0 { return 0, error.New("invalid mass") } return (float64(mass) / grams), nil }
The previous code uses the multi-return capabilities of Go functions to return both the mass and an error value. You will encounter this idiom throughout the book used as a mean to properly signal errors to the caller of a function. There will be further discussion on multi-return value functions covered in Chapter 5, Functions in Go.
Packages
Source files containing Go functions can be further organized into directory structures known as a package. Packages are logical modules that are used to share code in Go as libraries. You can create your own local packages or use tools provided by Go to automatically pull and use remote packages from a source code repository. You will learn more about Go packages in Chapter 6, Go Packages and Programs.
The workspace
Go follows a simple code layout convention to reliably organize source code packages and to manage their dependencies. Your local Go source code is stored in the workspace, which is a directory convention that contains the source code and runtime artifacts. This makes it easy for Go tools to automatically find, build, and install compiled binaries. Additionally, Go tools rely on the workspace
setup to pull source code packages from remote repositories, such as Git, Mercurial, and Subversion, and satisfy their dependencies.
Strongly typed
All values in Go are statically typed. However, the language offers a simple but expressive type system that can have the feel of a dynamic language. For instance, types can be safely inferred as shown in the following code snippet:
const grams = 100.0
As you would expect, constant grams would be assigned a numeric type, float64
, to be precise, by the Go type system. This is true not only for constants, but any variable can use a short-hand form of declaration and assignment as shown in the following example:
package main import "fmt" func main() { var name = "Metalloids" var triple = [3]int{5,14,84} elements := []string{"Boron","Silicon", "Polonium"} isMetal := false fmt.Println(name, triple, elements, isMetal) }
Notice that the variables, in the previous code snippet, are not explicitly assigned a type. Instead, the type system assigns each variable a type based on the literal value in the assignment. Chapter 2, Go Language Essentials and Chapter 4, Data Types, go into more details regarding Go types.
Composite types
Besides the types for simple values, Go also supports composite types such as array
, slice
, and map
. These types are designed to store indexed elements of values of a specified type. For instance, the metalloid
example shown previously makes use of a slice, which is a variable-sized array. The variable metalloid
is declared as a slice
to store a collection of the type metalloid
. The code uses the literal syntax to combine the declaration and assignment of a slice
of type metalloid
:
var metalloids = []metalloid{ metalloid{"Boron", 5, 10.81}, metalloid{"Silicon", 14, 28.085}, metalloid{"Germanium", 32, 74.63}, metalloid{"Arsenic", 33, 74.921}, metalloid{"Antimony", 51, 121.760}, metalloid{"Tellerium", 52, 127.60}, metalloid{"Polonium", 84, 209.0}, }
Go also supports a struct
type which is a composite that stores named elements called fields as shown in the following code:
func main() { planet := struct { name string diameter int }{"earth", 12742} }
The previous example uses the literal syntax to declare struct{name string; diameter int}
with the value {"earth", 12742}
. You can read all about composite types in Chapter 7, Composite Types.
The named type
As discussed, Go provides a healthy set of built-in types, both simple and composite. Go programmers can also define new named types based on an existing underlying type as shown in the following snippet extracted from metalloid
in the earlier example:
type amu float64 type metalloid struct { name string number int32 weight amu }
The previous snippet shows the definition of two named types, one called amu
, which uses type float64
as its underlying type. Type metalloid
, on the other hand, uses a struct
composite type as its underlying type, allowing it to store values in an indexed data structure. You can read more about declaring new named types in Chapter 4, Data Types.
Methods and objects
Go is not an object-oriented language in a classical sense. Go types do not use a class hierarchy to model the world as is the case with other object-oriented languages. However, Go can support the object-based development idiom, allowing data to receive behaviors. This is done by attaching functions, known as methods, to named types.
The following snippet, extracted from the metalloid example, shows the type amu
receiving a method called float()
that returns the mass as a float64
value:
type amu float64 func (mass amu) float() float64 { return float64(mass) }
The power of this concept is explored in detail in Chapter 8, Methods, Interfaces, and Objects.
Interfaces
Go supports the notion of a programmatic interface. However, as you will see in Chapter 8, Methods, Interfaces, and Objects, the Go interface is itself a type that aggregates a set of methods that can project capabilities onto values of other types. Staying true to its simplistic nature, implementing a Go interface does not require a keyword to explicitly declare an interface. Instead, the type system implicitly resolves implemented interfaces using the methods attached to a type.
For instance, Go includes the built-in interface called Stringer
, defined as follows:
type Stringer interface { String() string }
Any type that has the method String()
attached, automatically implements the Stringer
interface. So, modifying the definition of the type metalloid
, from the previous program, to attach the method String()
will automatically implement the Stringer
interface:
type metalloid struct { name string number int32 weight amu } func (m metalloid) String() string { return fmt.Sprintf( "%-10s %-10d %-10.3f %e", m.name, m.number, m.weight.float(), atoms(moles(m.weight)), ) }
golang.fyi/ch01/metalloids2.go
The String()
methods return a pre-formatted string that represents the value of a metalloid
. The function Print()
, from the standard library package fmt
, will automatically call the method String()
, if its parameter implements stringer
. So, we can use this fact to print metalloid
values as follow:
func main() { fmt.Print(headers()) for _, m := range metalloids { fmt.Print(m, "\n") } }
Again, refer to Chapter 8, Methods, Interfaces, and Objects, for a thorough treatment of the topic of interfaces.
Concurrency and channels
One of the main features that has rocketed Go to its current level of adoption is its inherent support for simple concurrency idioms. The language uses a unit of concurrency known as a goroutine
, which lets programmers structure programs with independent and highly concurrent code.
As you will see in the following example, Go also relies on a construct known as a channel used for both communication and coordination among independently running goroutines
. This approach avoids the perilous and (sometimes brittle) traditional approach of thread communicating by sharing memory. Instead, Go facilitates the approach of sharing by communicating using channels. This is illustrated in the following example that uses both goroutines
and channels as processing and communication primitives:
// Calculates sum of all multiple of 3 and 5 less than MAX value. // See https://projecteuler.net/problem=1 package main import ( "fmt" ) const MAX = 1000 func main() { work := make(chan int, MAX) result := make(chan int) // 1. Create channel of multiples of 3 and 5 // concurrently using goroutine go func(){ for i := 1; i < MAX; i++ { if (i % 3) == 0 || (i % 5) == 0 { work <- i // push for work } } close(work) }() // 2. Concurrently sum up work and put result // in channel result go func(){ r := 0 for i := range work { r = r + i } result <- r }() // 3. Wait for result, then print fmt.Println("Total:", <- result) }
golang.fyi/ch01/euler1.go
The code in the previous example splits the work to be done between two concurrently running goroutines
(declared with the go
keyword) as annotated in the code comment. Each goroutine
runs independently and uses the Go channels, work
and result
, to communicate and coordinate the calculation of the final result. Again, if this code does not make sense at all, rest assured, concurrency has the whole of Chapter 9, Concurrency, dedicated to it.
Memory management and safety
Similar to other compiled and statically-typed languages such as C and C++, Go lets developers have direct influence on memory allocation and layout. When a developer creates a slice
(think array
) of bytes, for instance, there is a direct representation of those bytes in the underlying physical memory of the machine. Furthermore, Go borrows the notion of pointers to represent the memory addresses of stored values giving Go programs the support of passing function parameters by both value and reference.
Go asserts a highly opinionated safety barrier around memory management with little to no configurable parameters. Go automatically handles the drudgery of bookkeeping for memory allocation and release using a runtime garbage collector. Pointer arithmetic is not permitted at runtime; therefore, developers cannot traverse memory blocks by adding to or subtracting from a base memory address.
Fast compilation
Another one of Go's attractions is its millisecond build-time for moderately-sized projects. This is made possible with features such as a simple syntax, conflict-free grammar, and a strict identifier resolution that forbids unused declared resources such as imported packages or variables. Furthermore, the build system resolves packages using transitivity information stored in the closest source node in the dependency tree. Again, this reduces the code-compile-run cycle to feel more like a dynamic language instead of a compiled language.
Testing and code coverage
While other languages usually rely on third-party tools for testing, Go includes both a built-in API and tools designed specifically for automated testing, benchmarking, and code coverage. Similar to other features in Go, the test tools use simple conventions to automatically inspect and instrument the test functions found in your code.
The following function is a simplistic implementation of the Euclidean division algorithm that returns a quotient and a remainder value (as variables q
and r
) for positive integers:
func DivMod(dvdn, dvsr int) (q, r int) { r = dvdn for r >= dvsr { q += 1 r = r - dvsr } return }
golang.fyi/ch01/testexample/divide.go
In a separate source file, we can write a test function to validate the algorithm by checking the remainder value returned by the tested function using the Go test API as shown in the following code:
package testexample import "testing" func TestDivide(t *testing.T) { dvnd := 40 for dvsor := 1; dvsor < dvnd; dvsor++ { q, r := DivMod(dvnd, dvsor) if (dvnd % dvsor) != r { t.Fatalf("%d/%d q=%d, r=%d, bad remainder.", dvnd, dvsor, q, r) } } }
golang.fyi/ch01/testexample/divide_test.go
To exercise the test source code, simply run Go's test tool as shown in the following example:
$> go test . ok github.com/vladimirvivien/learning-go/ch01/testexample 0.003s
The test tool reports a summary of the test result indicating the package that was tested and its pass/fail outcome. The Go Toolchain comes with many more features designed to help programmers create testable code, including:
- Automatically instrument code to gather coverage statistics during tests
- Generating HTML reports for covered code and tested paths
- A benchmark API that lets developers collect performance metrics from tests
- Benchmark reports with valuable metrics for detecting performance issues
You can read all about testing and its related tools in Chapter 12, Code Testing.
Documentation
Documentation is a first-class component in Go. Arguably, the language's popularity is in part due to its extensive documentation (see http://golang.org/pkg). Go comes with the Godoc tool, which makes it easy to extract documentation from comment text embedded directly in the source code. For example, to document the function from the previous section, we simply add comment lines directly above the DivMod
function as shown in the following example:
// DivMod performs a Eucledan division producing a quotient and remainder. // This version only works if dividend and divisor > 0. func DivMod(dvdn, dvsr int) (q, r int) { ... }
The Go documentation tool can automatically extract and create HTML-formatted pages. For instance, the following command will start the Godoc tool as a server on localhost port 6000
:
$> godoc -http=":6001"
You can then access the documentation of your code directly from your web browser. For instance, the following figure shows the generated documentation snippet for the previous function located at http://localhost:6001/pkg/github.com/vladimirvivien/learning-go/ch01/testexample/
:
An extensive library
For its short existence, Go rapidly grew a collection of high-quality APIs as part of its standard library that are comparable to other popular and more established languages. The following, by no means exhaustive, lists some of the core APIs that programmers get out-of-the-box:
- Complete support for regular expressions with search and replace
- Powerful IO primitives for reading and writing bytes
- Full support for networking from socket, TCP/UDP, IPv4, and IPv6
- APIs for writing production-ready HTTP services and clients
- Support for traditional synchronization primitives (mutex, atomic, and so on)
- General-purpose template framework with HTML support
- Support for JSON/XML serializations
- RPC with multiple wire formats
- APIs for archive and compression algorithms:
tar
,zip
/gzip
,zlib
, and so on - Cryptography support for most major algorithms and hash functions
- Access to OS-level processes, environment info, signaling, and much more
The Go Toolchain
Before we end the chapter, one last aspect of Go that should be highlighted is its collection of tools. While some of these tools were already mentioned in previous sections, others are listed here for your awareness:
fmt
: Reformats source code to adhere to the standardvet
: Reports improper usage of source code constructslint
: Another source code tool that reports flagrant style infractionsgoimports
: Analyzes and fixes package import references in source codegodoc
: Generates and organizes source code documentationgenerate
: Generates Go source code from directives stored in source codeget
: Remotely retrieves and installs packages and their dependenciesbuild
: Compiles code in a specified package and its dependenciesrun
: Provides the convenience of compiling and running your Go programtest
: Performs unit tests with support for benchmark and coverage reportsoracle
static analysis tool: Queries source code structures and elementscgo
: Generates source code for interoperability between Go and C