This is the first part of a series. Here are all 3 parts:
I’m reading “The Go Programming Language” by Brian Kernighan and Alan Donovan. It is a perfect programming language introduction, clearly written and perfectly structured, with nicely chosen examples. It contains no hand-waving – it’s aware of other languages and briefly acknowledges the choices made in the language design without lengthy discussion.
As an enthusiastic C++ developer, and a Java developer, I’m not a big fan of the overall language. It seems like an incremental improvement on C, and I’d rather use it than C, but I still yearn for the expressiveness of C++. I also suspect that Go cannot achieve the raw performance of C or C++ due to its safety features, though that maybe depends on compiler optimization. But it’s perfectly valid to knowingly choose safety over performance, particularly if you get more safety and more performance than with Java.
I would choose Go over C++ for a simple proof of concept program using concurrency and networking. Goroutines and channels, which I’ll mention in a later post, are convenient abstractions, and Go has standard API for HTTP requests. Concurrency is hard, and it’s particularly easy to choose safety over performance when writing network code.
Here are some of my superficial observations about the simpler features, which mostly seem like straightforward improvements on C. In part 2 I’ll mention the higher-level features and I’ll hopefully do a part 3 about concurrency. I strongly recommend that you read the book to understand these issues properly.
I welcome friendly corrections and clarifications. There are surely several mistakes here, hopefully none major.
No semicolons at the end of lines
Let’s start with the most superficial thing. Unlike C, C++, or Java, Go doesn’t need semicolons at the end of lines of code. So this is normal:
a = b c = d
This is nicer for people learning their first programming language. It can take a while for those semicolons to become a natural habit.
No () parentheses with if and for
Here’s another superficial difference. Unlike C or Java, Go doesn’t put its conditions inside parentheses with if and for. That’s another small change that feels arbitrary and makes C coders feel less comfortable.
For instance, in Go we might write this:
for i := 0; i < 100; i++ { ... } if a == 2 { ... }
Which in C would look like this:
for (int i = 0; i < 100; i++) { ... } if (a == 2) { ... }
Type inference
Go has type inference, from literal values or from function return values, so you don’t need to restate types that the compiler should know about. This is a bit like C++’s auto keyword (since C++11). For instance:
var a = 1 // An int. var b = 1.0 // A float64. var c = getThing()
There’s also a := syntax that avoids the need for var, though I don’t see the need for both in the language:
a := 1 // An int. b := 1.0 // A float64 d := getThing()
I love type inference via auto in modern C++, and find it really painful to use any language that doesn’t have this. Java feels increasingly verbose in comparison, but maybe Java will get there. I don’t see why C can’t have this. After all, they eventually allowed variables to be declared not just at the start of functions, so change is possible.
Types after names
Go has types after the variable/parameter/function names, which feels rather arbitrary, though I guess there are reasons, and personally I can adapt. So, in C you’d have
Foo foo = 2;
but in Go you’d have
var foo Foo = 2
Keeping a more C-like syntax would have eased C developers into the language. These are often not people who embrace even small changes in the language.
No implicit conversions
Go doesn’t have implicit conversions between types, such as int and uint, or floats and int. This also applies to comparison via == and !=.
So, these won’t compile:
var a int = -2 var b uint = a var c int = b var d float64 = 1.345 var e int = c
C compiler warnings can catch some of these, but a) People generally don’t turn on all these warnings, and they don’t turn on warnings as errors, and b) the warnings are not this strict.
Notice that Go has the type after the variable (or parameter, or function) name, not before.
Notice that, unlike Java, Go still has unsigned integers. Unlike C++’s standard library, Go uses signed integers for sizes and lengths. Hopefully C++ will get do that too one day.
No implicit conversions because of underlying types
Go doesn’t even allow implicit conversions between types that, in C, would just be typedefs. So, this won’t compile
type Meters int type Feet int var a Meters = 100 var b Feet = a
I think I’d like to see this as a warning in C and C++ compilers when using typedef.
However, you are allowed to implicitly assign a literal (untyped) value, which looks like the underlying type, to a typed variable, but you can’t assign from an actual typed variable of the underlying type:
type Meters int var a Meters = 100 // No problem. var i int = 100 var b Meters = i // Will not compile.
No enums
Go has no enums. You should instead use const values with the iota keyword. So, while C++ code might have this:
enum class Continent { NORTH_AMERICA, SOUTH_AMERICA, EUROPE, AFRICA, ... }; Continent c = Continent::EUROPE; Continent d = 2; // Will not compile
in Go, you’d have this:
type continent int const ( CONTINENT_NORTH_AMERICA continent = iota CONTINENT_SOUTH_AMERICA // Also a continent, with the next value via iota. CONTINENT_EUROPE // Also a continent, with the next value via iota. CONTINENT_AFRICA // Also a continent, with the next value via iota. ) var c continent = CONTINENT_EUROPE var d continent = 2 // But this works too.
Notice how, compared to C++ enums, particularly C++11 scoped enums, each value’s name must have an explicit prefix, and the compiler won’t stop you from assigning a literal number to a variable of the enum type. Also, the Go compiler doesn’t treat these as a group of associated values, so it can’t warn you, for instance, if you forget to mention one in a switch/case block.
Switch/Case: No fallthrough by default
In C and C++, you almost always need a break statement at the end of each case block. Otherwise, the code in the following case block will run too. This can be useful, particularly when you want the same code to run in response to multiple values, but it’s not the common case. In Go, you have to add an explicit fallthrough keyword to get this behaviour, so the code is more concise in the general case.
Switch/Case: Not just basic types
In Go, unlike in C and C++, you can switch on any comparable value, not just values known at compile time, such as ints, enums, or other constexpr values. So you can switch on strings, for instance:
switch str { case "foo": doFoo() case "bar": doBar() }
This is convenient and I guess that it is still compiled to efficient machine code when it uses compile-time values. C++ seems to have resisted this convenience because it couldn’t always be as efficient as a standard switch/case, but I think that unnecessarily ties the switch/case syntax to its original meaning in C when people expected to be more aware of the mapping from C code to machine code.
Pointers, but no ->, and no pointer arithmetic
Go has normal types and pointer types, and uses * and & as in C and C++. For instance:
var a thing = getThing(); var p *thing = &a; var b thing = *p; // Copy a by value, via the p pointer
As in C++, the new keyword returns a pointer to a new instance:
var a *thing = new(thing) var a thing = new(thing) // Compilation error
This is like C++, but unlike Java, in which any non-fundamental types (not ints or booleans, for instance) are effectively used via a reference (it just looks like a value), which can confuse people at first by allowing inadvertent sharing.
Unlike C++, you can call a method on a value or a pointer using the same dot operator:
var a *Thing = new(Thing) // You wouldn't normally specify the type. var b Thing = *a a.foo(); b.foo();
I like this. After all, the compiler knows whether the type is a pointer or a value, so why should it bother me with complaints about a . where there should be a -> or vice-versa? However, along with type inference, this can slightly obscure whether your code is dealing with a pointer (maybe sharing the value with other code) or a value. I’d like to see this in C++, though it would be awkward with smart pointers.
You cannot do pointer arithmetic in Go. For instance, if you have an array, you can’t step through that array by repeatedly adding 1 to a pointer value and dereferencing it. You have to access the array elements by index, which I think involves bounds checking. This avoids some mistakes that can happen in C and C++ code, leading to security vulnerabilities when your code accesses unexpected parts of your application’s memory.
Go functions can take parameters by value or by pointer. This is like C++, but unlike Java, which always takes non-fundamental types by (non const) reference, though it can look to beginner programmers as if they are being copied by value. I’d rather have both options with the code showing clearly what is happening via the function signature, as in C++ or Go.
Like Java, Go has no notion of const pointers or const references. So if your function takes a parameter as a pointer, for efficiency, your compiler can’t stop you from changing the value that it points to. In Java, this is often done by creating an immutable type, and many Java types, such as String, are immutable, so you can’t change them even if you want to. But I prefer language support for constness as in C++, for pointer/reference parameters and for values initialized at runtime. Which leads us to const in Go.
References, sometimes
Go does seem to have references (roughly, pointers that look like values), but only for the built-in slice, map, and channel types. (See below about slices and maps.) So, for instance, this function can change its input slide parameter, and that change will be visible to the caller, even though the parameter is not declared as a pointer:
func doThing(someSlice []int) { someSlice[2] = 3; }
In C++, this would be more obviously a reference:
void doThing(Thing& someSlice) { someSlice[2] = 3; }
I’m not sure if this is a fundamental feature of the language or just something about how those types are implemented. It seems confusing for just some types to act differently, and I find the explanation a bit hand-wavy. Convenience is nice, but so is consistency.
const
Go’s const keyword is not like const in C (rarely useful) or C++, where it indicates that a variable’s value should not be changed after initialization. It is more like C++’s constexpr keyword (since C++11), which defines values at compile time. So it’s a bit like a replacement for macros via #define in C, but with type safety. For instance:
const pi = 3.14
Notice that we don’t specify a type for the const value, so the value can be used with various types depending on the syntax of the value, a bit like a C macro #define. But we can restrict it by specifying a type:
const pi float64 = 3.14
Unlike constexpr in C++, there is no concept of constexpr functions or types that can be evaluated at compile time, so you can’t do this:
const pi = calculate_pi()
and you can’t do this
type Point struct { X int Y int } const point = Point{1, 2}
though you can do this with a simple type whose underlying type can be const:
type Yards int const length Yards = 100
Only for loops
All loops in Go are for loops – there are no while or do-while loops. This simplifies the language in one way compared, for instance, to C, C++, or Java, though there are now multiple forms of for loop.
For instance:
for i := 0; i < 100; i++ { ... }
or, like a while loop in C:
for keepGoing { ... }
And for loops have a range-based syntax for containers such as string, slices or maps, which I’ll mention later:
for i, c := range things { ... }
C++ has range-based for loops too, since C++11, but I like that Go can (optionally) give you the index as well as the value. (It gives you the index, or the index and the value, letting you ignore the index with the _ variable name.)
A native (Unicode) String type
Go has a built-in string type, and built in comparison operators such as ==, !=, and < (as does Java). Like Java, Strings are immutable, so you can’t change them after you’ve created them, though you can create new Strings by concatenating other Strings with the built in operator +. For instance:
str1 := "foo" str2 := str1 + "bar"
Go source code is always UTF-8 encoded and string literals may contain non-ASCII utf-8 code points. Go calls Unicode code points “runes”.
Although the built-in len() function returns the number of bytes, and the built in operator [] for strings operates on bytes, there is a utf8 package for dealing with strings as runes (Unicode code points). For instance:
str := "foo" l := utf8.RuneCountInString(str)
And the range-based for loop deals in runes, not bytes:
str := "foo" for _, r := range str { fmt.Println("rune: %q", r) }
C++ still has no standard equivalent.
Slices
Go’s slices are a bit like dynamically-allocated arrays in C, though they are really views of an underlying array, and two slices can be views into different parts of the same underlying array. They feel a bit like std::string_view from C++17, or GSL::span, but they can be resized easily, like std::vector in C++17 or ArrayList in Java.
We can declare a span like so, and append to it:
a := []int{5, 4, 3, 2, 1} // A slice a = append(a, 0)
Arrays (whose size cannot change, unlike slices) have a very similar syntax:
a := [...]int{5, 4, 3, 2, 1} // An array. b := [5]int{5, 4, 3, 2, 1} // Another array.
You must be careful to pass arrays to functions by pointer, or they will be (deep) copied by value.
Slices are not (deep) comparable, or copyable, unlike std::array or std::vector in C++, which feels rather inconvenient.
Slices don’t grow beyond their capacity (which can be more than their current length) when you append values. To do that you must manually create a new slice and copy the old slice’s elements into it. You can keep a pointer to an element in a slice (really to the element in the underlying array). So, as with maps (below), the lack of resizing is probably to remove any possibility of an pointer becoming invalid.
The built in append() function may allocate a bigger underlying array if it would need more than the existing capacity (which can be more than the current length). So you should always assign the result of append() like so:
a = append(a, 123)
I don’t think you can keep a pointer to an element in a slice. If you could, the garbage collection system would need to keep the previous underlying array around until you had stopped using that pointer.
Unlike C or C++ arrays, and unlike operator [] with std::vector, attempting to access an invalid index of a slice will result in a panic (effectively a crash) rather than just undefined behaviour. I prefer this, though I imagine that the bounds checking has some small performance cost.
Maps
Go has a built-in map type. This is roughly equivalent to C++’s std::map (balanced binary trees), or std::unordered_map (hash tables). Go maps are apparently hash tables but I don’t know if they are separate-chaining hash tables (like std::unordered_map) or open-addressing hash tables (like nothing in standard C++ yet, unfortunately).
Obviously, keys in hash tables have to be hashable and comparable. The book mentions comparability, but so few things are comparable that they would all be easily hashable too. Only basic types (int, float64, string, etc, but not slices) or structs made up only of basic types are comparable, so that’s all you can use as a key. You can get around this by using a basic type (such as an int or string) that is (or can be made into) a hash of your value. I prefer C++’s need for a std::hash<> specialization, though I wish it was easier to write one.
Unlike C++’, you can’t keep a pointer to an element in a map, so changing one part of the value means copying the whole value back into the map, presumably with another lookup. Go apparently does this to completely avoid the problem of invalid pointers when the map has to grow. C++ instead lets you take the risk, specifying when your pointer could become invalid.
Go maps are clearly a big advantage over C, where you otherwise have to use some third-party data structure or write your own, typically with very little type safety.
They look like this:
m := make(map[int]string) m[3] = "three" m[4] = "four"
Multiple return values
Functions in Go can have multiple return types, which I find more obvious then output parameters. For instance:
func getThings() (int, Foo) { return 2, getFoo() } a, b := getThings()
This is a bit like returning tuples in modern C++, particularly with structured bindings in C++17:
std::tuple<int, Foo> get_things() { return make_tuple(2, get_foo()); } auto [i, f] = get_things();
Garbage Collection
Like Java, Go has automatic memory management, so you can trust that instances will not be released until you have finished using them, and you don’t need to explicitly release them. So you can happily do this, without worrying about releasing the instance later:
func getThing() *Thing { a := new(Thing) ... return a } b := getThing() b.foo()
And you can even do this, not caring, and not easily even knowing, whether the instance was created on the stack or the heap:
func getThing() *Thing { var a Thing ... return &a } b := getThing() b.foo()
I don’t know how Go avoids circular references or unwanted “leak” references, as Java or C++ would with weak references.
I wonder how, or if, Go avoids Java’s problem with intermittent slowdowns due to garbage collection. Go seems to be aimed at system-level code, so I guess it must do better somehow.
However, also like Java, and probably like all garbage collection, this is only useful for managing memory, not resources in general. The programmer is usually happy to have memory released some time after the code has finished using it, not necessarily immediately. But other resources, such as file descriptors and database connections, need to be released immediately. Some things, such as mutex locks, often need to be released at the end of an obvious scope. Destructors make this possible. For instance, in C++:
void Something::do_something() { do_something_harmless(); { std::lock_guard<std::mutex> lock(our_mutex); change_some_shared_state(); } do_something_else_harmless(); }
Go can’t do this, so it has defer() instead, letting you specify something to happen whenever a function ends. It’s a annoying that defer is associated with functions, not to scopes in general.
func something() { doSomethingHarmless() ourMutex.Lock() defer ourMutex.Unlock() changeSomeSharedState() // The mutex has not been released yet when this remaining code runs, // so you'd want to restrict the use of the resource (a mutex here) to // another small function, and just call it in this function. doSomethingElseHarmless() }
This feels like an awkward hack, like Java’s try-with-resources.
I would prefer to see a language that somehow gives me all of scoped resource management (with destructors), reference-counting (like std::shared_ptr<>) and garbage collection, in a concise syntax, so I can have predictable, obvious, but reliable, resource releasing when necessary, and garbage collection when I don’t care.
Of course, I’m not pretending that memory management is easy in C++. When it’s difficult it can be very difficult. So I do understand the choice of garbage collection. I just expect a system level language to offer more.
Things I don’t like in Go
As well as the minor syntactic annoyances mentioned above, and the lack of simple generic resource (not just memory) management, I have a couple of other frustrations with the language.
(I’m not loving the support for object orientation either, but I’ll mention that in a later article when I’ve studied it more.)
No generics
Go’s focus on type safety, particularly for numeric types, makes the lack of generics surprising. I can remember how frustrating it was to use Java before generics, and this feels almost that awkward. Without generics I soon find myself having to choose between lack of type safety or repeatedly reimplementing code for each type, feeling like I’m fighting the language.
I understand that generics are difficult to implement, and they’d have to make a choice about how far to take them (probably further than Java, but not as far as C++), and I understand that Go would then be much more than a better C. But I think generics are inevitable once, like Go, you pursue static type safety.
Somehow go’s slice and map containers are generic, probably because they are built-in types.
Lack of standard containers
Go has no queue or stack in its standard library. In C++, I use std::queue and std::stack regularly. I think these would need generics. People can use go’s slice (a dynamically-allocated array) to achieve the same things, and you can wrap that up in your own type, but your type, can only contain specific types, so you’ll be reimplementing this for every type. Or your container can hold interface{} types (apparently a bit like a Java Object or a C++ void*), giving up (static) type safety.
I just glanced over this but will have to read it properly once I have time – seems like a good overview! I don’t feel any real need to move beyond C++ though. However, I would like to write something proper in Python sometime.
A minor correction: the C loop in the 2nd example was not fully converted, so this would not work: for (i int := 0; i < 100; i++) {
Thanks. Fixed.
It’s still not completely correct. It should read:
for i := 0; i < 100; i++ {
as := only uses type inference. You also can't use a var statement in an initializer, hence the reason for the two (var and :=) in the language.
Thanks. I’ve now fixed that too. I didn’t know about that limitation.
My main concern about Go is that it is so much targeting serverside apps, that it does not have any *stable and maintained* libs for desktop apps (not only toolkit, but all tied to desktop handling like audio, image, 3D). All tentatives are, for most of them, simply binding to C with cgo (and cgo is awful, to be honest).
More and more, I’m seeing Go as a serverside-only language instead of a true general-purpose, and I’m happy with it for my serverside stuff.
For other things, I’m sticking with C for now (and a bit of Python).
I’m dreaming about Go will become a true general-purposed language. But the Golang & Google teams doesn’t seems to be interested in. If you looked at their Desktop apps, they are still using C++ only.
In the same time, Mozilla devlops Rust and use it in Firefox already. But I’m not interested in Rust anyway, was just an example of true general-purposing.
Even C bindings are better and easier to use in Go.
I use go-sdl2 to write a 2d game in Go. Although it’s a fairly straightforward conversion, it’s still much easier to use than C+SDL2.
I even tried love2d for prototyping and surprised to realize that it’s neither easier nor shorter than using Go directly.
Hello Friend,
nice overview! I enjoyed it.
Regarding the section “References, sometimes”, you might find this articles interesting because there is really no passes by references in Go:
https://dave.cheney.net/2017/04/29/there-is-no-pass-by-reference-in-go
This one explains more in detail what happens with maps and slices:
https://www.goinggo.net/2013/09/iterating-over-slices-in-go.html
Thanks for dedicating the time to write this blog entry :)
I believe that the Garbage collection example is somewhat wrong.
C++ heap allocated pointers can escape the function stack safely..
I think what you meant to write is :
func getThing () Thing* {
a := Thing() //looks like stack allocated, UB in c/c++
…
return a
}
Thanks. Yes. That code was just meant to show that you don’t need to care about destroying the instance sometime. But that wasn’t clearly stated, and it should also show that it’s not just relevant when you create something with new. I’ve updated it, adding your suggested code.
correction.. :P
func getThing () Thing* {
a := Thing() //looks like stack allocated, UB in c/c++
…
return &a
}
One small correction: the article states that append does not grow a slice past it’s initial capacity, but it does. You don’t have to manually copy anything. Append returns a value, which is the new slice with the new value appended. It does this specifically because when you append to a full slice, it must allocate a new slice and copy the values over, resulting in a slice with a different underlying array. Because you can’t change what array a slice points to, it must create a new slice pointing to the new array, which is what it returns. When the original array is large enough to take the new value, it returns the original slice.
This can also be used to append to a zero capacity slice or even an uninitialized (nil) slice.
Thanks. Yes, I’ve fixed that. I got the wrong idea from the discussion of an appendInt() function in the book. It’s meant to be a discussion of how append() could be implemented, but I thought that we actually had to implement something like that ourselves.
Incidentally, I guess the book couldn’t show the real append() implementation because it’s generic (but built-in) so it can’t be implemented using only the language itself.
Nice article!
I think it might be interesting to mention that slices are views into underlying arrays. It basically means that golang wouldn’t allocate new slice, but it would rather allocate new array and return its view (which is a slice).
Thanks, but it does already say “though they are really views of an underlying array, and two slices can be views into different parts of the same underlying array.”
I really like this concise walk-through. Very much to the point. Looking forward to the coming parts.
I’m only doing C++ and Python at work, but looking at Go and Rust has long been on my TODO, just haven’t gotten around to it.
Will you do a series on Rust? :)
You really should have a look at Go. It only takes a couple of hours to learn, and it’s build-system is dead-simple. (No makefiles, build scripts, or other project metadata files to maintain).
Yes, I forgot to say that I did take a course on concurrent programming with Go while I was still at uni. I liked it quite a lot, just haven’t done any real projects with it yet.
> You have to access the array elements by index, which I think involves bounds checking
Go also has the range keyword to iterate over each element of a slice, array, or channel.
> Only for loops
You forgot the for {} syntax which is an infinite loop. :-)
> I wonder how, or if, Go avoids Java’s problem with intermittent slowdowns due to garbage collection
The Go team has done a nearly miraculous job of reducing GC pause times. For the first several versions, it was commonly in the hundreds of milliseconds. Then it dropped to 40ms, then to 10 ms and currently to 100us. In my application, we typically see 40us pause times.
One thing you don’t mention that a lot of people love is the ability to produce statically linked binaries, which makes for super-easy deployment. Given your C/C++ background, you’d expect this, but a lot of folks coming “down” from higher-level languages such as Ruby, Python, Java, etc. find this to be one of their favorite features.
Not only is it an ability, it’s the default. :)
Not that it is often used, but it is possible to register a function to be called when an object would otherwise be garbage collected.
https://golang.org/pkg/runtime/#SetFinalizer
Using this is subtle, and not normally done unless required to clean up and keep running.
In the “No implicit conversions because of underlying types” section, you’ve got the type name in the wrong place in your “type” statements. You’ve also used “:=” with “var”, which is a syntax error.
I think the important distinction here is that while C/C++’s typedef statement defines an alias for a type, Go’s “type” statement defines a new type. The most obvious difference is that the new type and its underlying storage type will have distinct method sets.
For “References, sometimes” those types contain pointers to some other backing storage (e.g. the array backing a slice). The types are still passed by value, but it only involves a shallow copy.
For maps and channels, this is effectively a reference, but for slices the two values will have independent length/capacity but a shared backing array.
On the subject of garbage collection, one interesting feature is that the language is a bit fluid about stack vs. heap usage. You’ve already noted that you can return a pointer to a local variable, and have it still be valid, which is an example where code that would traditionally stack allocate a variable actually generates code that allocates it on the heap.
What you might not realise is that the reverse can also happen. If you use “new” to allocate a value on the heap, and the compiler determines that no references to the value escape the scope, it will generate code that instead allocates it on the stack. This can also happen with the backing storage for slices that don’t escape their scope.
> type name in the wrong place in your “type†statements. You’ve also used “:=†with “varâ€, which is a syntax error.
Thanks. Fixed.
> I think the important distinction here is that while C/C++’s typedef statement defines an alias for a type, Go’s “type†statement defines a new type.
Yes, and I see that the latest version, go 1.9, has a type alias feature, which is like a typedef (or C++ “using” type alias): https://tip.golang.org/doc/go1.9
The second block of example code in “No implicit conversions because of underlying types” is still broken.
Thanks again.
It still reads “type int Meters” rather than “type Meters int”. It took me a while to get used to the ordering too, but it does make complex types easier to read (compare C’s syntax for declaring function pointer types).
One other interesting thing about the “No semicolons at the end of lines” point is that there is no code style arguments about whether you should add a new line before the brace in constructs like functions, if statements, and loops.
The automatic semicolon insertion logic makes one option will turn code like “if condition\n{” into “if condition;{“, which is syntactically invalid.
Not sure what you mean by “Unlike C++’, you can’t keep a pointer to an element in a map, so changing one part of the value means copying the whole value back into the map, presumably with another lookup. Go apparently does this to completely avoid the problem of invalid pointers when the map has to grow. C++ instead lets you take the risk, specifying when your pointer could become invalid.” Map values are commonly pointers in my experience; keys can be pointers too – but to what purpose? The following is a valid program: https://play.golang.org/p/Pn0OqPMgAN.
Thanks. Obviously if the value type is a pointer type then copying that will just give you a pointer. But I don’t think you can get a pointer to a regular value that is in a Go map.
C++ allows this, for instance, though this is a silly example:
std::map<int , std::string> m;
…
auto& str = m[2]; // Actually, I’d use m.find() with a check to be safer.
if (str == “2”) {
str = “two”;
}
I’m using a reference there (auto&) instead of a pointer, but that’s much the same thing. It’s something that I do sometimes, partly to avoid extra lookups (though they might be optimized away anyway) and to structure my code. Not being able to do it in Go is not a big deal, particularly if repeated lookups are optimized away or cached – I just thought it shows something about the safety/performance/simplicity choices in Go.
One small correction on the for range loop on slices optionally giving you the index, I would say that it optionally gives you the value. You actually get `for idx := range slice` and the index must be explicitly ignored with `for _, value := range slice`
Thanks. I don’t think I meant it exactly in terms of the syntax used to get the index, but I’ve updated the text to make that clearer.
No worries, it was just that when I read it I remembered that that was something that was unexpected for me when I started out with go.
> I don’t know how Go avoids circular references or unwanted “leak†references,
> as Java or C++ would with weak references.
Garbage Collection in Go (and in Java and everywhere else) doesn’t need to avoid circular references (Reference Counting needs to do that, e.g. in Swift).
Garbage Collection doesn’t release an object when if its (strong) reference count is zero, but when it’s no longer reachable from within the running code (which probably means that it can’t be reached either directly or transitively by references found on the stack. As a longtime Java dev my knowledge about memory management is extremely limited :P ).
So even if you have a circular dependency ring, all elements within are just marked unreachable and then purged within the same gc cycle. (Google “mark and sweep algorithm”.)
Nice post! I’ve also started learning Go. My notes in the form of Q&A are here (work in progress): https://github.com/sahajRe/goFAQ. It contains the comparison in the tabular format, unlike yours is more detailed with examples.
> I don’t think you can keep a pointer to an element in a slice.
You actually can as well as you can keep pointers to struct members.
> If you could, the garbage collection system would need to keep the previous underlying array around until you had stopped using that pointer.
That’s exactly what happens and a popular way to gang-allocate a larger bunch of elements.
Go has numerous features to pre-allocate larger collections.
Thanks. Could you give me an example of how to get a pointer to an element in a slice, please.
Regarding enums, one can also type them to avoid e.g. assigning int literals:
https://andrey.nering.com.br/2016/constants-and-enums-in-go-lang/
Well, literals are untyped, so they’ll pass right through typed “enums”: https://play.golang.org/p/ho9DeiKs8l
Oh indeed. The typing won’t help much then.
Nope :) not when it comes to literals anyway. But they’re still good for documentation / readability, and of course for typed values.
This was a well thought out and elaborated post, but I’d like to make brief mention of a couple of items… I have immense respect for both languages, so hopefully this seem fair.
(1) In the section about declarations (type before vs after the variable name) the notion that C programmers don’t like “changes to the language” doesn’t really apply, because it’s not C and therefore is not a change to “the” language.
An []int is a different type than [1]int or [2]int. There are good reasons for this:
ref: https://blog.golang.org/gos-declaration-syntax
(2) An often overlooked item when comparing Go to C++ is the standardized, open source aware toolset that Go provides. If you have Go at all, you already have nice, standard tools. The significance of this is often overlooked in side by side comparisons of language features — if you mentioned it in another post, I apologize.
(a) Having an *included* compiler and simplified build and deploy system with a large standard library is preferred to learning numerous different build tools with different flags. e.g. was that clang or g++ -o3 -std=c++11, or make, gmake, cmake.. &c. They are not all special cases like qmake… it’s just an external ecosystem of tools that you need to accept dealing with in order to play.
These external tools are necessary baggage with C++, even if not including them seems justifiable as innovations are left to the world at large while the language and standard library are focused on.
Other teams chose differently and honestly, it really shows in Go and is worth considering.
(b) You also get an included mechanism for unit testing that integrates with the toolset, i.e. go test
(c) You also avoid holy wars over code format via standard formatting , i.e go fmt
(d) You also have internet and open source aware tools for integrating with the community, i.e. go get (-u)
(e) You have an included race condition tool.
The impact of these standard features are a welcome part of the language for gophers. ( I don’t represent them or anything, I just like it so far). You don’t to download mingw, cygwin, or do backflips around the backyard to get up and running.
(3) Clever C++ advances like ‘auto’ and move constructors have analogs, but with simpler syntax in general.
It looks a bit like Pascal.