Hacker News new | past | comments | ask | show | jobs | submit login
Golang concepts from an OOP point of view (github.com/luciotato)
200 points by markkit on Sept 12, 2016 | hide | past | favorite | 46 comments



While I appreciate that this is a work in progress and meant for learning, i just want to caution readers and those new to Go that this is a somewhat dangerous way to approach Go.

Simply mapping familiar vocabulary from an traditional OO language into Go implies that the differences are superficial and the underlying concepts are largely the same. The truth is that a lot of the traditional vocabulary was very intentionally left out of Go.

For example, mapping embedded stucts to "multiple inheritance" masks the fact that Go is trying to guide you into understanding the power of composition over inheritance. It's not that it's impossible to understand that, but by carrying over the terminology, you burden yourself with some preconceptions.

Like I said, I know this is a work in progress, but I encourage you to perhaps map the terms but then write a paragraph or two about how they're _different_, not the same.

This was a really nice resource for me when I first came to Go: http://spf13.com/post/is-go-object-oriented/


>Simply mapping familiar vocabulary from an traditional OO language into Go implies that the differences are superficial and the underlying concepts are largely the same. The truth is that a lot of the traditional vocabulary was very intentionally left out of Go.

I find that what has been "intentionally left out of Go" is not necessarily a good choice.

I also don't get the obsession with "idiomatic" programming.

Idiomatic Java/J2EE circa 1998-2008 was total crap (the programming style, not the language). Even its then adherents admit it. Same holds for many other languages/times.

Programmers should be consistent with a style, and teams should work with an agreed upon style. But some languages designers to impose their ideas upon all users of a language is wrong.


>Idiomatic Java/J2EE circa 1998-2008 was total crap (the programming style, not the language)

I think part of the aim of Go was to make this style impossible, by e.g. removing inheritance, which plays a key role in most crappy Java code.


Go's lack of generics however (except for special blessed built ins that are OK) forces some of those bad patterns however.

Want to write a linked list that works on all types? Hope you like reflection or annoying code generation or copy and paste.


or casting, which is a selective bit of loss of type safety (& a speed hit), but is what the std library linked list impl actually uses, and is what pre-generics java & objc used forever.

it ain't great, but it ain't a disaster, either.


It's 1990s coding in 2016. We have a better way, and even go implements it for certain special types.


i'm not saying it's great, but it's the most practical thing in most cases in go, & it was missing from your list.

whether or not go would be better with generics is still an open question imo.


> This is a discovery process, I'm writing this document to help myself understanding golang and maybe help others.

First: I took a brief look through this, and I didn't see anything that jumped out as wrong. It's a pretty comprehensive document, especially for someone who says they're just starting out, so kudos to James for writing it!

That said, from my experience in teaching Go classes and workshops for beginners, comparing embedding to inheritance is usually a bad idea. It's not that there aren't similarities, but in my experience it prompts new Go programmers to write unidiomatic Go code when they think of embedding in that way. They end up embedded structs the way they'd use inheritance in object-oriented languages, and since Go isn't really intended as an object-oriented language, it makes for unidiomatic code.

The main use cases of inheritance in OOP are accomplished by interfaces in Go, not struct embedding. But at the same time, the main benefits of embedding come from embedding interfaces - embedding structs is almost an afterthought in the language. In fact, I tend to advise people against embedding structs altogether. There are a few cases where it's worthwhile to embed structs, but the use case for embedding interfaces is much more powerful.

My advice for Go beginners: use interfaces more than you think you need to, and forget that struct embedding is even possible. This rule of thumb helps to write more idiomatic Go code when you're just getting started, before you have a feel for what that really means.


Go discourages classical inheritance, but otherwise it seems pretty object-oriented to me. Interfaces are object types. The only operation they support is calling methods. That's pretty much what objects are: self-contained entities with internal state (fields) and externally visible behavior (methods).

If anything, C++ is less object-oriented than Go, and more oriented towards abstract data types (which C++ programmers call “concepts”).


> Go discourages classical inheritance,

Go makes classic inheritance impossible, it is a bit more than discouraging it.


Go might not provide built-in support for classical inheritance, but that doesn't prevent you from rolling your own:

(0) Embed base structs.

(1) Implement non-overridden methods as redirected calls to base struct methods.

(2) Implement overridden method calls however you wish.

What Go doesn't make possible is tying inheritance to subtyping, like Java and C++ do. And this is a very good thing.


> Go might not provide built-in support for classical inheritance, but that doesn't prevent you from rolling your own:

What you described is not inheritance. If you pass your "inherited" type to a function that expect the "parent" type it will not compile. There is no inheritance in Go, and you cannot roll you own.

> What Go doesn't make possible is tying inheritance to subtyping

There is no subtying whatsoever in Go to begin with. There is no point in "inheritance" if it is not polymorphic. What you just did is create a new type. that is not inheritance in a statically typed language.


> What you described is not inheritance. If you pass your "inherited" type to a function that expect the "parent" type it will not compile.

That is called “subtyping”, not “inheritance”. Java and C++ happen to conflate the two concepts, but it's perfectly possible to keep them separate.

> There is no subtying whatsoever in Go to begin with.

A struct is a subtype of every interface its methods are compatible with.


It doesn't really matter. Your suggestion is worthless in practice with Go since "your subtype" doesn't enable polymorphism. you can't pass your "subtype" to a function that expect the parent type. You can absolutely pass a sub class in place of a class in a method expecting the class in Java. See the difference ? what you suggested makes absolutely no sense in Go. That's not even subtyping, you just created another struct type with composition.


> It doesn't really matter. Your suggestion is worthless in practice with Go since "your subtype" doesn't enable polymorphism.

I never said that struct embedding creates a subtype. I said that it can be used to emulate inheritance (subclassing). Inheritance and subtyping are very different things, and the fact languages like Java and C++ conflate them actually creates several problems. See:

[0] http://www.cs.utexas.edu/~wcook/papers/InheritanceSubtyping9...

[1] http://stackoverflow.com/a/23592298/46571


Interesting. I'm moving away from interfaces and toward plain structs (not consciously, but naturally). I tend to only use interfaces when I want polymorphism, not modularity. This is probably a consequence of me moving away from a mock-all-the-things approach to testing.


I recently heard a rule of thumb: "accept interfaces, return structs".

I haven't put it into practice, but it seems like this should give callers enough flexibility to pass in whatever they like.


Indeed I remember someone laying out some Go good practices (Rob Pike? Andrew Gerrand? damn can't recall) and "accept interfaces, return types" was definitely one of them. Again, a rule of thumb that works very well in most (not all) cases, and, conversely, a code smell.


That really depends on what you're tying to do. Say you have a Datastore interface, which has some methods like "Create", "Update", "Delete", and a MySQL + an inmem implementation.

You can have `datastore.New() Datastore`, which returns one of the implementations, or you can have `datastore.NewMySQLStore, datastore.NewInMemStore()`. Now you need a different method for each implementation.

Also be careful with returning concrete type, you can run into this issue: https://play.golang.org/p/WHAW1X6elS


Wow, that seems like a big issue. Is this accepted as a bug and being worked on or yet another feature?


It's not a bug. Interfaces are reference types. In this case, the interface is implemented by a pointer type. The interface can be thought of as a pointer to a nil pointer, but the "interface pointer" itself is not nil.

This is a tricky concept until you learn that interfaces are reference types.

Here's a simplified version of the previous example that illustrates this concept: https://play.golang.org/p/pODBgaHXq5


Hmm, I'm not sure that rule of thumb is very good. I think a better one would be "accept interfaces when you want polymorphism". Often I have a very high degree of confidence that your caller will never want to pass a different implementation, and creating the extra interface for an improbably hypothetical situation just isn't worth my time? I'm not making any statements here, just wondering out loud.


Toward what kind of testing, mind sharing?


I'm not sure that there is a name for it, but I construct my unit with its real dependencies (except for I/O or IPC). I've found that mocking ends up tightly coupling your tests to the implementation (not the behavior) of your unit. This means changes to your unit's implementation (that don't change its behavior) create a bunch of test failures. Also, mocking in Go isn't as easy as in Python, so even installing the mocks is a bit of work.


I just want to point out that embedding structs does not look like an afterthought at all, the Plan 9 C compilers had a feature akin to it for ages.

http://plan9.bell-labs.com/sys/doc/comp.html


Is embedding interfaces still composition, in your opinion? And if not, does this means you recommend not using composition?


If you like these notes you might also like my article on the subject: http://hackthology.com/object-oriented-inheritance-in-go.htm...

In general, using struct embedding to simulate sharing code and data is pretty limited. I rarely use this feature in my own code. However, having structs which automatically implement interfaces is awesome because you can "say what you need." You can also compose the interfaces together by embedding them. For example of that, checkout the Map type in my data-structures repository: https://godoc.org/github.com/timtadh/data-structures/types#M...


Struct embedding has been mostly unpleasant in my experience. When a type is embedded, only the methods that are explicitly on the interface/struct actually get propagated, rather than all of public methods. This means that wrapper types break optimizations around type punning. For example: https://play.golang.org/p/vIo1HtAeb4

This happens within the standard library in ways that hurt performance. The 'image' package allows people to register their own decoders but limits the API to an io.Reader. It can be an optimization if you could cast the inputted reader to another more efficient type (like a ReaderAt), but you can't because the image package wraps the Reader in its own "peeking" type. There's no way to tell the type was embedded, or what other actual methods might have worked.


> "receiver implict this parameter"

The parameter is actually quite explicit (unlike some other languages).

It would probably help your write up to talk about the difference between (using your example):

  func (r Rectangle) Area() float64 {
and

  func Area(r Rectangle) float64 {
As well as (this is a common rookie mistake):

  func (r Rectangle) SetWidth(w float64) {
    r.Width = w // does nothing
and

  func (r *Rectangle) SetWidth(w float64) {
    r.Width = w


You might want to point to point that (if?) the second example does work? (And that the difference is the *)


"Go is a great OO language" is quite a simple and interesting take on it.

https://www.youtube.com/watch?v=HqZQBmNVhBw


Author here, any pull request fixing typos, spelling and grammar, is welcomed.


I would use the real name of the language, Go.

Other than that, nice article.


I think it also highlights how a lot of programmers have learned everything only from one point of view. Concepts like interfaces, structures and types are general and abstract, they should not be explained in terms of OOP, instead they should be used as tools to explain OOP.


thanks, this is helpful for quick understanding of go


Go has most of the machinery of objects, but not the encapsulation. There are no private fields in structs.


There are, actually --- any field which begins with a lower case letter is private, any field which begins with an upper case letter is public. (Don't ask me what happens if the field begins with a letter which has a single form and does not have an explicit upper or lower case.)

Privacy is only enforced between packages, so anything in the same package as the struct can see the private fields.


> Don't ask me what happens if the field begins with a letter which has a single form and does not have an explicit upper or lower case.

So, on to the spec: https://golang.org/ref/spec#unicode_letter

> In The Unicode Standard 8.0, Section 4.5 "General Category" defines a set of character categories. Go treats all characters in any of the Letter categories Lu, Ll, Lt, Lm, or Lo as Unicode letters, and those in the Number category Nd as Unicode digits.

Which reads:

    Lu = Letter, uppercase
    Ll = Letter, lowercase
    Lt = Letter, titlecase
    Lm = Letter, modifier
    Lo = Letter, other
So I suppose (but am not sure) the case you describe falls into Lo.


Well, yes kind of.

    type foo struct {
        a int // "private"
        B int // "public"
    }
You cannot specify default values and Go refers to them as "exported" not private, but technically they are "private" in the sense that they can only be accessed via methods.


Technically, exported/unexported is different than public/private since its at the package level. You can access unexported fields of a struct from anywhere within the same package (not just from that struct's "methods"), which isn't true of a 'private' field.


IIRC, that is also true for protected members in Java (it has been a while, though, since I have written anything in Java).

If that is a concern, you can create a new package for your type.


is a field named "ß" private or public? Or ĸ?


An identifier is exported if both:

1. the first character of the identifier's name is a Unicode upper case letter (Unicode class "Lu"); and

2. the identifier is declared in the package block or it is a field name or method name.

All other identifiers are not exported.

https://golang.org/ref/spec#Exported_identifiers

Both of the characters you asked about are lowercase:

http://www.fileformat.info/info/unicode/char/00df/index.htm

http://www.fileformat.info/info/unicode/char/0138/index.htm

Here's the full list of Lu characters:

http://www.fileformat.info/info/unicode/category/Lu/list.htm


Here are the rules: https://golang.org/ref/spec#Exported_identifiers

    An identifier may be exported to permit access to it from another package.
    An identifier is exported if both:
    
        the first character of the identifier's name is a Unicode upper case letter (Unicode class "Lu"); and
        the identifier is declared in the package block or it is a field name or method name.

    All other identifiers are not exported.


Definitely one or the other, but the confusion means it's best practice to avoid fancy letters for identifiers.


Well, there's people writing code in other languages.

I know in Japan and Germany it's still kinda common for larger companies to build codebases in local languages.

Which in turn can lead to these issues.




Join us for AI Startup School this June 16-17 in San Francisco!

Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: