Go is an open source programming language from Google that makes it easy to build simple, reliable, and efficient software. It's part of the programming language lineage that started with Tony Hoare's Communicating Sequential Processes and includes Occam, Erlang, Newsqueak, and Limbo. The Go language project currently has more than 1,800 contributors and is led by Rob Pike, a distinguished engineer at Google.
Go was originally developed as an alternative to C++. Essentially, Pike got tired of long C++ compilations of a core Google program. As it turned out, though, very few Go language converts came over from C++. When asked what surprised him most after rolling out Go in 2012, Pike responded, "Although we expected C++ programmers to see Go as an alternative, instead most Go programmers come from languages like Python and Ruby.”
At the time, almost everyone coming from C++ or Java wanted Go to have classes and generics. Pike and others pushed back, but in 2022, things changed. As of Go 1.18, generics are finally part of the Go language.
This article demonstrates some of the differentiating features of Go, including extremely lightweight concurrency patterns and the new generic types.
Slices
Go extends the idea of arrays with slices, which have variable size. A slice points to an array of values and includes a length. As an example, [ ]T
is a slice with elements of type T
. In the following code, we use slices of slices of unsigned bytes to hold the pixels of an image we generate, where the pixel values range from 0 to 255. Go programs start running with package main
. The import
statement is an extended version of C and C++'s include
statement.
package main
import "code.google.com/p/go-tour/pic"
func Pic(dx, dy int) [][]uint8 {
slice := make([][]uint8, dy)
for i := range slice {
slice[i] = make([]uint8, dx)
for j := range slice[i] {
slice[i][j] = uint8(i * j)
}
}
return slice
}
func main() {
pic.Show(Pic)
}
The :=
syntax declares and initializes a variable, and the compiler infers a type whenever it can. Note, also, that make
is used to create slices and some other types. A for...range
loop is the equivalent of C#'s for...in
loop. The pattern shown in Figure 1 is determined by the expression in the inner loop above: (i*j)
. See the pic package and its source code to learn more.
Figure 1. A pattern demonstrating slices in Go.
Maps
The Go map
statement maps keys to values. As with slice
, you create a map with make
, not new
. In the following example, we map string keys to integer values. This code demonstrates inserting, updating, deleting, and testing for map elements.
package main
import "fmt"
func main() {
m := make(map[string]int)
m["Answer"] = 42
fmt.Println("The value:", m["Answer"])
m["Answer"] = 48
fmt.Println("The value:", m["Answer"])
delete(m, "Answer")
fmt.Println("The value:", m["Answer"])
v, ok := m["Answer"]
fmt.Println("The value:", v, "Present?", ok)
}
Here is the program's print output:
The value: 42
The value: 48
The value: 0
The value: 0 Present? false
Structs and methods
The Go language lacks classes but has a struct
, which is a sequence of named elements, which are called fields
. Each field
has a name
and a type
. A method
is a function with a receiver. A method
declaration binds an identifier (the method name) to a method and associates the method with the receiver's base type.
In this example, we declare a Vertex
struct
to contain two floating point fields
, X and Y, and a method, Abs
. Fields that begin with uppercase letters are public; fields that begin with lowercase letters are private. Fields and methods are addressable through the dot notation (.
) and ampersands (&
) signify pointers, as in C. This program prints 5
.
package main
import (
"fmt"
"math"
)
type Vertex struct {
X, Y float64
}
func (v *Vertex) Abs() float64 {
return math.Sqrt(v.X*v.X + v.Y*v.Y)
}
func main() {
v := &Vertex{3, 4}
fmt.Println(v.Abs())
}
Interfaces
An interface type is defined by a set of methods. A value of the interface
type can hold any value that implements those methods. In this next example, we define an interface, Abser
, and a variable (a
) of type Abser
:
package main
type Abser interface {
Abs() float64
}
func main() {
var a Abser
f: MyFloat(-math.Sqrt2
v = Vertex{3, 4}
a = f // a MyFloat implements Abser
a = &v // a *Vertex implements Abser
// In the following line, v is a Vertex (not *Vertex)
// and does NOT implement Abser.
a = v
fmt.Println(a.Abs())
}
type MyFloat float64
func (f MyFloat) Abs() float64 {
if f < 0 {
return float64(-f)
}
return float64(f)
}
type Vertex struct {
X, Y float64
}
Note that the assignments a=f
and a=&v
work, but the assignment a=v
does not even compile. The Abs
method of Vertex
, which you saw in the previous section, has a pointer to the Vertex
type for its receiver. So, a *Vertex
implements Abser
, but a Vertex
does not.
Switch
The switch
statement in Go is similar to the switch
statement in other C-like languages, except that the case
statements can be types or expressions in addition to simple values. Cases automatically break unless they end with fallthrough
statements. The cases are evaluated in the order they are defined.
package main
import (
"fmt"
"runtime"
)
func main() {
fmt.Print("Go runs on ")
switch os = runtime.GOOS; os {
case "darwin":
fmt.Println("macOS.")
case "linux":
fmt.Println("Linux.")
default:
// freebsd, openbsd,
// plan9, windows...
fmt.Printf("%s.", os)
}
}
Goroutines
Goroutines are basically extremely lightweight threads, in the spirit of Tony Hoare's Communicating Sequential Processes. In the example below, the first line of func main
calls the say
function asynchronously, while the second line calls it synchronously. The difference is in the use of the go
qualifier for the asynchronous goroutine:
package main
import (
"fmt"
"time"
)
func say(s string) {
for i := 0; i < 5; i++ {
time.Sleep(100 * time.Millisecond)
fmt.Println(s)
}
}
func main() {
go say("world")
say("hello")
}
Goroutines, channels, and select
statements are the core of Go's highly scalable concurrency, one of the language's strongest selling points. Go also has conventional synchronization objects, but they are rarely needed. This program outputs:
hello
world
hello
world
hello
world
hello
world
hello
Channels
Channels in Go provide a mechanism for concurrently executing functions to communicate by sending and receiving values of a specified element type. Here's an example:
package main
import "fmt"
func sum(s []int, c chan int) {
sum := 0
for _, v := range s {
sum += v
}
c <- sum // send sum to c
}
func main() {
s := []int{7, 2, 8, -9, 4, 0}
c := make(chan int)
go sum(s[:len(s)/2], c)
go sum(s[len(s)/2:], c)
x, y := <-c, <-c // receive from c
fmt.Println(x, y, x+y)
}
Note that the value of an uninitialized channel is nil. c = make(chan int)
creates a bidirectional channel of integers. We could also make unidirectional sending (<-c
) and receiving (c<-
) channels. Following that, we call sum
asynchronously with slices of the first and second half of a
. Then, the integer variables, x
and y
, receive the two sums from the channel. In the expression "for _, v range a
", the underscore (_
), the blank identifier, means to ignore the first result value from the for...range
loop, which is the index. The program output is 17 -5 12.
Range and close
In this example, we see how a sender can close
a channel to indicate that no more values will be sent. Receivers can test whether a channel has been closed by assigning a second parameter to the receive expression.
package main
import (
"fmt"
)
func fibonacci(n int, c chan int) {
x, y := 0, 1
for i := 0; i < n; i++ {
c <- x
x, y = y, x+y
}
close(c)
}
func main() {
c := make(chan int, 10)
go fibonacci(cap(c), c)
for i := range c {
fmt.Println(i)
}
}
The for
loop at the third line of main
(for i := range c
) receives values from the channel repeatedly until it is closed. The cap
of the channel is the capacity, which is the size of the buffer in the channel. The cap
is set as the optional second argument when you make a channel, as in the first line of main
. Note the compact form of the assignment statements in the fibonacci
function. The program output is the first 10 values of the Fibonacci series, 0 through 34.
Select
A select
statement chooses which of a set of possible send
or receive
operations will proceed. It looks similar to a switch
statement, but with all the cases referring to communication operations. A select
blocks until one of its cases can run, then it executes that case. It chooses one at random if multiple cases are ready.
package main
import "fmt"
func fibonacci(c, quit chan int) {
x, y := 0, 1
for {
select {
case c <- x:
x, y = y, x+y
case <-quit:
fmt.Println("quit")
return
}
}
}
func main() {
c := make(chan int)
quit := make(chan int)
go func() {
for i := 0; i < 10; i++ {
fmt.Println(<-c)
}
quit <- 0
}()
fibonacci(c, quit)
}
Here, the main
function calls the fibonacci
function with two unbuffered channels, one for results and one for a quit
signal. The fibonacci
function uses a select
statement to wait on both channels. The anonymous, asynchronous go
function that starts at the third line of main
waits to receive values (<-c)
, then prints them. After 10 values, it sets the quit
channel, so the fibonacci
function knows to stop.
Concurrency patterns in Go
We've looked at a few differentiating features of the Go language. Now, let's see how they work together in programming examples. We'll start with a couple of concurrency patterns in Go, both taken from Rob Pike's 2012 talk on Concurrency Patterns in Go.
Concurrency pattern #1: fan in
In this example, we're using select
to create a fan-in goroutine that combines two input channels of string, input1
and input2
, into one unbuffered output channel, c
. The select
statement allows fanIn
to listen to both input channels simultaneously and relay whichever is ready to the output channel. It doesn't matter that both cases are using the same temporary variable name to hold the string from their respective input channels.
package main
func fanIn(input1, input2 <-chan string) <-chan string {
c := make(chan string)
go func() {
for {
select {
case s := <-input1: <- s
case s := <-input2: c <- s
}
}
}()
return c
}
Concurrency pattern # 2: parallel search
This concurrency example implements a parallel search of the internet, sort of like what the Google search engine actually does. To begin with, replicas …Search
is a variadic parameter to the function; both Search
and Result
are types defined elsewhere:
package main
func First(query string, replicas ...Search) Result {
c := make(chan Result)
searchReplica := func(i int) { c <- replicas[i](query) }
for i := range replicas {
go searchReplica(i)
}
return <-c
}
The caller passes a given number of (N) search server functions to the First
function. The First
function creates a channel, c
, for results and defines a function to query the i
th server, then saves it in searchReplica
. First
then calls searchReplica
asynchronously for all N servers, always returning the answer on channel c
. It returns the first result to come back from the servers.
Packages in Go
Next, we'll look at a couple of packages.
The http package
The Go net/http
package provides HTTP client and server implementations. This example implements a simple web server that returns the contents of the directory /usr/share/doc
to a web client:
package main
import (
"log"
"net/http"
)
func main() {
// Simple static webserver:
log.Fatal(http.ListenAndServe(":8080", http.FileServer(http.Dir("/usr/share/doc"))))
}
This example doesn't work properly in the Go Playground online environment. If you run it on a Mac command line, however, it returns the following to a web browser asking for http://localhost:8080/:
bash/
ccid/
cups/
groff/
ntp/
postfix/
The template package
The html/template
package implements data-driven templates for generating HTML output that is safe against code injection. For example:
package main
import "html/template"
func main() {
t, err := template.New("foo").Parse(`{{define "T"}}Hello, {{.}}!{{end}}`)
err = t.ExecuteTemplate(out, "T", "<script>alert('you have been pwned')</script>")
}
The code above produces safe, escaped HTML output:
Hello, <script> alert(' you have been pwned')</script>!
Without the escaping added by the html/template
package in this example, we could have produced the following runnable JavaScript string:
Hello, <script> alert(' you have been pwned')</script>!
Generics in Go
Yes, at long last, Go has generics. They are deeply integrated into the language, and include the idea of type constraints. This means a function's type parameters appear between brackets, before the function's arguments. In general, a function implemented with generic types is more efficient than a function implemented with the any
type, and is less wasteful than reimplementing the function for every type that may be of interest. Seeing a group of functions in your code that differ only in their argument types should be a red flag to consider writing a generic version, instead.
The following two examples are from a new section of the Tour of Go programming page.