A code editor with four icons symbolizing DevOps, developers, a gear, and a cluster.

After years of working on software written in C and C++, I switched to working on a project that is implemented in Go. More developers may find themselves working in the Go ecosystem as more software, such as Red Hat OpenShift and Kubernetes, is implemented in Go. This article discusses the primary language differences between Go and C++, differences in the development environments, and differences in the program-building environment. Examples and code snippets are from the Grafana sources.

Program syntax

Go statements are terminated by a semicolon. Go treats the end of a non-blank line as a semicolon unless it can be determined that the line is incomplete. As a result, go requires the opening brace to be on the same line as the function definition:

 func (s *Server) init() error {
  ...
 }
}

Go requires the closing then brace to be on the same line as the else:

if hs.Cfg.UnifiedAlerting.IsEnabled() {
	notifiersAuthHandler = reqSignedIn
} else {
	notifiersAuthHandler = reqEditorRole
}

Variable and type declaration

Go declarations start with the keyword var followed by the name and then the type, which is the opposite of C. A series of declarations may be placed in parentheses.

 var dashboardId int64
 var (
         hasPublicDashboard bool
         err                error
 )
// Go uses the clearer form:
 var r1, r2 *regexp.Regexp
// whereas the C equivalent would be:
 regexp.Regexp *r1, *r2;

A variable may be initialized when it is declared. If the type is not specified, the variable type will be the type of the initialization expression. var msg = "unknown" which will define msg as a string.

A short declaration syntax is allowed within a function or loop. One or more variables may be defined this way. If multiple variables are defined using this method, then one of the variables may have already been declared. This is often used to define and use an error variable.

req, err := http.NewRequest("GET", url, nil)
...
resp, err := client.Do(req)

A type declaration defines a new named type. A common use is to create a type for a defined structure.

type Leaf struct {
     ...
}
type patternType int8 
leaves   []*Leaf
typ        patternType

Variable assignment

Go has a blank identifier, which causes the return value of the right side of the assignment to be ignored.

_ = l.next()

Go permits multiple assignments, which are done in parallel.

i, j = j, i    // Swap i and j.

Any declared but not explicitly initialized variable is automatically initialized to the zero value of the type: false for booleans, 0 for numeric types,"" for strings, and nil for other types. A variable must either be initialized or the type must be specified.

Go, unlike C++, requires that two variables can only be compared or assigned if their type definitions match. Go does not support implicit type conversion.

Go constants can be typed or untyped. If the type is present, then the expressions must be assignable to that type. If the type is omitted, the constant takes the type of the expression. An untyped numeric constant represent values using arbitrary precision.

const eof = -1		// untyped integer
const orgID int64 = 2  // int64 

Data containers

Go does not support enums. Instead, you can use the special name iota in a constant declaration that represents successive untyped integer constants.

type State int
const (
	Normal State = iota
	Alerting
	Pending
	NoData
	Error
)

Arrays in Go are first-class values. When an array is used as a function parameter, the function receives a copy of the array, not a pointer to it. However, in practice, functions often use slices for parameters; slices hold pointers to underlying arrays.

A slice can be understood to be a struct with three fields: an array pointer, a length, and a maximum size. Slices use the [] operator to access elements of the underlying array. The len function returns the length of the slice, and the cap function returns the maximum size.

name = name[lastslash+1:]
encrypted := []byte{0x2a, 0x59, 0x57, 0x56, 0x78}

Given an array or another slice, a new slice is created via:

newarr := arr[i:j]

where newarr starts at index i and ends prior to index j. newarr refers to arr, thus changes made to newarr are reflected in arr. An array pointer can be assigned to a variable of slice type:

var s []int ; var a[10] int ; s = &a

A slice is similar to std::vector in C++.

Hash tables are provided by the language. They are called maps. A map is an unordered collection of key-value pairs where the keys are unique. It is written as map[key_type]value_type,where key_type is the type of the map key and value_type is the type of the map value.

encrypted := make(map[string][]byte)
encrypted[key] = encryptedData

A Go map is similar to std::unordered_map in C++.

Strings are provided by the language. They are immutable and cannot be changed once they have been created.

Expressions

The for statement is the only looping construct in Go. It may be used with a single condition, which is equivalent to a while statement, or the condition can be omitted, which is an endless loop. Parenthesis are not required:

for i := 1; i <= int(orgID); i++ {
	...         
}

A for statement can also iterate through the entries of an array, slice, string, or map:

for _, team := range query.Result {
    ...
}

The blank identifier _ is an anonymous placeholder which indicates that the first value returned by range, the index, is ignored. Go permits break and continue to specify a label. The label must refer to a for, switch, or select statement.

LOOP:
       for {
	       ...
	       break LOOP
       }

In a switch statement, case labels do not fall through. You can make them fall through using the fallthrough keyword. A case may have multiple values. The case value need not be an integer; it can be any type that supports equality comparisons.

switch hs.Cfg.Protocol {
case setting.HTTPScheme, setting.SocketScheme:
...
default:
}

The increment and decrement operators may only be used in statements, not in expressions.

patchedIndex++

Go has pointers but not pointer arithmetic. You cannot use a pointer variable to walk through the bytes of a string. Go uses nil for invalid pointers, where C++ uses NULL or simply 0.

b := &bytes.Buffer{}
copy := *msg

Functions

A function in Go is defined with the func keyword. Input and output parameters are defined separately. There can be multiple return types. In the following example, name is an input string parameter. The function returns two strings.

func splitName(name string) (string, string) {
	names := util.SplitString(name)
	switch len(names) {
	case 0:
		return"  ",""
	case 1:
		return names[0], ""
	default:
		return names[0], names[1]
	}
}

The return values can be returned by name. For example, in the above:

func splitName(name string) (s1 string, s2 string) {
       ...
       s1 = names[0]
       s2 = names[1]
       ...
}

Parameters are passed by value except for maps and slices, which are passed by reference. The defer statement can be used to call a function after the function containing the defer statement returns.

func performGet(url string, av *Avatar, handler ResponseHandler) error {
     ...
     defer func() {
	      if err := resp.Body.Close(); err != nil {
	       alog.Warn("Failed to close response body", "err", err)
     }()
     ...
}

This is typically used to handle cleanup, such as closing files before the containing function returns. A function definition without a corresponding function name is an anonymous function, as shown in the previous example. The anonymous function can reference variables in the containing function's scope.

Each variable in Go exists as long as there are references to the variable. Go uses garbage collection to free the variable's memory when there are no longer references to it. The memory cannot be released explicitly. The garbage collection is intended to be incremental and efficient on modern processors.

Interfaces

Go uses interfaces in situations where C++ uses classes, subclasses, and templates. A Go interface is similar to a C++ pure abstract class: a class with pure virtual methods and no data members. Go allows any type that provides the methods named in the interface to be treated as an implementation of the interface.

type Expander interface {
        SetupExpander(file *ini.File) error
        Expand(string) (string, error)
}

A method definition is similar to a function definition with the addition of a receiver, which is similar to the this pointer in a C++ class method.

type fileExpander struct {
}

func (e fileExpander) SetupExpander(file *ini.File) error {
       return nil
}
func (e fileExpander) Expand(s string) (string, error) {
...
}

fileExpander implements the Expander interface by defining a SetupExpander and Expand method. Any function that takes Expander as a parameter will accept a variable of type fileExpander. If we think of fileExpander as a C++ pure abstract base class, then defining SetupExpander and Expand for fileExpander made it inherit from Expander. A type may satisfy multiple interfaces.

An anonymous field can be used to implement something resembling a C++ child class:

        type myExpanderType struct { Expander; count int }

This implements myExpanderType as a child of Expander that inherits its methods.

A variable that has an interface type may be converted to have a different interface type using a special construct called a type assertion. This is implemented dynamically at run time, like C++ dynamic cast. Unlike dynamic cast, there does not need to be any declared relationship between the two interfaces. This is typically used in a type switch, which switches based on the type of value:

        switch v := value.(type) {
        case time.Time:
        	return v.Format(timeFormat)
        case error:
        	return v.Error()
        case fmt.Stringer:
        	return v.String()
        default:
        	return v
        }

Goroutines

Go permits starting a new thread of execution, known as a goroutine, using the go statement. The function runs in a different, newly created goroutine, which shares the address space with the parent. The Go runtime schedules an arbitrary number of goroutines onto a random number of OS threads. Goroutines are more lightweight than OS threads, use a smaller stack, and are scheduled by a userspace runtime scheduler.

          func updateUsageStats(ctx context.Context, reader *bluge.Reader, logger log.Logger) {
               ...
               }
          go updateUsageStats(context.Background(), reader, i.logger)

Go statements frequently use function literals:

               go func() {
                        defer close(done)
                        for {
                        ...
               }()

Channels

Channels are used to communicate between goroutines. Any value may be sent over a channel. Channels are efficient and cheap. To send a value on a channel, use <- as a binary operator. To receive a value on a channel, use <- as a unary operator. When calling functions, channels are passed by reference. A channel can control access to a single value.

        attemptChan := make(chan int, 1)
        attemptChan <- 1
        for {
           ...
           go e.processJob(attemptID, attemptChan, cancelChan, job)
           select {
        	   case <-unfinishedWorkTimer.C:
        	 return e.endJob(grafanaCtx.Err(), cancelChan, job)
              case <-attemptChan:
        	 return e.endJob(nil, cancelChan, job)
              }

Development environment

Go provides dependency management, interface abstraction, and documentation tools to assist with programming in the large.

The go command provides various tools for managing the Go development environment:

  • go help gives an overview of the go command. 
  • go env displays the environment variables that define the Go environment.
  • The Go compiler and tools are installed into the value displayed by GOROOT.
  • gofmt is a tool that enforces layout rules. Most Go code has been run through gofmt. It enforces a single standard Go style.

Go does not use header files. Instead, each source file is a member of a set of related files that form a module. Go has tools to manage versions of modules. Executables created by go are typically statically linked; thus the referenced modules are statically linked.

When a package defines an object (type, constant, variable, function) with a name starting with an upper case letter, that object is visible to any other file that imports that package. Exported names and functions form a module application binary interface, which should not be changed or removed within a major release so that compatibility is maintained.

The environment variable GOPATH, which is typically $HOME/go and can be displayed by go env, contains a directory: pkg where compiled package files are stored based on import statements. Go downloads modules into the directory $GOMODCACHE; the default path is pkg/mod.

go mod init creates a new go.mod file, which defines the module contents in the current directory. If after creating go.mod we create a simple Go program:

package main

import (
"fmt"
)

func main() {
var s string = "abc"
ss := "def"
var ssa = []string {"efg","hij"}
fmt.Println(s, ss, ssa)
}

go mod init example.com/test creates a go.mod that describes a module's characteristics. 

go mod edit modifies the attributes in go.mod

go mod tidy will download any modules that our module is importing.

go run . will build and execute a Go program. 

go build . will build a Go program

./test abc def [efg hij]

Go provides a test infrastructure. To add a module test, create a program MOD_test.go, where MOD is the module name. The program imports "testing." Each test routine is called Test where Testfoo is an individual test name. t.Fatalf is called if the test fails.

package MOD
import {
    "testing"
}
 func Testfoo(t *testing.T) {
     ...
     if ... {
        t.Fatal(`Expected %d but got %d, error`, expected, value)
     }
}     

Conclusion

Go is an easy-to-learn language with a large ecosystem of tools. This article gives a C++ programmer the fundamentals to start using Go effectively.