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.
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.
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.
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.
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.
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.
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();
}
Generic programming is not a claimed feature of Go.