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 thego
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 throughgofmt
. 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.