An Introduction to Go for C++ Programmers

An Introduction to Go for C++ Programmers

By Arun Saha

Overload, 30(172):15-20, December 2022


Learning another language is always interesting. Arun Saha walks us through Go as a C++ programmer.

Go is a statically typed, compiled programming language with memory safety, garbage collection, and CSP-style concurrency [Go][Wikipedia]. It was designed at Google in 2007, publicly announced in 2009, and version 1.0 was released in 2012. It is open-sourced under BSD-3-Clause license and developed at github [Github].

You might wonder why we are talking about the Go programming language. While most of the other top programming languages are much older, Go has achieved significant usage and popularity within just ten years of its existence [TIOBE] [Stackoverflow]. I believe that this is not accidental but a result of different language design decisions. On one hand, it has almost C- and C++-like efficiency, while on the other hand, it has Python-like brevity and a batteries-included approach.

I have been a long-time C++ and C programmer. I started learning and using Go last year. During this (ongoing) journey, I have noticed a lot of elements in Go that are similar to C++ and many elements that are different. In this article, I would like to share that learning with you. (The concurrency aspects are part of a future article.)

Variable declaration

A variable declaration in C++ has the type specified to the left of the identifier. For example,

  int result = 42;

In a variable declaration in Go, the order is reversed – the type is specified to the right of the identifier. The equivalent in Go is the following.

  var result int = 42

This is perhaps the biggest habit change necessary for reading and writing Go. The designers have chosen this deliberately [Pike10]. It took me a while to get used to this.

Semicolons

Unlike C++, semicolons are optional to terminate statements in Go. The lexer insert semicolons automatically, so the source code is mostly free of them. If only multiple statements are written on a line, then semicolons are necessary to separate them.

Declaration versus assignment

Go chose := (colon equals) as a shorthand notation to define and initialize a variable within the scope of a function or a loop.

  attempt := 1 // Shorthand declaration and
               // assignment

A variable declaration needs the var keyword outside of a function. It can be used inside a function as well. The following notation first defines a variable and later assigns to it.

  var attempt int // Declaration
  …
  attempt = 1 // Assignment

Obviously, the above two approaches can be combined to have an explicit type declaration and assignment, as shown in the following.

  var attempt int = 1 // Long declaration and
                      // assignment

While using the new shorthand notation, a common beginner confusion is the following.

  sum := 0
  …
    sum := newsum // Error: Multiple declaration
                  // of 'sum'
    sum = newsum  // OK. Assignment

Zero initialization

In Go, any declared but not explicitly initialized variable would be automatically zero-initialized. There are well-defined zero values for each type, for example, 0 for numeric types, false for boolean, "" (empty string) for strings. Thus, the following statement not only declares but also initializes the variable.

  var result int

I love this feature!

In C++ (and some other languages), a lot of bugs boil down to uninitialized variables as they do not have any automatic or implicit initialization. This required introduction of compiler flags like -Wunintialized, -Wmaybe-uninitialized [GCC] to detect uninitialized variables that the programmers must remember to enable and enforce. Go eliminates all those hassles and errors through this simple language specification.

Type declaration

A type declaration defines a new named type that has the same underlying type as an existing type. The following example declares Miles as a new type with float64 as the underlying type.

  type Miles float64

Two named types with the same underlying type cannot be assigned or compared as shown in the example below.

  type Kilometers float64
  var m Miles = 26.2
  var k Kilometers = 42
  k = m // compilation error
  equal := k == m // compilation error

Functions

A function is defined with the func keyword as shown below.

  func add(a int, b int) int {
    return a + b
  }

Go allows multiple return values from a function. The following function returns both the sum and the difference of two values.

  func sumdiff(a int, b int) (int, int) {
    sum := a + b
    diff := a - b
    return sum, diff
  }

It can be called and used in the following way.

  func multipleReturn() {
    sum, diff := sumdiff(2, 3)
  }

The return values could be named. It helps disambiguate between multiple return values of the same type.

  func sumdiff2(a int, b int) (sum int, diff int) {
    sum = a + b
    diff = a - b
    return
  }

The returned variables (sum, diff) are defined in the return statement and assigned in the body of the function. The final return statement is required.

Go does not support function overloading.

Constructor and destructor

In C++, the name of the constructor is the same as the class name. A class may have one or more constructors.

Go does not have constructors. Instead, the following convention is followed. A package provides public functions with names starting with New to (1) allocate an object, (2) initialize it per the package’s needs, and (3) return the allocated object. The following is an example of creating a new list from the “container/list” package in the Go standard library [Go].

  // Create a new list and put some numbers in it.
  l := list.New()
  e4 := l.PushBack(4)

In absence of such New functions, instantiating a struct performs zero initialization to all its members.

Go does not have destructors.

Error handling

Go does not have exceptions. However, there is a strong and widely used convention for generating and propagating errors. Any function where something can go wrong usually returns an error along with its usual return value(s). The returned error is part of the function signature, it is usually the last of the returned values. If a function can return an error, then the caller is expected to check that; it can handle it or pass it up to its caller.

The following example is from the Go standard library [Go]; Open() opens the named file for reading. On successful opening, it returns a File object and nil error. If it fails to open, it returns a nil File object and an error object to capture the cause.

  func Open(name string) (*File, error)

It can be used as follows.

  f, err := os.Open("notes.txt")
  if err != nil {
    log.Fatal(err)
  }

In Go, nil is the zero value for pointers, interfaces, maps, slices, functions, etc. It is equivalent to nullptr in C++.

Go represents a potential error state with the built-in interface type, error. A nil error represents no error.

Go has a built-in function panic() that stops the ordinary flow of control and begins panicking. It can be initiated by invoking panic() directly. They can also be caused by runtime errors, such as division by zero.

Defer

Go provides a defer mechanism to specify a function that will be called at the exit of the current scope. It is similar to ScopeGuard or std::experimental::scope_exit in C++. Defer is used as a regular pattern for unlocking mutexes, closing files, etc. The example below uses defer for closing a file when the function returns.

  // Contents returns the file’s contents as a 
  // string.
  func Contents(filename string) (string, error) {
    f, err := os.Open(filename)
    if err != nil {
      return "", err
    }
    defer f.Close() // f.Close will run when we’re
                    // finished.
  <truncated>

defer is not a substitute for a destructor since there is no way to use it when a heap-allocated object is deconstructed.

The built-in function recover() regains control of a panicking situation. It is only useful inside deferred functions. If the current flow of control is panicking, a call to recover will capture the value given to panic and resume normal execution.

Visibility

Unlike C++, Go does not have class member visibility qualifiers like public, protected, or private. In Go, any variable, constant, function or struct data member starting with an upper-case character is public; others (starting with a lower-case character) are private. For example,

  type Person struct {
    Name string // public data member
    Phone string // public data member
    creditCardNumber string // private data member
  }

Methods

Go allows defining methods on types. A method is a function with a special receiver argument. The receiver appears in its own argument list between the func keyword and the method name.

In this example, the Distance method has a receiver of type Point named point.

  type Point struct {
    X, Y float64
  }
  func (point Point) Distance() float64 {
    return math.Sqrt(point.X*point.X 
                     + point.Y*point.Y)
  }

Like C++, Go has pointers. A pointer holds the memory address of a variable. (Go does not allow pointer arithmetic though.)

  point := Point{X:3, Y:4}
  ptr := &point

If the method needs to change any of the data members, then the method needs a pointer receiver as the following.

  func (point *Point) Move(dx, dy float64) {
    point.X += dx
    point.Y += dy
  }

Like C++, methods can be invoked either on the variable type or the pointer type.

  dist1 := ptr.Distance()
  dist2 := point.Distance()

A significant difference from C++ is that the object of the member function can be named anything, as opposed to the reserved keyword this.

Const

A Go program can define compile-time constants as const.

  const separator = ","

But a variable cannot be qualified as const at its declaration and initialization. I.e., there is no equivalent of the following C++ expression.

  int const result = ComputeResult(…);

There is no mechanism for const pointers or pointers to const data. Methods with non-pointer receivers behave as const member functions.

The following member function uses a non-pointer receiver (i.e., Person instead of *Person) and is equivalent to a const-member function in C++.

  func (p Person) GetName() string {
    return p.Name
  }

On the contrary, the following member function uses a pointer-receiver (*Person) and is equivalent to a non-const-member function in C++.

  func (p *Person) SetPhoneNumber(ph string) {
    p.Phone = ph
  }

Loop

There is only one kind of loop available in Go, the for loop.

The following is a traditional init-condition-post style for loop. There are no parentheses to enclose the init-condition-post portion. Note that, the only kind of increment that Go offers is post-increment (i.e., i++).

  func Sum(n int) int {
    sum := 0
    for i := 1; i <= n; i++ {
      sum += i
    }
    return sum
  }

The following is a range-based for loop iterating over a sequence of ints.

  func SumIntSequence(nums []int) int {
    var sum int
    for _, elem := range nums {
      sum += elem
    }
    return sum
  }

The range returns two values for each iteration, the index and the element. The _ is a placeholder for a return value that is not used subsequently in the code. In the above code, _ is used to ignore the returned index value.

Common data structures

The two most widely used data structures in Go are slices and maps. Both are built into the language.

Array

Like almost all other languages, an array is a sequence of contiguous mutable elements of fixed length. The following is an array of four strings.

  suits := [4]string{"clubs", "diamonds", "hearts",
    "spades"}

Slices, described below, are based on arrays. Most of the time, instead of using arrays directly, Go programs use slices.

Slice

Slice is a non-owning view of a subsequence of contiguously stored mutable elements in an underlying array. It is written as []T where the elements are of type T. A slice has three components: a pointer, a length, and a capacity.

A slice can be defined using a new underlying array or specifying a half-open range of the subsequence in an existing array or another slice.

  myCards := []string{"CA", "D9"} // slice based on
                          // a new underlying array
  redSuits := suits[1:3]  // slice based on
                          // an existing array
  trump := redSuits[:1]   // slice based on
                          // another existing slice

Multiple slices may refer to the same underlying storage, and those slices’ views may overlap.

  majorSuits := suits[2:] // overlaps with redSuits

For slices with overlapping contents, mutating an element through one slice is visible to the other slices.

  redSuits[1] = "xxx"
  fmt.Printf("%q\n", majorSuits) 
    // prints: ["xxx" "spades"]

Unlike arrays, slices are growable using the built-in function append(). If the underlying array has reached its capacity, then append() allocates a new underlying array, copies the previous contents, and appends the new ones.

  myCards = append(myCards, "CQ") 
    // myCards: ["CA" "D9" "CQ"]

If other slices were sharing the original array, those slices and the original array stay untouched.

Erasing elements from a slice is achieved by concatenating the slice before and the slice after. For erasing the ith element, we concatenate the (i-1) elements on the left, i.e., [:i], to all the elements on the right, i.e., [i+1:]. The following example erases the element at index 1.

  myCards = append(myCards[:1], myCards[2:]...) 
    // myCards: ["CA" "CQ"]

From a C++ viewpoint, the slice has some similarities to std::vector from the storage management aspect and it has some other similarities to std::span, and std::string_view from the view sharing aspect.

Map

The map is a reference to a hash table, an unordered collection of key-value pairs, in which all the keys are distinct, and the value associated with a key can be retrieved, updated, or removed in constant time. It is written as map[K]V, where K and V are the types of its keys and values. The following map associates strings to ints.

  var rgbMap map[string]int

A map needs to be initialized with the built-in make function.

  rgbMap = make(map[string]int)

The following shows insertion and retrieval.

  rgbMap["red"] = 1          // insert or update
  redCode := rgbMap["red"]   // retrieve

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

Generics

Ten years after the initial release, Go started supporting Generics in 2022. It is equivalent to templates in C++.

Go allows expressing type constraints. The following example composes (union) the standard library provided Integer and Float constraints to define the Numeric constraint.

  type Numeric interface {
    constraints.Integer | constraints.Float
  }

The generic function SumSequence() accepts a slice of type T where T satisfies the Numeric constraint. The generic type T and its constraint Numeric are enclosed in a pair of square brackets after the function name. The return type is also the generic type T.

  func SumSequence[T Numeric](nums []T) T {
    var sum T
    for _, elem := range nums {
      sum += elem
    }
    return sum
  }

The statement var sum T performs default zero initialization for the actual type. Like C++, you can build generic data structures. The following example shows building a generic set data structure.

  type Set[K comparable] struct {
    elems map[K]bool
  }
  func NewSet[K comparable]() *Set[K] {
    var set Set[K]
    set.elems = make(map[K]bool)
    return &set
  }
  func (set *Set[K]) Add(elem K) {
    set.elems[elem] = true
  }

A sample user code is the following.

  seti := NewSet[int]()
  seti.Add(42)

Stack versus heap allocation and garbage collection

In C++, the local or automatic variables in a function are allocated in the stack. They are deallocated when the function returns. Thus, returning the address of such a variable is a recipe for disaster.

In Go, however, the placement of a variable in stack versus heap is up to the compiler. If the lifetime of a variable exists beyond the scope of a function – based on escape analysis – then the compiler places it on the heap. Based on this principle, in the NewSet() function above it is okay to return the address of its local variable.

Go has automatic memory management or garbage collection. If there is no path to reach a heap variable from any other package level variable or any currently active functions, then the variable is unreachable and can be deallocated.

Interface

An interface in Go is an abstract type; it is a collection of one or more behaviors (methods) that are offered as part of this interface. This way it is like Pure Abstract Virtual Classes (PABC) in C++. One or more structs can satisfy an interface by implementing all the methods of the interface. Such structs are known as instances of that interface.

The methods are named as verbs (e.g., Read, Write, Close) and the interfaces are named as nouns that perform those verbs (e.g., Reader, Writer, Closer).

The Reader interface offers a Read method to read from some source, outside the scope of this function, into the byte buffer buf, returning the number of bytes read n (where 0 <= n <= len(buf)) and any error encountered err.

  type Reader interface {
    Read(buf []byte) (n int, err error)
  }

The Writer interface offers a Write method to write len(buf) bytes from the buffer buf to the underlying data stream, returning the number of bytes written n (where 0 <= n <= len(buf)) and any error encountered err that caused the write to stop early.

  type Writer interface {
    Write(buf []byte) (n int, err error)
  }

A user-defined type can implement such standard interfaces and avail the standard library methods. The following example shows how a user-defined type Gadget implements the Writer interface.

  type Gadget struct {
    serial []byte
  }
  func (gadget *Gadget) Write(data []byte) 
       (n int, err error) {
    gadget.serial = make([]byte, len(data))
    copy(gadget.serial, data)
    return len(data), nil
  }

A client of Gadget can use it like the following.

  serial := []byte("123456789")
  gadget := Gadget{}
  fmt.Fprintf(&gadget, "%s", serial)

Interfaces can be composed to make bigger interfaces.

The interface ReadWriter is an interface that combines the Reader and Writer interfaces.

  type ReadWriter interface {
    Reader
    Writer
  }

An expression may be assigned to an interface if and only if its type satisfies the interface.

  // Declaration: w is a variable of interface 
  // type io.Writer
  var w io.Writer
  w = os.Stdout
  // OK: os.Stdout is of type *os.File which 
  // has Write method
  w = time.Second
  // compile error: time.Second is of type 
  // time.Duration lacking Write method

The empty interface, interface{}, also known as any, is satisfied by any value.

A struct can satisfy more than one interface. When a struct implements an interface, then it may or may not explicitly specify the interface. When it is not explicitly specified, the compiler uses structural typing to determine if a struct is implicitly satisfying an interface and allows substitution.

An interface value can be converted to its concrete value or a different kind of interface value by an operation known as type assertion. The following example shows how an interface value w may be converted to a variable f of its concrete type.

  var w io.Writer
  w = os.Stdout
  f := w.(*os.File) // success: f == os.Stdout
  c := w.(*bytes.Buffer) // runtime panic:
    // interface holds *os.File, not *bytes.Buffer

The following example shows how the interface value w of interface type io.Writer (from above) is converted to interface value rw of interface type io.ReadWriter.

  rw := w.(io.ReadWriter)
  // success: *os.File has both Read and Write

Inheritance

Go does not offer inheritance. A struct cannot inherit another struct. However, inheritance-like behavior can be achieved by designing interface(s), and struct(s) satisfying those interface(s) [ Saha21].

Packages and modules

Go source files are bundled into packages, and packages are bundled into modules.

A package groups files of similar functionalities together. The source code for a package resides in one or more .go files, usually in a directory whose name is the same as the package name. All such files list the name of the package at the beginning of the file, e.g., package fmt. Files outside the package can refer to or use a package by importing it, e.g., import "fmt". Each package serves as a separate namespace. From a C++ point of view, it is similar to a library from the file organization and build aspect, and namespace from the naming scope aspect.

A module is a collection of Go packages stored in a file tree with a go.mod file at its root. The go.mod file defines the module’s dependency requirements.

Eco system

Go is not just a language; it comes with a rich toolchain [Edwards19] ecosystem around it. Following are some frequently used tools in the ecosystem:

  1. go build to build,
  2. go run to build and run an executable,
  3. go test to build and run the tests and benchmarks, and
  4. go doc to build the documentation from comments and examples.

Beyond the basics, there are

  1. go get to download a package from the Internet
  2. go fmt to format the source code uniformly, and so on.

Go offers a flag -race that can be passed to go build or go test to instrument the code for race detection.

Conclusion and further reading

This article is a quick introduction to Go from a C++ perspective. It is by no means a tutorial on Go. For that, please refer to the resources below.

Effective Go [Go-1] and The Go Programming Language [Donovan15] are excellent sources for starting to learn Go. The Go Playground [Go-2] is an excellent tool to write and execute Go programs from the comfort of a browser.

Acknowledgments

Many thanks to Prakash Jalan, Frances Buontempo, and the Overload reviewers for their feedback on the earlier versions of this article.

Note: The opinions expressed in this article are solely the author’s.

References

[Donovan15] Alan A. A. Donovan and Brian W. Kernighan (2015) The Go Programming Language, Addison-Wesley Professional Computing Series, ISBN: 978-0134190440

[Edwards19] Alex Edwards ‘An Overview of Go’s Tooling’, published 15 April 2019 at https://www.alexedwards.net/blog/an-overview-of-go-tooling

[GCC] ‘Options to request or suppress warnings’ at https://gcc.gnu.org/onlinedocs/gcc/Warning-Options.html

[Github] Go: The Go Programming Language – https://github.com/golang

[Go] The Go website: https://go.dev/

[Go-1] Effective Go, https://go.dev/doc/effective_go

[Go-2] The Go Playground: https://go.dev/play/

[Pike10] Rob Pike ‘Go’s declaration syntax’ published 7 Jul 2020 at https://go.dev/blog/declaration-syntax

[Saha21] Arun Saha ‘Inheritance in golang’ published 27 Oct 2021 at https://medium.com/@arunksaha/inheritance-in-golang-44680461cbcf

[Stackoverflow] ‘Most poular technologies’ at https://survey.stackoverflow.co/2022/#technology-most-popular-technologies

[TIOBE] ‘TIOBE Index for November 2022’: https://www.tiobe.com/tiobe-index/

[Wikipedia] ‘Go (programming language)’: https://en.wikipedia.org/wiki/Go_(programming_language)

Arun Saha Arun is a software engineer and works in different areas of software-defined data centers including networking and storage systems. Arun is passionate about building robust software infrastructure, engineering high quality software, and improving productivity. Arun holds a B.S. and Ph.D. in Computer Science.