Hacker News new | ask | show | jobs
by MetaCosm 4242 days ago
Composition. That is the single biggest thing that has impressed me as my codebase has grown. Concurrency and messaging is nice, but I come from Erlang... I am not easily impressed by concurrency and messaging. Composition, the power of interfaces in complex systems is the key for me. It is what makes me stay with Go, and why I will probably stick around for a long time. It is so obnoxiously useful, without ever getting in my way. Let's talk for a second about the tiny little function

io.Copy(dst io.Writer, src io.Reader)

It reads data from reader and writes to writer... simple. Now what makes this little function so darn useful is it takes anything fulfilling its interfaces (io.Writer and io.Reader). The first way you will probably use it will be to copy between some stream and a file without having to eat up all the memory to store the buffer (not using ioutil.ReadAll for example)... but then you realize you can use a gzip compressor on the writer side, or a network socket, or your own code... and io.Copy works with anything that fulfills its interface.

As you build out a complex application, you start by creating your own functions that take advantage of existing interfaces foo.OCR(dst io.Writer, src img.Image). After that you start building out your own interfaces... like a MultiImage interface that has ImageCount and ReadImage methods that returns the count of images and binary data... but then you realize the images could be big, so you make the ReadImage method return an io.Reader... and now you have gone full circle and are using io.Copy to copy imageX from a stack of images to return to your OCR function that will output the data to an io.Writer which you made actually a gzip writer because text compresses well.

Beyond composition -- obviously, the concurrency and messaging is nice and when you need it vital. The other thing that will help make Go "click" is being very "data oriented" in your design... be vicious and minimal: http://youtu.be/rX0ItVEVjHc (great talk on data oriented design) and be absolutely pragmatic, focus on getting shit done, always...

4 comments

>It reads data from reader and writes to writer... simple. Now what makes this little function so darn useful is it takes anything fulfilling its interfaces (io.Writer and io.Reader). The first way you will probably use it will be to copy between some stream and a file without having to eat up all the memory to store the buffer (not using ioutil.ReadAll for example)... but then you realize you can use a gzip compressor on the writer side, or a network socket, or your own code... and io.Copy works with anything that fulfills its interface

And how is that any different than any language with interfaces? (Besides the implicit thing?).

Because of the implicit thing! The api designer doesn't have to write the interface, you can do it yourself. As long as naming conventions are kept to and the signature matches, you can apply this anywhere.

Imagine a close() interface in Java. There isn't one - but having a try {...} finally { x.close(); } can be very useful sometimes. But having interface graphs made of granular interfaces makes everything slow and causes a lot of complexity when you try to think about your type hierarchy. That's why interfaces always just grow and you can't use them any longer because other classes only implement part of the api. Something usable for all of awt, swing, file handling, random foreign libraries? Unthinkable. Also, adding something in a later release (like CharSequence in 1.4) can have a wide ranging impact and requires you to change a lot of code.

In Go, you just add an interface from the union set of multiple structs api - and you can use it. No matter who wrote those structs. The value is enormous. Think of it as something like dependency injection at compile time.

>Imagine a close() interface in Java. There isn't one

https://docs.oracle.com/javase/7/docs/api/java/lang/AutoClos...

You can even leave out the finally when you use an autocloseable resource.

    //r will be closed no matter what
    try(Resource r = getResource()) {

    } catch(SomeException e) {

    }
My Java is getting rusty... I read about this sometime somewhere but forgot it, thanks! I should have picked something like "String getText()", then.
> Because of the implicit thing!

Known as structural typing and available in most modern languages.

available in most modern languages

Well, "modern" is an ill-defined concept. As far as I'm aware, structural typing is not really that common, is it? Besides OCaml and Scala, is there any relevant (used outside of academia) language that supports it?

D and C++ templates for example.

F# also supports it, given its ML linage.

C# tricks with dynamic, although in this case it is dynamic typing, so not really the same thing.

The disadvantage of C++ templates is the structural type is implicit - you only know if the input object satisfies the type if you read the documentation, code, or can decipher the error message that occurs if it didn't.

Concepts would have fixed this, but we don't have concepts and maybe never will!

Would rust's "traits" count as structural typing? (I know Rust may not count as "relevant (used outside of academia)" yet, but maybe in the future.)
Haskell's typeclass provide it.
Typeclasses aren't structural typing, they are nominative typing, as typing is controlled by explicit declaration of relations between types and typeclasses, not inferred from structural properties.
Like duck typing?
I suspect this is like duck typing with some verification before compilation : the compiler checks that the object sent as parameter implements the correct interface.
It's called structural typing.
So, compile-time duck typing.
> Now what makes this little function so darn useful is it takes anything fulfilling its interfaces (io.Writer and io.Reader).

I've implemented InputStreams from byzantine transport layers in Java that work with the standard library. I don't quite understand what is special about this concept in Go (maybe it's nice for people coming from typeless, messy dynamic languages or nice languages with horribly inflexible and ad-hoc standard libraries like Python and Ruby).

Go has nothing unique (and maybe that makes it special). I mean that sincerely, everything it has, has been done dozens of times. Interfaces in Go are implicit (which is important, IMHO) and really, tremendously simple. These two features make them exceptional easy to use, and ACTUALLY used.

I have used interfaces (or equivalent concepts) in dozens of languages and they always felt like far more of a chore, explicitly using X interface or Y interface, ugly complex declarative specs, etc. Go just makes it painless.

replace Go with Python in the text, and you won't see a difference: there are iterable, keyable, file-like interfaces, etc. And creating your own is trivial.
Yes, but Python doesn't compile to a single binary, and usually runs much slower.

What people like about Go is its mix of features, not some specific one by itself.

Depends on the implementation.
Python doesn't verify the interface is met until run time.
With all the cases were Go pushes you to use interface{} that's the case in Go a lot of the time too.

If it had Generics that would be another thing.

With all the cases were Go pushes you to use interface{} that's the case in Go a lot of the time too.

However, you can wrap an interface{} using container (for example), with type-specific input and output functions, getting back compile-time type-checking. Yes, it is not optimal, and yes, you're still paying the price for type assertions.

If it had Generics that would be another thing.

I would like to see a good solution for this that integrates well with the rest of the language.

You can make an io.Reader interface in any language. Interfaces don't really shine until you start composing them.

Say you have a type that implements the io.Reader, io.Writer, io.Closer and io.Seeker interfaces, eg. an os.File. This type would also automatically implement io.ReadCloser, io.WriteCloser, io.ReadWriter and io.ReadWriteCloser, io.ReadSeeker, io.ReadWriteSeeker, io.ReadWriteSeekerCloser, io.WriteSeeker, io.SeekCloser etc.

Yeah, I simply think that this is hard to explain in a comment how useful and easy it is. But, I gave it my best Go...
Composition

I read your comment, but this seems more like A sane standard library with proper interfaces and not a language feature per se (also composition to me sounds like the pattern where you create objects which are composed of other objects but that's not what you mean, right)? What you display here can be done in pretty much any language suporting inheritance (or I'm missing something), so your point is those languages don't have io.Copy out of the box?

I think the point here is implicit inheritance. There are many interfaces in Go, but you don't need to specify which interface you are implementing. So you can easily implement io.Reader and io.Writer among many others, without extending your type declaration for lines on end.

This means that many standard types in Go implements either io.Reader and/or io.Writer. That's neat. The implicit interface implementation is definitely a language feature, and the libraries are making good use of it.

(That is not to say, you could not do the same in other languages, but you would have to specify the interface they implement rather than just adding a method with a specific layout.[0])

[0] I am not remembering the right term right now.

I suspect you will come to hate this feature. Just because an class happens to have a method called close() does not mean it works the same way as another class that also has a close method. An interface is much deeper than the prototypes of its methods, and pretending you can pattern match an API to whatever names a code author happened to pick is likely to lead to pain eventually ...
The very point of an interface is to decouple the caller from all the implementation specifically because it will work differently between implementation. If you expect two classes to implement it the same way, you an abstract base, not an interface.

Close is a particularly good example. One need only look at C#'s IDisposable to see that it does, in fact, work well. A mock might noop it, another class might close an FD, and yet another might make an RPC call.

I agree that interfaces tend to have fairly narrow family tree. And, by this narrowness, there's little ambiguity about what T GetById(id int) means. As the tree expands, which happens with implicit interfaces, ambiguity is more likely. Nevertheless, there's a fairly large common vocabulary that we'd all largely agree on. Closer, Reader, Writer, Logger, etc. Even in more complex ones, I see little risk of confusion, say, http.ResponseWriter. And, something that I've noticed from Go (which I never did in C# or Java), is the tendency to favor very small interfaces, which ends up being pretty awesome.

That aside, consider that implicit interfaces allow the consumer to define the interface. For example, you create a library that has a concrete struct called MyStruct with a method called DoStuff(). You define no interface because you don't need one.

I, a consumer of your library, need an interface because in some cases I'm using your MyStruct to DoStuff and in other cases, I'm using my own implementation. So I create an interface, define DoStuff(), and BAM!, your structure now implements my interface. I don't have to change your code.

Sure, the workaround is to wrap your structure in my own which implements the interface. But how, in this case, is the implicit interface not a huge win?

Maybe, as you say, it'll screw over people who use it poorly. For everyone else, I see no drawbacks.

It's not just "people who use it poorly". The point of an interface is to abstract over some details while guaranteeing others. If I am unaware of an interface, I don't know to avoid the names used in that interface, and I don't know to abide by the invariants assumed in that interface. That seems like it will bite people who've done nothing wrong. If I am providing a library, I can't possibly be aware of every interface anyone might define in code that uses it. I've no clue how frequently this will occur, in practice.

Haskell's typeclasses work around this by letting you define "how a type implements an interface" at either the definition of the interface or the definition of the type. (Or, strictly, anywhere else both are in scope - but that gets messy for a few reasons, so it's discouraged and GHC warns about "orphan instances" unless you tell it not to.)

If I am unaware of an interface, I don't know to avoid the names used in that interface, and I don't know to abide by the invariants assumed in that interface. That seems like it will bite people who've done nothing wrong. If I am providing a library, I can't possibly be aware of every interface anyone might define in code that uses it. I've no clue how frequently this will occur, in practice.

That's not a problem with Go's module system. If someone is using your library, and wants to use your interface in a particular package, that's fine. If they want to use another library, with another interface of the same name in another package, that's also fine.

If they want to use both libraries in the same client package, they'll have to locally rename one or both of the imports.

Structural types, as opposed to nominal types?
Basically. But Go obviously also have nominal types.
No, io.Copy was just the example that everyone would recognize in Go, and likely the first interface you will touch.

My point was implicit, exceptional simple interfaces are sorta magic. They are effortless, and because they are implicit, there is no harm in creating a 1 function interface (or 10 of them). Because your caller is never going to have to do an ... Implements ThingA, ThingB, ThingC, ThingD, ThingE, Thi... it implicitly supports an interface if it fulfills the signature.

So your interfaces end up being tiny (just what you need) and pervasive. This means the way your system ends up working tends to be far more aligned along interfaces than anything else.

When I speak of composition (in this case the composition of functions) I am speaking of the ability to use functions together rather freely and with little effort. This is a bit hard to explain, but it feels a bit like a modern shell, you can wire together lots of commands (functions), from lots of places that have no awareness of each other with the pipe | operator. In Go, simple, minimalistic interfaces act as the glue and let you compose lots of diverse functions together exceptionally quickly.

I NEED to work Go into one of my projects, but dammit I love Python so much. it is a warm and safe and comfortable cocoon. :)
Then you need to scale and your library isn't actually built in C... or only runs on 2.x (or 3.x) and the cocoon seals up and you can't escape... you scream but no one can hear you... you look for help, desperately clawing at cython, numpy, jython, pypy and C extensions -- they all require you to leave your cocoon far behind... you struggle and break free... suddenly you are exposed to the big wide world outside of your cocoon... you look back and realize the cocoon was just a cleverly disguised prison. /hackernewsstorytime
hahah well I am very comfortable with C and other languages, but I love python for it's versatility and ease of use! It has some very expressive one liners that are surprisingly coherent for being one liners.
Just keep in mind Go IS NOT like python, but instead it's a better C

I'd say it's a better C, not a better C++ (or Java/C#)

As a language maybe but for usage, not so much.

For me, most modern uses of C fall into 3 camps:

+ extreme portability

+ complete, low level resource control

+ foundational libraries

And Go doesn't really qualify for any of these. For the latter 2 largely because of the GC.

I think in practice, that its engineering bent i.e. no frills language and high quality tooling, will more likely see it being used in the server-side application/middleware space. I guess not surprisingly.

So it really is up against Java, Python, Ruby and C++ and is, therefore, interesting in that it isn't trying to compete on lingustic goodies.

"As a language maybe but for usage, not so much"

Yeah, I agree with your points. Which is a shame really.

I loved python and wouldn't've given any of it up. Then I switched to Scala and discovered how much I could get the computer to do for me, without giving up the conciseness, readability and expressiveness of Python.