The Go tour

I recently spent a little time with the Go tour to perhaps learn enough Go to become dangerous. For a few of the examples I made C++ versions.

Hello world

This is a Go program that shows string, float and boolean output:

package main

import "fmt"

const Pi = 3.14

func main() {
    const World = "世界"
    fmt.Println("Hello", World)
    fmt.Println("Happy", Pi, "Day")

    const Truth = true
    fmt.Println("Go rules?", Truth)
}

Okay, maybe this one isn't substantial or interesting, but we can't well do without a take on the classic hello world. This C++ version uses stream I/O, but C++ also includes printf(3) from the C standard library and has easy access to the underlying system call write(2).

#include <iostream>

constexpr auto pi = 3.14;

int main()
{
  constexpr auto world = u8"世界";
  std::cout << "Hello " << world << '\n';
  std::cout << "Happy " << pi << " Day" << '\n';

  constexpr auto truth = true;
  std::cout << "C++ rules? " << truth << '\n';
}

I have not attempted to get the C++ examples to generate identical output; merely “equivalent” output.

Numeric Constants

This example shows a unique feature of Go, high-precision compile time constants:

package main

import "fmt"

const (
    Big   = 1 << 100
    Small = Big >> 99
)

func needInt(x int) int { return x*10 + 1 }
func needFloat(x float64) float64 {
    return x * 0.1
}

func main() {
    fmt.Println(needInt(Small))
//  This next line causes a compile time overflow:
//  fmt.Println(needInt(Big))
    fmt.Println(needFloat(Small))
    fmt.Println(needFloat(Big))
}

C++ inherits it's compile time constants from C, restricting them to fundamental types.

GCC and Clang (on 64 bit targets) provide a fundamental type large enough for this particular example. This is not standard and has limitations: for example, you can't express a literal of this type.

#include <iostream>

constexpr __int128_t big = __int128_t(1) << 100;
constexpr __int128_t small = big >> 99;

int needInt(int x) { return x*10 + 1; }
double needFloat(double x) {
  return x * 0.1;
}

int main()
{
  std::cout << needInt(small) << '\n';
//This next line causes a compile time overflow:
//std::cout << needInt(big) << '\n';
  std::cout << needFloat(small) << '\n';
  std::cout << needFloat(big) << '\n';
}

More interesting is how you might use high-precision numbers at run time in your programs. In C++ the types provided by libraries can be used very much like fundamental types.

#include <iostream>
#include <boost/multiprecision/gmp.hpp>

using namespace boost::multiprecision;

mpz_int const big = mpz_int(1) << 100;
mpz_int const small = big >> 99;

mpz_int needInt(mpz_int x) { return x*10 + 1; }
mpf_float needFloat(mpf_float x) {
  return x * 0.1;
}

int main()
{
  std::cout << needInt(small) << '\n';
  // Hey, this next line works now!
  std::cout << needInt(big) << '\n';
  std::cout << needFloat(small) << '\n';
  std::cout << needFloat(big) << '\n';
}

Notice that “big” and “small” are now const and not constexpr: they are constructed at run time.

With Go I tried out the math/big package.

package main

import (
    "fmt"
    "math/big"
)

func needInt(x *big.Int) *big.Int {
    y := big.NewInt(0)
    y.Mul(x, big.NewInt(10))
    y.Add(y, big.NewInt(1))
    return y
}

func needFloat(x *big.Rat) *big.Rat {
    y := big.NewRat(0, 1)
    y.Mul(x, big.NewRat(1, 10))
    return y
}

func main() {
    Big := big.NewInt(1)
    Big.Lsh(Big, 100)
    Small := big.NewInt(0)
    Small.Rsh(Big, 99)

    fmt.Println(needInt(Small))
    fmt.Println(needInt(Big))
    fmt.Println(needFloat(big.NewRat(Small.Int64(), 1)).FloatString(1))
    fmt.Println(needFloat(new(big.Rat).SetFrac(Big, big.NewInt(1))).FloatString(1))
}

In my Go example, Big and Small had to move into main(). Perhaps someone with more Go skills could make this look better, but without the ability to define the normal operators + and * for the types big.Int and big.Rat I'm not sure how much better it's going to get.

Maps

In Go, maps are built in just like strings:

package main

import "fmt"

type Vertex struct {
    Lat, Long float64
}

var m map[string]Vertex

func main() {
    m = make(map[string]Vertex)
    m["Bell Labs"] = Vertex{
        40.68433, -74.39967,
    }
    fmt.Println(m["Bell Labs"])
}

In C++ the standard library provides strings and maps:

#include <iostream>
#include <string>
#include <unordered_map>

struct Location {
  double latitude, longitude;
};

std::ostream& operator<< (std::ostream& stream, const Location& p) {
  return stream << '{' << p.latitude << ' ' << p.longitude << '}';
}

std::unordered_map<std::string, Location> m;

int main()
{
  m["Digilicious"] = Location{
    34.1161, -118.1761,
  };
  std::cout << m["Digilicious"] << '\n';
}

In Go, the fmt package has a default format for printing structs; in C++ the operator<<() function must be provided for the type Location.

Map literals

Go wins for brevity with it's built in data structures:

package main

import "fmt"

type Vertex struct {
    Lat, Long float64
}

var m = map[string]Vertex{
    "Bell Labs": {40.68433, -74.39967},
    "Google":    {37.42202, -122.08408},
}

func main() {
    fmt.Println(m)
}

In C++ you have to provide all the code to print maps and structs.

#include <iostream>
#include <string>
#include <unordered_map>

struct Location {
  double latitude, longitude;
};

std::ostream& operator<<(std::ostream& s, Location const& p) {
  return s << "{" << p.latitude << " " << p.longitude << "}";
}

typedef std::unordered_map<std::string, Location> NamedLocations;

std::ostream& operator<<(std::ostream& s, NamedLocations const& locs) {
  for ( const auto& loc: locs ) {
    s << loc.first << ": " << loc.second << '\n';
  }
  return s;
}

NamedLocations const locations {
  { "Digilicious", { 34.11602, -118.17606 } },
};

int main() {
  std::cout << locations;
}

C++ wins for completeness by including ordered and unordered maps, both. Also multi-maps and sets and on and on.

One major difference between the languages is that Go has a map type built-in, where C++ provides these types in the standard library. With C++ you can write your own map or unordered_map types that could work as neatly as the standard versions. With Go, you get the data structures that the language designers selected for you. Adding a new or different map type to Go would be as clumsy as the math/big situation.

Function closures

Closures: go has them.

package main

import "fmt"

func adder() func(int) int {
    sum := 0
    return func(x int) int {
        sum += x
        return sum
    }
}

func main() {
    pos, neg := adder(), adder()
    for i := 0; i < 10; i++ {
        fmt.Println(
            pos(i),
            neg(-2*i),
        )
    }
}

There are a few ways to skin this cat in C++. This example uses lambdas with C style I/O:

#include <cstdio>

auto adder()
{
  int sum = 0;
  return [=](int x) mutable -> int {
    return sum += x;
  };
}   

int main()
{
  auto pos = adder();
  auto neg = adder();

  for ( int i=0; i<10; ++i ) {
    printf("%d %d\n", 
           pos(i),
           neg(-2*i)
          );
  }
}

The old-school function object way (with iostream) looks like this:

#include <iostream>

class Addr {
public:
  int operator()(int x) {
    return sum_ += x;
  }

private:
  int sum_ = 0;
};

int main()
{
  Addr pos, neg;

  for ( int i=0; i<10; ++i ) {
    std::cout << pos(i) << ' '
              << neg(-2*i) << '\n';
  }
}

Both versions generate extremely tight code as sizeof(pos) == sizeof(int) and pos and neg are on the stack: with no heap allocation.

Concurrency

Go has goroutines, lightweight threads managed by the Go runtime.

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")
}

C++ has threads that (with GCC and Clang) correspond to OS threads.

#include <chrono>
#include <future>
#include <iostream>

void say(char const* s) {
  for ( int i=0; i<5; ++i ) {
    std::this_thread::sleep_for(std::chrono::milliseconds(100));
    std::cout << s << '\n';
  }
}

int main()
{
  std::thread t(say, "world");
  say("hello");
  t.join();
}

Thoughts

Generic programming is not a claimed feature of Go.

Valid HTML 5