As to the minimalism in Go, I wish it came with a few more default data containers.
Sorting a map by using a slice gets old after awhile. Why can't I pick a sorted tree based map (like std::map or std:set in C++) or an un-ordered hash based map like std::unordered_set, etc.? Why do the containers have to be so minimal too?
This just leads to people rolling their own and many times, that leads to poor performance and other issues which ultimately reflect poorly on the lanaguage. I've seen more string implementations in C than I can count. Most of them were bad. I hope Go doesn't end-up being like that.
I too wish there was more standard containers available in Go's standard library. However, I don't think there will be a collections package unless and until generics make it into the language. That said, there are some pretty good libraries out there:
Thank you for the links. I hope Go gets a good, solid container library that is reputable and widely used. And even better if that makes into Go proper some day.
>This just leads to people rolling their own [containers]
Nah, Go was careful to make that impossible. (You know exactly what I'm talking about, I'm not going to come out and say it. What one, specific thing makes rolling your own container in Go literally impossible?)
If code generation is cheating, then that's a shame, because apart from differences in the type of artifact created, that's basically what generics do.
Code generation is one possible implementation of generics. But generics isn't just about the code you run but also about making sure the source code is sound at compile-time, i.e. they also have implications for the compiler's type checks.
Surely if you use code generation to create an "IntFancyDataStructure" then, once generated, type safety is ensured? There's no type erasure, no interface{} etc., in fact from compiler's point of view it's as simple as it gets. Or am I missing something?
> How would you define a function that accepts/returns FancyDataStructure of any type or only of a type that has X embedded?
I'm not sure what you mean by this. If you mean that it accepts any type known at compile time, then you use the code generator (we're talking about a function template, after all). If you want this to work with any type known at runtime, then use Go's interface since you need the vtable.
> BTW type erasure isn't necessarily a bad thing when the compiler can prove the code is correct at compile time.
Sure, though I'm not sure what the performance implications -- and while I can think of some counter-examples were this wouldn't be the case, in most cases the impact would probably negative.
What exactly does "cheating" mean, and why doesn't it count if it solves a problem? With generics, the compiler will internally generate type-specific implementations for each instantation, so the end result is the same.
I'm not arguing against generics at all (I wish Go had it), but I'm also not sympathizing with this response.
You might as well claim C has garbage collection since there are libraries for it. It's a hack. Might as well claim no language benefits from generics since you can always code gen.
For practical issues you have things like compile time, extra tool dependencies and up front setup time to enable a given type.
It makes c++ templates look down right elegant in comparison.
Yes, people absolutely use code generators with C. But they do this because of deficiencies in the language, and everyone basically agrees that C is the lowest common denominator in terms of languages -- it's portable assembly essentially.
Go missing generics except for built-in containers; and then not having anything but the bare minimum for those built-ins is a critical defect of the language. The fact that you can work around that defect doesn't make it any less of a defect.
It's not cheating, but you haven't rolled your own container: because in the end you don't have one.
Let's make an analogy. Suppose a language didn't have tail recursion, but by using goto you could "roll your own." So we're on the same page, right? you get what I'm saying, and how rolling your own might look and work, right?
Now suppose I said that without goto, it is literally impossible to roll your own tail recursion. (ignore whether this is true it's just an analogy.) You are saying, no it's not: you can patch the generated executable, so as long as it is possible to patch executables using go (i.e. if it can read and write binary files), then you can roll your own tail recursion even without goto, through binary patching post compilation. It might just be tedious or special-cased to one specific file but you can do it (you're saying)
See the difference? It's not that patching an executable is "cheating." It's just that it's outside of the definition of rolling your own tail recursion.
Likewise it's not that source code generation is "cheating." It's just outside the definition of rolling your own container. it's simply not what rolling your own means. after you've done it, you don't have a container.
my analogy is a bit leaky but I think it gives insight, and I hope you see what I mean.
I don't understand your analogy at all, but I believe the principal difference is that a code-generated container is a final, closed thing — it is not, for example, composable at compile time, so it doesn't fit into a larger type ecosystem.
For example, you could write a Vec<T> and instantiate it for Vec<string>, which is fine, but then a different package may want to declare func sort<T>(values Vec<T>). Maybe one is an app and the other is a library. How do you now fit the two together? If the tool is smart enough and both of them are generated at the same time using the same tool and module files, it might work, but I don't think there's anything like this for Go right now, and either way, you have the "same tool" problem.
Exactly! Which is why currently in Go rolling a container is "impossible." (it requires templates, as you showed with your Vec amd sort examples....something missing from the language.). It's very clear.
Containers are the level 0 of generic data structures: not only is an "array" significantly less useful than an "array of T" it also leads to loss of safety (you may intend your array to be an array of int, but codegen aside there's nothing precluding a user from shoving a string in there) inefficiencies (since the type of values must be erased you need an indirection, usually a pointer) and usercode-complexity (the user needs to downcast all values to the data type they hope they are).
After working with it for a year, these are the things I don't like about Go:
1) No constructors. When I create a struct, without a constructor, I can't be 100% if certain fields are populated or not. With a constructor, I could force this. With a large number of people working on a code base, you can't assume anything, unfortunately, but without a constructor to force exact data in a struct, it makes it more tedious to be sure.
2) Error handling is ugly and tedious. Not being able to get the type of an error makes it a lot more inconvenient. At least in our codebase, errors are almost string parsing, it's ridiculous.
3) Nil map vs empty map is a very strange thing to keep in the code. Why even allow this distinction?
4) Named return parameters are really stupid. This is another thing that doesn't fit the philosophy of what I understand Go to be. It forces a bunch of verbose opinionated rules on us, but then allows named return parameters which causes confusion.
5) It's very hard to tell the difference between package names vs structs. Luckily Gogland allows you to change the color for package names so this helps me out tremendously, but there should be a better way to differentiate without relying on your IDE.
Overall it's okay but nowhere near perfect and has some really weird deficiencies. The company I work for uses Go with dependency injection which I hate and I think is very ugly but a paycheck is a paycheck.
1) The struct's fields will have the default value for their respective type. I understand you might want to force specific values, but if this is the case, maybe you could only export an interface to the struct? And said interface would have the "constructor" methods.
2) You can always encapsulate errors. Your code will still have a type assertion or switch, but you don't need to use string parsing (although I have seen string parsing used in libraries such as mgo - the MongoDB client library).
4) This is subjective I think, but I disagree. I like just being able to type "return" instead of for instance typing in 6 different places "return something, err".
5) Do you mean un-exported structs (and therefore lowercase)? I haven't come across this, but I can see where it could be an issue. I usually just use VSCode and mouse over to quickly see a name's definition.
I agree with you that it's not perfect, but as I saw someone else say on the internet, it's the least bad language (in my opinion).
And I don't envy you for working with dependency injection in Go :)
>Error handling is ugly and tedious. Not being able to get the type of an error makes it a lot more inconvenient.
See https://stackoverflow.com/a/7006853/5849285 to switch on type (error is a interface with a Error func[1]), so you can define different error types [2], or use `int` to define error type.
> 4) Named return parameters are really stupid. This is another thing that doesn't fit the philosophy of what I understand Go to be. It forces a bunch of verbose opinionated rules on us, but then allows named return parameters which causes confusion.
They exist because go really has exceptions. The only way to "catch" and modify variables in the "catch" before they are returned is to use named return values. You can't return anything from a panic/defer/recover directly. Error as value are just a convention.
> 2) Error handling is ugly and tedious. Not being able to get the type of an error makes it a lot more inconvenient. At least in our codebase, errors are almost string parsing, it's ridiculous.
You can make arbitrary types implement the error interface. Your custom error type can carry all the information you need. If err != nil you make a type assertion and access the error’s information. See [1].
> 1) No constructors. When I create a struct, without a constructor, I can't be 100% if certain fields are populated or not. With a constructor, I could force this. With a large number of people working on a code base, you can't assume anything, unfortunately, but without a constructor to force exact data in a struct, it makes it more tedious to be sure.
Is there something similar to opaque types in c? Like a "constructor" function that returns an interface and then methods that operate on the interface instead of the struct itself?
What I don't get about go is why certain trade offs were made, some work well for a systems language, like error codes, but then there are different trade offs that don't, like the GC. Similarly the GC makes sense for a high level apps language, but features like lacking generics don't.
So far I fail to understand the excitement with Go.
Error handling in go feels like going back to 1960s programming.
Garbage collection is already implemented with high performance in Java and other platforms, what's new? You can use GC with different strategies in C++ as well, if you like, the libs are there. There are many libs to get dynamically managed memory in C. But the author wants to program for embedded devices and my choice there would be to avoid dynamically managed memory for the most part.
Coroutines and tons of different concurrency and multiprogramming and parallelism paradigms and strategies are already available for most mainstream languages.
Syntax in Go looks like a poorly done copy of Algol-68 (1968, and -surprisingly- a more powerful language.) Why?
At least if they would have went with s-expression syntax, they would have enabled metaprogramming and this would have solved some of the problems with missing features.
As for use in embeddable devices, the author discards D and other languages for lack of portability to other platforms; but AFAIK embedded devices programming goes to very low level details and thus I can't think how a code that is tied to the specific hardware would be portable, no matter what the language is.
I thought Google had smarter language designers. At least Mozilla did a decent effort with Rust. Perhaps Google do have smarter engineers and they actually use Julia, Lisp, and Smalltalk for Google's internal projects...
> Garbage collection is already implemented with high performance in Java and other platforms, what's new?
A simple language that can be used for both 'scripting' projects and larger applications, natively, statically compiled, with a modern standard library aimed at what a lot of "ops" development is currently in need of. Hence the explosion of tools written in Go targeting that field. Maybe it's not suitable for your applications, but there is clearly a market for this.
> but AFAIK embedded devices programming goes to very low level details and thus I can't think how a code that is tied to the specific hardware would be portable, no matter what the language is.
That depends on the type of embedded stuff. I wrote a ton of embedded code targeting various embedded OS's on POS systems running m86k, MIPS, ARM and other stuff. I was responsible for designing and writing a generic hardware abstraction layer. It took considerable resources, but once streamlined, porting to another platform took about 3 months (if the hw and compiler/toolkit vendor were helpful, otherwise it could take years). It gave us the massive advantage of being able to develop/debug/showcase the application logic on a PC or on alternative hardware, and then finally target the real hardware when the HAL was finished and work out some kinks there, taking another month or so. So yes it's absolutely possible to write portable embedded code.
> I thought Google had smarter language designers. At least Mozilla did a decent effort with Rust.
Given the adoption rate of Go in the ops field, which incidentally is what Google needs a lot, I'd say they at least got something right.
> the author discards D and other languages for lack of portability to other platforms
...but it's very portable. It has llvm and gcc backends, supporting whatever platforms those support (all that you're likely to ever use), and it's possible to compile without runtime/gc, for a small enough footprint to run on embedded.
> I thought Google had smarter language designers.
Really? Knowing that it's the likes of Rob Pike or Ken Thompson?
If you had told me ten years ago that these guys are going to make a language for Google, I'd have accurately predicted that it will be exactly the same sort of insipid blub that it is.
They are not going to see very far outside of the C + UNIX box, and much of whatever they do see, they will have an allergic reaction against.
> I thought Google had smarter language designers.
They do, but you may be misunderstanding the purpose of Go for Google. It's to take recent untrusted grads and have them be quickly "productive" generating reviewable code.
It's not an accident that Go's feels a lot like a somewhat improved and simplected Java (1), very much tuned towards network services.
>At least if they would have went with s-expression syntax, they would have enabled metaprogramming and this would have solved some of the problems with missing features.
Interesting. Do you think having that syntax would help with developing generics for Go (a thing that I've read a lot of devs feel want to be there in it)?
"Go is a better* C" It seems like Go is propelled into popularity by people discovering the joy of C/POSIX programming for the first time.
Some of us remember, however, why we left C behind (it does not work beyond a certain degree of complexity). Why would I want to do Web programming in C? We spent years creating better tools for this.
The kernel is so large and successful despite C, not because of it.
A large number of the common kernel bugs, such as refcounting errors, buffer overruns, messy goto logic (resulting in use-after-frees sometimes) are because the kernel has had to build up poor abstractions due to their language not supporting better abstractions.
Go improves on C substantially, especially for high level programming. GC, interfaces, slices, maps, cross-platform types, expansive standard library, high performance cross platform concurrency, memory safety, runtime type information, etc, etc. So I wouldnt write web programs in C, but Go is one of the best options today, especially if you care about performance, maintainability, or operational complexity.
Doesn't all that already exist in Java? And have already existed for a while, and thus has a big ecosystem of tools? Why would someone use Go over Java? (Just your opinion, what are the differences?)
Lots of small differences mostly. Go gives you control of memory layout, it has better ergonomics (type inference), simpler tooling, no dependency on IDEs (you can if you want but most use vim or emacs), built in testing, better standard library (production ready HTTP server, JSON, templating, etc), low latency GC out of the box, simpler GC tuning, statically compiled binaries, fast startup times, simpler primitives (e.g., no exceptions or inheritance), implicit interfaces/structural subtyping etc. all while competing with Java for performance.
This is my list of things that Go does better than Java; I don't want to give the impression that Go is perfect--I have lots of criticisms for it (no generics, hacky workarounds for sum types, error handling is often too simple out of the box, no decent REPL, etc), but it lets me be more productive than any language I've used to date.
Thank you, I was looking for just that kind of perspective from someone who uses go and thinks they are getting something better. It's understandable of course that neither of languages are perfect.
Go programmer here (operations stuff mostly). Came from Ruby, but have done C, Objective C, Perl and others.
Simplicity of deployment is nice, but the main reason I don't use Java is because it's just painful to write. I had to write some Java just the other day to duplicate a bug a customer bug and it was tortuous.
I love Go, but there needs to be some work around dealing with the problems the community is solving via code generation.
I feel like a lot of the things I find tedious about Go should be much easier to generate somehow so I can get on with what I was trying to do. sqlboiler is an interesting version of something that Go by-it-self can't provide without code generation if you require not using reflection.
Otherwise, I like the language a lot. I mainly love it's simplicity to deploy, good cli, and low memory foot print. I also like that it's very easy to read, that the community is focused on "reading code", and that there are many good libraries out there now in addition to the standard library.
Started looking at Scala recently, and I'll probably play around with it, but it just seems Go is so much nicer to use for the kinds of projects I'm trying to develop.
I wish Go could have "just-in-time" code generation.
Right now, we have the magical //go:generate comment, but since it requires manual invocation of "go generate", artifacts can and do get out of sync with the Go code.
It would much nicer if Go code could contain code that caused generation to happen when the code was compiled. Adding the ability to execute Go at compile time would be one possible solution, similar to Rust's custom derive [1] support. As a use case, Rust has libraries that make extensive use of this system, such as serde [2], which can automatically generate efficient, type-safe serialization/deserialization code for structs (e.g. to/from JSON).
> Right now, we have the magical //go:generate comment, but since it requires manual invocation of "go generate", artifacts can and do get out of sync with the Go code.
It's pretty easy to enforce that as a build constraint in your CI system; we do that with our Go projects. If the output of `go generate` isn't checked into git, the build fails. (Same if the code hasn't been formatted with gofmt).
True, but you have to do it for every single project. We have tons of apps that all need to manage little rules like this, it gets pretty exhausting after a while.
And NOT having to maintain little rules like this is one of the reasons I fell in love with Go initially. Between gofmt and gofix, the go tooling just works(tm) so danm well.
Go seems to be a study in how to ignore decades of both acedemic research and practical experience. Did no one learn from Java that code generation is a red flag that something is wrong with your language?
I agree that dependency management and GOPATH are two annoyances that need to go away. GOPATH in particular is a constant pain when you work across multiple machines like I do, and feels like a mandated anachronism/catechism (your workspace must be like we want it to, and we don’t care about your own methods).
On the other hand, lack of proper vendoring, version pinning and getting random versions of libraries off GitHub makes a lot of people anxious about long-term reproducibility.
Like a peer of mine once said, “maven is forever. They don’t call things ‘artifacts’ for nothing, I can even do archeology in my dependencies”.
I actually like the GOPATH. It has forced me to order my code correctly, I now have a single ~/code directory where all my checkouts reside.
I created a small bash script (called giclo) which automatically places checkouts of git repo's in the correct ~/code/src/HOST/USER/REPO directory, super convenient and very well organized.
> On the cost of some repetition here and there, it makes programs look very explicit, which helps readers to understand the code better, instead of wondering: can the exception be thrown here or not?
I don't get this. In practice, most functions end up having an error result by convention, so the answer to "can the exception be thrown here" is always "maybe", which is the same as the answer would be to any function call in a language with exceptions...
Also, even without exceptions, it's possible to have explicit error handling without so much boilerplate. Rust's `?` operator achieves the same thing as Go without filling your code with conditionals and return statements.
I do appreciate the difficulty in balancing simplicity and verbosity, but there are a handful of cases in Go where I wish they added some complexity to make the code significantly more terse.
Well, Lattner did propose adoption some of Go's concurrency models in Swift. Then you already have optionals, a faster garbage collector, and a much deeper language.
If swift had a better concurrency story I would be all over it. The general vibe I get though (as an iOS super noob) is that concurrency patterns were already established back in the ObjC days and there’s little incentive for Apple to change it.
I think they mean that they know in advance whether to write error handling code or not, not whether or not an exception will actually be thrown at runtime. The answer to _that_ is always maybe, but the answer to the former depends on the function.
I think that misses the point -- explicit behavior is always more desirable than implicit behavior. I don't think most functions end up having an error result by convention -- many functions never return errors. Treating errors as values means the flow of control is linear and obvious to the reader. Operations happen in their expected order. Resource management becomes trivial. It also allows errors to naturally be more expressive.
> I don't think most functions end up having an error result by convention -- many functions never return errors.
Only the most trivial functions don't have the possibility of error. And if a function doesn't have an error condition today doesn't mean it won't be changed to have one tomorrow.
Java has ruined too many programmers on exceptions by making them checked and making you do a lot of unnecessary try/catch dancing. A program should only have a few key try/catch blocks in the places where recovery is possible and it should be only concerned with what it can recover from. This obsession with every error that any method can throw/return is entirely the wrong way to deal with exceptional conditions. Therefore, error handling in Go is as backwards as it comes.
Two of the fundamental principles of object-oriented programming are encapsulation and polymorphism; the idea that exceptional errors are part of the API contract is completely at odds with idea of isolating the implementation of different components.
A component that calculates exchange rates might use a file today and a network service tomorrow. My code should not have to care how every single component is implemented all the way down through the entire code base and every 3rd party library. And if my code is designed to restart high-level operations due to temporary network exceptions then it will be robust without knowing which components use the network now or in the future.
Properly typed `result` types and checked exceptions make implementation details explicit. I don't want that part of the API contract.
What matters is whether the component is _fallible_ or not. If a component can fail, its caller has to be able to deal with the failure, even if it deals with the failure by also failing (propagating the failure).
Your example already could fail (on a missing file today, on a network problem tomorrow), so a well-designed API contract (which doesn't unnecessarily expose the _cause_ of the failure) wouldn't have to change. The implementation detail is the cause of the failure, not the fact that it can fail.
All components can fail; even if just by programmer error.
There is a difference between expected failures and unexpected failures. I like to use the example of int.Parse() and int.TryParse() in C#. These functions do exactly the same thing but treat errors very differently. You use int.Parse() when you expect the parse to always succeed. It will raise an exception if the parse fails. int.TryParse() doesn't raise an exception, it returns a boolean and you use it when you expect the parse to fail.
Returning a boolean indicating success/failure can be part of the API contract with well defined semantics. But that can't help you with unexpected errors that are part of the implementation.
For unexpected failures, you want to know the cause of it and you want as much implementation detail as possible.
I can't recover from a missing file, but I can recover from temporary network problems. If the file is missing, I want to know what file is missing so I can fix that manually.
> If your function doesn't throw errors now, but does in the future, that's an API breakage.
So what you're saying is that, to avoid future API breakages, you should include an error result in every non-trivial function, and probably the trivial ones too in case a changing spec makes the behavior not so trivial anymore.
It's a bit silly to avoid API breakages by always returning a nil error.
If a function is changed to now return an error, it's API breakage. I would consider it a breakage if it previously return just nil or not since the previous "always nil" is documented or implicitly contracted as part of the API.
It's so strange seeing people who write these sorts of articles constantly praise Go's error handling. In what world is doing the whole if err != nil thing every 30-50 lines not ridiculous?
I get that goroutines are great, but that really seems to be all Go has going for it. The GC sucks, error handling feels like something from a bad language designed in the 70s, missing generics, math library is awful, Rob Pike being super opinionated, etc.
> doing the whole if err != nil thing every 30-50 lines not ridiculous?
It's more like every 5 lines. I love it as every error is handled correctly and in scope. No error is left behind. Most languages I've used devolve into the GLOBAL MASTER DEFAULT HANDLE ALL ERRORS which is just gross.
In practice, scoping becomes an issue because "err" sneaks in everywhere.
For example, consider:
foo, err := getFoo()
Now your scope has an "err" and you open yourself to logic errors such as:
foo, err := getFoo()
... do stuff ...
if errr := makeBug(); errr != nil {
return err
}
Spot the (heh) error? It's caused by ":=" allowing variables to be reassigned.
You can avoid this dangerous pollution, but it's super ugly:
var foo *Foo
if f, err := getFoo(); err == nil {
foo = f
} else {
return err
}
(You have to have the return in the second branch, otherwise "go vet" will complain. Sigh.)
I have lots of these, and they're not helped by the fact that Go has no type-level mechanism to guard against the misuse of multiple-return values. That is, in the above, either "foo" or "err" are usually set, and if "err" is set, it means "foo" isn't. But the type system doesn't prevent you from accidentally ignoring "err" and using "foo", or vice versus. This gets even worse if the func returns a (non-pointer) struct as its value.
The fact that Go allows shadowing (exacerbated by the convention of using "err" as every single error variable) and promotes the reuse of variables (e.g. the above ":=", or declaring "err" outside a loop) also means it's super easy to assign the wrong thing.
All of the above are my single biggest source of bugs during development. That, and how accidentally using nil/zero values, including the damnable nil map/slice cases.
"go vet" (whose shadowing check is disabled by default (!)) and the various linters only go some ways towards helping here. The errcheck linter (which you can enable through gometalinter) is absolutely essential.
You almost always defined your variables. None of this `foo, err := getFoo()` nonsense.
Most functions return an error so err is already defined:
func doIt(a, b) (result string, err error) { ... }
Then you are free to reuse err as needed.
func doIt(a, b int) (result string, err error) {
var x, y string
x, err = doA(a)
if err != nil {
return
}
y, err = doB(b)
if err != nil {
return
}
result = x + y
return
}
That solves nothing. The point was that you pollute the scope with error variables, leading to dangerous logic errors that can be hard to spot. Your snippet is no better than:
There are real problems, and not real problems. I'd classify this as a not real problem. How hard is it to name errors differently when it would be a real problem? A few keystrokes.. and BOOM! Problem solved.
I wonder what is the correlation between volume of complaining about minutia such as this compared to real life ability and level of accomplishment.
> I love it as every error is handled correctly and in scope.
Not in scope. Because you are forced to deal with the error right here right now, sometimes it's in scope and sometimes, you're simply not able to do anything with the error so you effectively mishandle it.
Exceptions allow you to handle the error where it actually makes the most sense.
There are options between these two extremes. Haskell's Except monad is a great example. Or javascript promises. You can launch a bunch of actions in sequence and provide a single error handler for them. Each step doesn't have to worry about it, but neither is the error handling invisible and completely global.
Error handling in Haskell is, in practice, terrible. Not only do you constantly have to wrap and unwrap potential error values to unify different error types, you also have to deal with a dozen variants on Either and related typeclasses, none of which are used consistently across the ecosystem. And as if that wasn't bad enough, there are also actual throw-catch-style exceptions, which are not checked in anyway and, combined with laziness, can explode in your face at absolutely any time whatsoever. It's a big big mess. I stopped writing Haskell maybe a year ago, but at least back then getting GHC to print a stack trace when an exception wasn't caught was a major feat that involved recompiling every single one of your hundreds of dependencies with profiling flags. Guess how well that went most of the time.
Anyway, Go-style error handling's lack of any of these issues is what makes it attractive to me: it's simple, straightforward and get's the job done. Granted, with a bit more typing and the occasional bug that could have been avoided by not having to type the (almost) same three lines once again, but not having to jump through any of the hoops I keep encountering in more sophisticated languages is so, so worth it to me. I feel like Go stays out of my way, lets me focus more on actually getting things done than on dealing with the language's intricacies.
> Not only do you constantly have to wrap and unwrap potential error values to unify different error types
This is a legitimate issue, although in Haskell you can use one error type like Go does if you want to.
> And as if that wasn't bad enough, there are also actual throw-catch-style exceptions, which are not checked in anyway
This is also the case in Go (panics and recover).
> I stopped writing Haskell maybe a year ago, but at least back then getting GHC to print a stack trace when an exception wasn't caught was a major feat that involved recompiling every single one of your hundreds of dependencies with profiling flags.
This is also the case in Go. Errors do not retain anything about their state. You need to use a package like https://github.com/go-errors/errors, which has the exact same issue you describe with dependencies.
>This is also the case in Go (panics and recover).
It is considered good style to not let a panic escape your library API unless something just went very severely wrong that the library could not possibly continue to function normally.
>This is also the case in Go. Errors do not retain anything about their state.
Errors are an interface, you can put any state inside it you want and I've frequently done so (retain line counts in parser or network data for example)
>> Not only do you constantly have to wrap and unwrap potential error values to
>> unify different error types
>
> This is a legitimate issue, although in Haskell you can use one error type
> like Go does if you want to.
SomeException is pretty much equivalent to Go's error, but it's not commonly
returned by libraries and such so using it barely helps with the need to
convert often. In addition all the types used by libraries as error values now
need to have Exception implemented, which they don't. So, in practice, no, you
cannot. Or rather, it wouldn't solve the core problem and create a new one.
>> And as if that wasn't bad enough, there are also actual throw/catch-style
>> exceptions, which are not checked in anyway
>
> This is also the case in Go (panics and recover).
True, panic/recover exist, but in practice I find them to be more akin to
setjmp/longjmp in C than throw/catch-style exceptions in other languages. Off
the top of my head I can't recall a case where I had to recover from a panic
that I didn't create in my own code (i.e. originated in a library call).
Exceptions in Haskell and other languages on the other hand are usually all
over the place (ok, usually not in pure code in Haskell) and I feel like I have
to constantly keep them in mind to avoid letting one slip through the seams and
then later having to track down where the hell that one came from.
>> I stopped writing Haskell maybe a year ago, but at least back then getting
>> GHC to print a stack trace when an exception wasn't caught was a major feat
>> that involved recompiling every single one of your hundreds of dependencies
>> with profiling flags.
>
> This is also the case in Go. Errors do not retain anything about their state.
> You need to use a package like https://github.com/go-errors/errors, which has
> the exact same issue you describe with dependencies.
That's fair and indeed it would often be handy to know where an error came
from. But since errors are just return values I don't really expect them to
have a stack trace attached to them, just like I don't expect any other value
to have that. It would still be neat for debugging sometimes, more so for
errors than other values. I think there are some proposals for improving on
this in Go 2.
What really frustrated me about Haskell though was debugging throw/catch-style
exceptions without a stack trace, because I basically had zero clues about
where it came from and there were no clear paths to trace in the code. In Go on
the other hand, errors don't just plop up from out of nowhere and panics, which
can, print a nice stack trace when not recovered from.
Haskell is an evolving language and the error story isn't entirely resolved yet. Personally, I like the MonadThrow stategy which gives the flexibility to the caller but the IO story gets in the way.
So yes, it can be messy when using other people's code because this is very much an unstable part of the language right now. I still prefer that over the worst possible strategy (what Go picked) being effectively baked in.
Yep. For my money, the war is over, and the monad won. I shouldn't say the m-word, as it turns people off. But that's the unifying pattern.
Javascript messed it up a little bit with promises, because it's so easy to swallow errors. IMO, they should have made it so that unhandled promise rejections throw by default at the next turn of the event loop, with a method that could be called to opt out individual promises.
I don't think so. You can choose to ignore the error (I know it's bad practice), but it's allowed. And sometimes you don't know what to do with the error
Handling errors as values is great, but making that value a product type rather than a sum type is not good design. It means you have to return a result even when you have an error, and so nothing enforces that the user actually handles the error properly.
No. That is incorrect. Errors are to be handled by the programmer and every book I read says "We are ignoring errors here for the sake of simplicity, you are strongly advised not to ignore them".
Error handling is what happen after you discover there was an error.
In Go, "checking" for errors may be skipped (and people make mistake, even True Programmers), in which case they are handled just as if they were ignored.
Even if the error can be ignored for fmt.Println, it is not clear from the example whether it was even taken into account or not. The following could be required:
_ := fmt.Println("hello world")
That means that the function call can return an error, and thus you have to use the return value, but the programmer decided to ignore it. Contrast it with:
fmt.Println("hello world")
Did the programmer know there was possibly an error? What happens if there is an error (not specifically for this example, but in general), and you don't check for it? then it is ignored.
In another language, the same line could be the source of an error; but only error handling depends on the programmer, not error checking. Moreover, the default handling strategy is to abort execution. Actually ignoring errors requires more work.
Most linters that concern themselves with errors will complain about this and I would not accept any code that does this beyond tests.
Go allows you to do certain things but it's not a fallacy because it's something that's allowed but not necessarily tolerated by what most consider "good Go coding standards"
My consulting experience has shown that beyond startups, or a few Fortune 500 companies like Google and FB, linters and code reviews are seldom done, unless we are speaking about safety critical domains like aeronautics, autos, train control systems,medical devices....
So because a lot of people don't use best practises that means the language in which these are to be used is worse off?
It strikes me a bit like saying "because most people don't wear seatbelts, cars are unsafe because you may get ejected out of the cockpit in a crash". Wear a seatbelt and use linters, I'm not going to excuse the malpractices of the larger industry.
Optional external tools, are exactly that, optional.
Lint was created for C in 1979, a minority used it or its commercial versions like Gimpel, until clang was introduced with static analysis built into the compiler.
Anything that isn't enforced by the language, tends to be ignored, hence why we need good defaults that make workarounds a big pain.
More safe defaults also means I have to think more about escape hatches. Sometimes ignoring an error is the intended way of operation, although rare. Same goes for some other parts of the go coding standards. Sometimes you need to violate them and having to first use some escape hatch like Rust's unsafe is annoying.
Not really. It has changed my coding style so I now only add variables when needed, even in languages like JS. I only get the "unused variable" error about once a month.
I really think error handling could be solved by adding some simple syntactic sugar.
For example: "if err" being compiled directly to "if err != nil" would be a quick win.
Second more complex idea: add a new keyword like "res := throw MyFunction()", being allowed if "MyFunction()" returns an "error" as its last argument and the calling function returns an "error" as its last argument, simply return the zero value or named return value of any other return values + the error if there is one, otherwise continue.
Point being, the core of Go is really good. When I come across a command line tool written in Go, my first thought is "fuck yeah, this is going to be so easy to install", versus something like Python which is a bitch unless the developers have taken the time to brew it or something. Countless planet-scale infrastructure tools have been written in it; Kubernetes and Docker to start. I can name a dozen startups, both large and pre-seed, which use it with resounding success.
Yes, it has warts. Every language has warts. Go is just "different" because it tries to be simple. Nearly no other language in the world has "simplicity" as one of its core values, and a lot of people don't fully respect what that means.
None of that helps with the real problem of golang errors.
The real problem with go's errors is complex and significant.
There's no Either/Result type so the multiple return doesn't actually tell you that you get a zero value when there's an error (and in fact there are functions in the stdlib that do this).
There's no good way to check whether an error is a given thing because error chaining isn't built in and errors rarely are typed as sum-type-enums as they are in rust or other decent functional languages. For example, `os.IsNotExist(err)` will not work for a caller if I did `return fmt.Errorf("error opening file: %v", err)` in a function.
The stdlib encourage a dozen really bad methods of doing error handling; there's `io.EOF` (which is a mutable variable btw). There's PathError. There's os.IsNotExist. There's random panics in some functions. There's nil returns with no further info.
The stdlib has multiple examples of 3 or 4 different error handling styles, and they're all horrific because they don't chain and don't allow for exhaustive matching.
Go may be good compared to C or other languages that were designed way back when. In modern times, go is a massive step backwards with its incredibly underwhelming type system, absolutely asinine community, lack of good abstractions, and so on.
Nothing ridiculous about it. I work on a Java code base where there is try catch every 4-5 lines. The application is working fine and nobody is unduly worried about fine taste in exception handling. If application were in Go it would have err != nil every few line that would also be fine for us.
With all the things that you mention Go does not offer helps it keep simple. It has already proven much useful for many of of my projects where a 'better' language would fail for variety of reasons.
> I work on a Java code base where there is try catch every 4-5 lines.
There is a huge difference.
The Java code you are reading is caused by bad programmers who are not using exception handling correctly. Proper exception use means sometimes catching and sometimes ignoring the exception and declaring it in your `throws` clause.
In Go, testing against `nil` every twenty lines is not only common, it's the ONLY way to write robust code. You simply don't have a choice.
Considering so many projects in last 10 years where code was not even formatted even though it was one command away in IDE, ridiculous variable names, useless comments like 'method getA() returns A" I am not sure it is result of bad programmers or just natural state of IT industry. Java succeeds despite lot of bad code mean immense hard work on performance/ libs side by JDK teams.
I think same is going to be true for Go. I have written few projects in Go and they have produced much more value than time taken to write them.
At the end of day companies I worked for prefer mediocre code that is written and used vs excellent code in brilliant programing language that was never completed.
Interestingly it makes me a better programmer after being used to the Go style. Now I don't allow unused headers/variables in my C/C++ code and feel natural to check every return error. 'Optional' warnings don't happen anymore either since -Werror feels familiar. Overall my C/C++ code becomes more reliable and easier to debug.
No, it is not ridiculous, it is meticulous. That's one style and approach in programming and not without merits.
I personally like exceptions, as they enable focusing into what's supposed to happen instead of what all could go wrong. And the abstraction power that comes with this approach.
But I wouldn't rush to say I'm right and the meticulous party is wrong. There are many instruments in the orchestra. In the end all that matters is the skill of the player.
I don't like exceptions. Go's error handling is still ridiculous.
It doesn't have sum-types, it has the incredibly stupid interface/struct nil punning so you must return an interface and type-assert down, it doesn't have a tuple object being returned but actually multiple return which is a semi-broken thing, it doesn't have an either/result type or an optional type to express mutual-exclusivity / optionality....
Yeah, the whole thing is disgustingly bad. That doesn't mean I prefer exceptions.
I do prefer meticulous "errors as value" error handling. Go's implementation is the worst implementation of that because the type system doesn't support various features that make handling error values better.
Yes, it's amazing that a language made when Go was is basically a version 1.5 of languages made 40 years ago. C had the worst possible error handling and Go has swooped in to take the second spot.
The very best error handling I know of is in CLOS. Instead of exceptions, they have "conditions". A condition is signaled like an exception but with a big difference described below. The thing about errors is, the code that detects the error can never know how to deal with that error. If it did, then it wouldn't be an error it would be a case within that function. However, that code may know ways the error could be dealt with, it's just not in a position to choose. So what conditions do is, you can define a condition and so called "restarts" along with that condition. What the restart does is say "here is a way I could handle this error" and code higher up the stack has the option to (1) use the restart, (2) define additional restarts on that condition or let the condition propogate upwards. The condition signalling does not necassarily unwind the stack: if you choose a restart, the code continues in the stack that restart was defined.
So this means every part in the stack that knows how a condition could be recovered from can provide the option but it is left to code higher in the stack to chose the actual strategy. This is superior to the exception strategy of unwinding the stack until you find a handler because sometimes you really want the code to continue execution where it is.
But, of course, such a system is really expensive to implement. If you can't have that my preference is the Haskell style of using a sum type where errors can happen but writing as much of your code as possible with any error conditions prohibited by the type system.
Well, that's really bad, because I think that things should be implemented in the best way possible. "Nobody used them", yeah, probably because no other language (apart from CL) had it, so nobody knew how to leverage them. But leave it in place and at some point in future time, some blogger would write a nice article on using them in Rust and developers would catch up in this cool feature.
I develop in Common Lisp and conditions&restarts are so much superior to the other error handling paradigms i've seen, that it's not fun being left without them.
I think the point is that sometimes showing documentation that something exists isn't enough for everyone to "get it". Sometimes you need some kind of technology evangelist to show something really cool they did with a feature for the creative process to begin on a larger scale.
Out of curiousity, if this was just a macro and no one was using it, what would have been the harm in just leaving them in? Is this a part of the language that changes so much it made maintainance a pain?
Macros in Rust are global, insta-stable, and have no namespacing. This means any macro we put in std means you cannot define a macro in any Rust program with that same name. This is one of the reasons we've been working on a new macro system, to fix these kinds of issues.
"Just leave it in" isn't a good justification for a feature. We'd then require that every implementation of Rust ever, forever, implements this too. Being in std is a high bar.
Perl6 & Smalltalk also have them (conditions&restarts).
And i've seen modules to add condition systems to perl5, Ruby & Clojure. But these are kinda of hacks so the more languages that have it baked-in the better!
Compared to Hotspot, it doesn't STW; it makes a different throughput/latency tradeoff that is more suited for its application domain; and it's actually needed a lot less often because the compiler can put many objects on the stack.
A generational collector is currently under development (but won't be in 1.10).
Stop the world typically happens during compaction phases. Golang "solves" this by just not doing any compaction. That's certainly a big tradeoff.
> it makes a different throughput/latency tradeoff that is more suited for its application domain
I don't really agree. While low latency is nice for consistency, low throughput means you may have to buy more servers to handle your load. That isn't always a win.
> and it's actually needed a lot less often because the compiler can put many objects on the stack
HotSpot can, and does, do this too!
> A generational collector is currently under development (but won't be in 1.10).
This is the original proposal, although I believe it has evolved significantly during development (in testing some of the proposed ideas didn't pan out as expected).
As a Java aficionado, i have a lot of respect for Go's GC. It may not be as sophisticated as the JVM options, but it has a laserlike focus on low pause time, and does a phenomenal job of delivering it, given how much less development effort has gone into it. It's really lit a fire under the JVM developers, at Oracle and Red Hat, to improve the realtime collectors in the JVM.
Not IBM or Azul, though, their JVMs already have kickass realtime collectors.
Hotspot has several GC algorithms available, FYI. And the term "stop the world" is misleading, it doesn't fully capture the nuance of garbage collection. Any GC algorithm that does compaction will require some sort of blocking. You can do compaction incrementally, so you only lock small sections of the heap at a time, but that would still be considered "stop the world" despite being orders of magnitude faster than the naive case.
Go GC gets to make the claim that it doesn't "stop the world" because it doesn't do heap compaction. Which will become a major problem when dealing with large in-memory data structures or certain usage patterns.
And the fact that they still don't have a generational collector is concerning.
Erlang has a really nice solution here by using a separate heap per Erland "process", and because they're owned by that process, can do a copying/compacting GC without having to take any kind of locking. Ruby's Guilds could adopt a similar idea and avoid STW GC there too.
Again, the term "stop the world" is misleading, there are a variety of compaction strategies that greatly minimize locking. There are even realtime GC implementations for the JVM.
Erlang's solution is interesting, but remember in Erlang processes can't share memory, they only have message passing. So you never have to worry about multiple threads writing to the same data structure at once, which is what makes compaction hard.
In a memory constrained environment, we recently noticed the throughput/latency tradeoff being made by the GC, but having to say that this behavior was not obvious in the first place and we had to dive into the source code to understand things better (which is a good benefit of go being open source, as already stated in the article).
The bottom line is that the scavenger will allow the OS to claim free memory only at least 5 minutes after it was GC'ed. For us the solution was to "force" releasing the free memory to the OS [1], and even since then things have been much smoother in the context of our particular app.
On this topic, there's an open issue [2] to look at making the scavenger more prompt and aggressive.
I like https://godoc.org/github.com/jessevdk/go-flags, since it also supports environment variables. It's nice to be able to get configurable defaults from the environment and still be able to override them on the command line.
I maintain an application that used a similar custom flag package. I am in the middle of porting it back to the stdlib one, (A) for consistency with the wider ecosystem and (B) to avoid reflecting on struct tags.
Personally, I don't like Go. I wish my projects needed Rust's speed, or its safety, or its engineering powers.
But I don't. For simple parsing or number crunching, Go is just right.[1]
I find the fast prototyping, the wonderful IDE environment (VSCode, vim), and the community to be _so good_ that I don't need anything else.
There was a time I thought of learning Python[2] or Rust, but found both lacking.
[1]. Sure, I wish I was smart enough to get a job working on Quantum, working as a kernel dev, or in fintech where every nanosecond counts. But I'm not. The Rust community is great, there's a lot of help out there, and it seems like an amazing and cool language.
[2]. I tried it out a bit. My main issue is that it doesn't (didn't) have nearly as good of an IDE community (PyCharm is practically the only good IDE for Python, and in the bit of time I spent with it, I found it much less powerful than their Java IDE) revolving around it.
I went from using primarily C++ to using primarily Python and now to doing some development in Rust. Rust is suitable for systems programming. Like C++, it's also suitable for application programming, which is what most development is. One need not be writing kernels to justify using Rust.
At my job, for example, I'm slowly introducing bits of Rust here and there to do some data engineering. I wouldn't use it for high-performance numerical computations, but ergonomically it's almost as easy as Python (for me) and the safety, type, and other guarantees it provides make for a more robust pipeline.
Have a look at Nim(https://nim-lang.org/ ). It doesn't have great IDE support, but VSCode plugin works well enough and the language has a lot going for it.
Edit: posting this because I'm in the same situation. I like Rust/Nim/Elixir but Go has a great productive ecosystem even though the language is quite "barebones".
This is one of the more measured cases for golang I have read.
When it comes to my default set of tools, I gravitate towards dynamic, expressive languages that provide greater leverage rather than ones with fast startup times, c-like speed, native binaries, etc.
That said, when you need those things, I agree with this guy--golang has been a far better experience for me than any other language of its ilk. I'm not claiming it has magic powers like rust or a sentient type system like haskell, but I spent hours learning go fundamentals, and days figuring out how to get real work done with it. I can't say that for many other languages with go's compile and runtime capabilities.
Google reacted to the criticism saying there are many products named Go. Well... that's a 2-way street. If there are many products called Go then why picking that name in particular?
I actually like this feature. If I truly don't need a field, I leave it out. If I do need it, I have a clear definition of what it is.
This feels so much easier to work with than untyped scripting languages where I basically get a hash bag and read values out ad-hoc, checking what they are as I go.
As a C-C#-C++-Java-ES2015+ guy, I found it hard to read and write in Go. :( Same problem goes for Python. (Not Ruby and CoffeeScript tho, I don't know why)
You don't need hugs, you need the realization that the knowledge in the languages you mentioned will continue to stay relevant and continue to make you employable in the next ten years.
As I read this it made me wonder why I was still using Javascript to write my server code. Thing I always hated about it was the lack of types, and practitioners bleating to "write unit tests" instead. So much less efficient and effective. Even Typescript doesn't seem like much of a fix.
Go sounds like Swift, strongly type compiled languages. Go has better concurrency features and likely compiles faster (I mean anything has to compile faster than Swift).
Swift still seems to have better, faster garbage collection, better language features such as generics, and optionals make error handling and writing safe code much easier.
But Swift doesn't have the community and infrastructure around server development that Go does. I think I'm giving Go a go next project.
It’s funny because I use JavaScript every day on the front end and often have to write backend in Go.
I honestly don’t think Go does enough to it to make it worth using over something like JavaScript. You still have to write unit tests and integration tests. It’s strongly typed, sure, but it’s type system isn’t robust and doesnt really help you that much, and it just feels.. outdated. Every other line of code is “if err != nil”. Nil pointer exceptions are.. still a thing. Yuck.
Typescript has a far more expressive type system than Go.
I've used JavaScript/TypeScript and Swift extensively, and I was very surprised by how much I disliked Go when I tried it.
The usual complaints: error handling and lack of generics. But both TypeScript and Swift do a better job in those areas. If Swift had a better story for async then I'd want to write everything in it. As you say, the server-side community just isn't there yet, so it's Typescript for me.
Of course having type system doesn't free you from the need to write tests, and yet, you need to have less test cases, and there are much less possibilities of allowing silly errors like typos etc.
My point is Golang doesn't have a very safe type system.
It's better than plain JS, but if valuearb really wants to experience type saftey, they should look somewhere other than Golang. Swift, which they also brought up, would be a much better choice.
I'm afraid to try Swift on the server. On one hand I love it and am very productive in it. On the other hand, if I build anything commercial out of it have I coded myself into a trap? How easy is it to use Mongo, or some real database? How much open source code is out there to help build web servers with restful APIs?
It matters if there's no way to have a flag without a default. If your flag doesn't have a clear "user didn't supply anything" default it can be annoying
It's always possible to define a custom flag type to handle this. It's definitely what I would consider niche however, since usually you don't care about set/unset just whatever the value is, default or not.
I just ran into this for the first time in many years of Go usage. I wrote a command line utility for some code gen. If a flag was left off, I needed to prompt the user for an explicit value. For Boolean flags, this was not doable. I had to create a type that matched the flagset interface and use flag.Var for parsing it.
Personally, I've written a lot of stuff in Go, it might not be a perfect language but it's certainly the most fun one. I can sit down, write 1k LOC and have implemented something that I can look at in 10 years and still know what I'm doing.
It has it's downsides but I love it despite those, errors could be better but it's mostly fine and I'd love some generics so I could optimize my storage structures (though by using a backing array and referencing indices helps a lot)
As to the minimalism in Go, I wish it came with a few more default data containers.
Sorting a map by using a slice gets old after awhile. Why can't I pick a sorted tree based map (like std::map or std:set in C++) or an un-ordered hash based map like std::unordered_set, etc.? Why do the containers have to be so minimal too?
This just leads to people rolling their own and many times, that leads to poor performance and other issues which ultimately reflect poorly on the lanaguage. I've seen more string implementations in C than I can count. Most of them were bad. I hope Go doesn't end-up being like that.