-
Notifications
You must be signed in to change notification settings - Fork 7
GoNotes
This page contains misc notes about Go that might be particularly helpful for people coming from C++ or related languages.
General summary that covers a lot of the basics: Go For C++ Programmers and Another One
In C++ terms, an interface in go creates a virtual table -- anytime you need virtual functions, you must create an interface.
I was confused about the nature of inheritance in Go for a long time, in part because the Go folks like to emphasize the idea that there is no inheritance in Go, and in general like to emphasize the differences from C++ (mostly for good reason, because Go offers a lot of additional flexibility for which there are typically many advantages, BUT sometimes, a standard C++-style OOP pattern is useful, and this happens to be the case in GoKi -- see Design for discussion).
Anonymous embedding another struct type at the start of a given struct gives transparent access to the embedded types -- e.g., you can call their methods directly, and access their fields as well (and so on for the embedded types of that type), so it looks like inheritance in C++. But, critically, these embedded methods are NOT virtual, EVEN if they are part of an interface, unless you take one extra step (described below).
In short, while it is true that there is no C++-like inheritance for standard struct methods, once you have an interface that defines methods, almost everything functions just like in C++, with a virtual table. Through anonymous embedding, you can inherit base-class definitions of interface methods, and selectively override (redefine) the ones you want to change for the sub-class, no problem!
Key Difference from C++: However, when you call another interface method from another method (whether that other method is in the interface or not), the receiver type will always be the exact one that the method is defined on, NOT the overriden version that you might have defined on the actual struct type in question. However, it turns out there is a simple way to avoid this problem, which is employed in GoKi: Keep an interface pointer available in your struct itself (e.g., as the This
field in the Node
struct that implements the Ki
interface in GoKi), and call any interface methods through the this pointer:
func (n *Node) MyMethod() {
n.This.OtherMethod() // properly calls overridden version.
}
Thus, in the specific cases when the standard C++ OOP pattern does make sense to use in Go, you can use this technique to achieve the full inheritance and overriding features of C++. Again, see the Design page for discussion of why this pattern makes sense for GoKi. Furthermore, having the This
interface pointer is great for many other uses, because only the interface pointer truly knows what actual type of object you have, when you don't otherwise have that info. Thus, by using the interface pointer, you can find out everything you need to know about a generic object. This is important because again that information is lost whenever you're in the context of a method defined on a particular type.
Another problem arises when you have a pointer to a struct that you know to be at least of a given type, but it might actually be something that just embeds that type. If you try to convert the pointer to the known minimum type, it will fail for the "derived" type, because there is no implicit type conversion in Go:
bt := obj.(*BaseType) // this will fail if obj is actually a derived type of BaseType
To get around this problem, the kit
package defines an EmbeddedStruct
method, and the Ki
interface provides this convenience directly, which you can use like this:
bt := obj.EmbeddedStruct(KiT_BaseType).(*BaseType) // this will always work if obj is at least BaseType
EmbeddedStruct basically just finds the embedded type buried within the derived type, and gives you an interface{}
that is directly of that type.
Although it seems like a really easy thing to fix in Go, to make it support derived types in this way, it does require a (little bit expensive) dynamic (runtime) traversal of the reflect type info, and it wasn't that hard to write, so it seems reasonable to just have it available as an option.
In general it is better to provide an explicit interface method to provide access to embedded structs that serve as base-types for a given level of functionality. Or provide access to everything you might need via the interface, but just giving the base struct is typically much easier.
Although the derived types have direct access to members and methods of embedded types, at any level of depth of embedding, the reflect
system presents each embedded type as a single field as it is declared in the actual code. Thus, you have to recursively traverse these embedded structs -- methods to flatten these field lists out by calling a given function on each field in turn are provided in kit embed.go
: FlatFieldsTypeFun
and FlatFieldsValueFun
Go makes extensive use of the interface{}
type which is effectively a built-in universal Variant type in Qt http://doc.qt.io/qt-5/qvariant.html -- you use type switches or conversions or the reflect system to figure out what kind of thing it actually is -- extremely powerful yet not very dangerous it seems.
It is very convenient to use anonymous functions directly in the FuncDown
(etc) and Signal Connect
cases, but for performance reasons, it is important to be careful about capturing local variables from the parent function, thereby creating a closure, which creates a local stack to represent those variables. In the case of FunDown / FunUp etc, the impact is minimized because the function is ONLY used during the lifetime of the outer function. However, for Signal Connect
, the function is itself saved and used later, so using a closure there creates extra memory overhead for each time the connection is created. Thus, it is generally better to avoid capturing local variables in such functions -- typically all the relevant info can be made available in the recv, send, sig, and data args for the connection function.
As the docs state, you really have to use it for a while to appreciate all of the design decisions in the language, so these are very preliminary and subject to later reconsideration..
- Reference arg type -- the receiver arg (the "this" pointer in C++) has automagical ability to either be a pointer or a value depending on the function declaration, but other args do NOT have this flexibility. It means that the caller has to do a little bit of extra work adding a & or not where necessary. The counter-argument is presumably that this means that the caller KNOWS that by passing a pointer, the value of the arg will be changed, and that is probably a good thing. The same point could be made for the receiver arg, but there I suppose the syntax would have been a bit clunky and confusing.