Go is not an easy language
arp242.net
arp242.net
I once had a conversation with a founder about their signup flow. This founder had aspirations to be a global player. They said that step 2 of their flow included entering a credit card number. I had to stop them. "What about people who don't use credit cards?" They were nonplussed. "You know, China, much of Africa ..." they had never actually thought about whether people had credit cards because every single person they interacted with had one.
Back to languages. If you've never taught an introduction to computer programming for a general audience you are blind to what does and does not matter about a language. You look at things like `foo.bar()` and think "Yeah that's a simple method invocation" and have no idea how many people you just lost.
Never underestimate how much ease of use matters. We, as a community, have selected for decades against people who care about language ergonomics because they get hit in the face with a wall of punctuation filled text and turn away. We fight over where braces belong while not realizing how many hundreds of thousands of people we've excluded.
Ease of use is the most important part of a language. It's just that the barriers to entry are invisible and so the people who happen to be well suited to vim vs emacs debates get to talk while the vast majority of potential programmers are left on the outside.
We create barriers to entry accidentally because we design for people like us. To us the barriers are invisible.
Each level requires notation that is concise (so it can fit in short-term memory to be actually useful) - one cannot expect graduate-level mathematics to share the same notation/symbols as beginner (elementary) maths.
Programming languages have to trade off which level they are biased towards, I do not believe in a universal language that is both easy for beginners while being concise for advanced users.
Back on topic - Go is not an easy language, it is a simple language, those 2 things are not the same.
There are some kinds of complexity you can't eliminate; only contain through careful, thoughtful architecture. Unfortunately, people often confuse this with the eliminatable kind, and end up making a bigger and bigger mess as they push things around.
This distinction is fundamental to any sort of design, but is unfortunately lost on a lot of people (especially developers, in my experience). Easy and simple are near-universally conflated.
> I do not believe in a universal language that is both easy for beginners while being concise for advanced users.
Exactly. I really want some languages to be more intuitive and easy to pick up for non-programmers. The average person (whatever that means) does not have the first clue about the machines that run so many parts of their lives.
At the same time, such a language would likely be a bad fit for most professional software development. That hardly means it's without value.
I know languages like that exist, but they're often aimed at absolute beginners and are treated like toys. There doesn't seem to be much middle ground or "transition languages".
Insert joke about how $LANGUAGE_YOU_DISLIKE is a toy.
That’s an interesting statement as it doesn’t always work in other languages. In German (my native language) my first instinct was to translate both words as “einfach” which contains both concepts. In fact, in my online dictionary of choice the word “einfach” is the first entry for both “easy” and “simple”. So if Germans conflate these two it might be because of the language they speak :) But more to the point, I’m wondering how universal the distinction between easy and simple is when other languages cannot express that distinction as easily as in English.
Sadly, I would argue that this is also true of many developers.
I often see this point here, but I always wonder what people mean by it. Could you elaborate on that point?
"How do you remove an item from an array in Ruby? list.delete_at(i)...Pretty easy, yeah?"
"In Go it’s … less easy; to remove the index i you need to do:"
list = append(list[:i], list[i+1:]...)
So Go is simple in that it doesn't have shortcut functions for things. There's generally one way to do things, which is simple. But it's not easy, because it's certainly not intuitive that "append" is the way to remove an array element.Simple means uncomplicated, not a lot of pieces. A violin is arguably simpler than a guitar because of a lack of frets.
Easy means it is not difficult. A guitar is arguably easier than violin [0] because it has frets.
It's important to remember that "easy" is very subjective. What is easy for one person might be insurmountable to another.
tl;dr "simple" usually means "easy to understand" and "easy" usually means "easy to do". Both inherently assume some amount of prior knowledge or skill, so neither is entirely universal or objective.
[0]: I'm not trying to say one instrument is better or disparage and guitarists. It's an example.
(I am not entirely sure I agree with its thesis or its applicability to Go, but since nobody had actually linked you directly to the concept, I thought it would be worthwhile to do so.)
Ease is measured by how a language may be used to solve a problem. As an example, text-wrangling with Perl is easy - but you may have to do it using complex (i.e. not simple) representation.
Back to Go - channels are a simple concept, but are not (always) easy to work with if you do not put a lot of thought into your concurrency model.
Edit: I just thought of another way to express the difference between "simple" and "easy". The notation of adding "1+1=2" is simple, however proving that "1+1=2" is not easy (at least at the level of elementary students).
> "You know, China, much of Africa ..."
Oh, you don't even have to go that far. Of all the people I know maybe 1 out of 10 has a credit card. The irony in regard to your post: Most of them got one because "it was needed for something online" at some point. This is in Germany.
https://www.statista.com/statistics/865943/credit-card-owner...
You may need to copy & paste the title of that page into Google and access the page from there to see the data.
Go's simplicity forces developers to introduce insane amounts of complexity.
The main example in my mind is how powerful Cobra/Viper is to create CLI apps where config can come from multiple sources - files, flags, env vars. To do the same in Rust you need to write a lot of your own code and glue together several libraries.
There's also nothing I can find for Rust that can do database migrations as nicely as Goose for go. Diesel can create its own migrations, but that's only if you're using Diesel and I prefer SQLX over an ORM and Diesel isn't async yet
Go is definitely more verbose and less "fun" to write, but it's 10X easier to reason what's going on in a large application.
Of course, it's also the correct kind of solution for some types of problems. If you need a compiled language (many do), the competition to Go isn't Ruby and Python, it's C++ and Rust.
I keenly remember in college when they were first starting to teach me C++ and I asked something like “Ok: I hear what you’re saying that this is a function, and that’s a parameter, but my question is how does the computer know that you’ve named it [whatever the variable name was]?
The teacher had no understanding that this was a conceptual barrier.
Of course now I know that the answer is “because order of the syntax tells it so“, but stuff like that made those classes much harder than they needed to be.
It's really a matter of who your target audience is and what they're trying to achieve, though, isn't it? You might make a language easy for the whole world to use, but simultaneously make it hard for specific tasks. Likewise, a language might be easy to use for experts who are trying to achieve a specific task, but difficult for newbies. That's totally okay.
BASIC was super easy to understand and helped me get into programming, but there's no way I would use it for anything serious today.
I just want to say that I really, really like this phrasing.
I have a lot of opinions on how programming languages could be improved, and however much I disagree with Rob Pike on types, I still think Go hit a real sweet spot.
Another thing I find very interesting is Go is very popular in China
https://blog.jetbrains.com/go/2021/02/03/the-state-of-go/
the US ranked #7, it barely had more devs than say, Belarus.
list.delete(value)
without realizing it involves a linear search is considered one of the benefits of Go.That said, I agree Go could use a bit for ergonomic and handy methods, while still remaining efficient.
The example on Concurrency also has a different answer. Go gives you all the tools, but there are many different ways their problem can be solved. By explicitly avoiding a "join" method and having any default 'channels', Go makes all these possible. What is missing is some of the popular patterns that emerged in the last few years need to make it to into stdlib or some cookbooks.
1. If you want a worker model, I would start n=3 go-routines and they all receive from a single channel. They don't need to much around with a channel of buffer 3, as in the example.
2. If the workers already return data from a compute, the read from driver serves as the wait.
3. In other cases, there is sync.Waitgroup available to synchronize completion status.
4. End of work from the driver can be indicated via a channel close. Closed channels can still be read from until they are empty and the reader can even detect end-of-channel.
Designing concurrent system is a bit complicated. Some tutorials do make it sound like a `go` keyword is all you need. All of these can fixed by improving std-lib or cookbooks.
I think this is mostly okay. Most experienced programmers will realize this is a linear search and that it may be slow on large arrays. And turns out that in the overwhelming majority of the cases that's just fine!
For other cases it's not-so-fine, but the mere presence of "list.delete" doesn't really stand in the way of implementing another, more efficient, solution.
Overall, I certainly think it's better than implementing the same linear searches all the time yourself!
My main problem with having "remove items by value" in standard library apis is that it assumes a notion of value equality. Having that notion inserted at the very "bottom" (Like Equals/GetHashCode in .NET for example) is a mistake that has caused an infinite amount of tears.
I much prefer this situation where the user must provide his own implementation and think about equality. It's boilerplate, but the boilerplate is useful here.
But that doesn’t scale beyond the first user. Every subsequent developer now needs to read implementations to understand what the code does thanks to a lack of standardization for these functions. Consider if there was a smaller standard library with fewer interfaces and conventions, it would become a lot harder to understand a number of concepts in Go, by design. That’s fine, but conventions are what made Ruby on Rails projects so successful that they scaled up to being the monolith monstrosities most of us with startup experience ended up knowing them to be.
Note that I’m suggesting something akin to C++’s standard library where algorithms are already written for you to use and compose with. Yes, the drawbacks are a slower compile time, and some conventions like constexpr can really complicate things, but… I can’t say that a larger standard library or a larger set of conventions would make Go harder to use assuming the implementations hide a sufficient amount of complexity to outweigh the overhead of learning of them in the first place.
What functions provide more value than the overhead required to learn them? Delete is one such function, mutable immutable data structures generally are likely another. Yes, the documentation needs to specify big O complexity, but it can still be easy to read. For example: https://immutable-js.github.io/immutable-js/docs/#/List
The only way to get easier than that would be to use the same syntax for immutable operations as mutable ones: https://immerjs.github.io/immer/docs/introduction
I recognize that the Go community finds the built-in standard library restrictive now, but that’s no reason not to support a versioned standard library that ships separately but contains common functionality. I can only point to TypeScript for how such a system might work, given the large community project that exists to provide and publish types for that language, without actually publishing them with the language or compiler itself, excluding browser dom etc.
There is no standardization. It’s a hidden piece of logic that developers slowly and painfully understand.
If I you have to pass an equality function when removing an item from a list by value (or equivalently when creating a set or dictionary) it would always be explicit. That doesn’t mean it can’t be standardized. A framework can provide typical implementations (and often does!) such as “ReferenceEquals” or “StringComparison.Ordinal” etc.
Another unfortunate side effect of the “equality as a property of the type” is that you can only have one such equality. And if you are passed a Set of dogs you still can’t know for sure whether the equality used is actually the one declared in the Dog type of at Set creation (or possibly in the Animal base class). It’s a mess. And it’s so easy to avoid - the default virtual equality is simply not necessary.
Go in particular is worse on this front than any language more high-level than C. It defines equality for a very limited subset of built-in types, with no way to extend that notion of equality to anything that is not covered; nor any way to override the default equality assumptions. This makes it extremely painful whenever you want to do something even slightly advanced, such as creating a map with a struct as key when that struct has a pointer field .
And since pointers have lots of overloaded uses in Go, this turns a potentially small optimization (remember this field by pointer to avoid creating a copy) to a mammoth rewrite (we need to touch all code which was storing these as map keys).
If you ever need to delete based on value, it's a smell that a list is the wrong data structure. I'm sure there are cases in constrained environments where a linear search is heuristically okay, but generally in application development list.delete(value) is a hint that you're using the wrong data structure.
Experienced programmer can think that values in a list with such operation are indexed by a hash and delete by value is O(1) unless there is a big warning in documentation. Novice programmer probably haven’t seen such combination of an array and a hash table and will assume O(N) as taught in a college.
Which, for virtually all languages, is “obviously not”.
An example would be https://hackage.haskell.org/package/containers-0.4.0.0/docs/....
size :: Map k a -> IntSource
O(1). The number of elements in the map.
member :: Ord k => k -> Map k a -> BoolSource
O(log n). Is the key a member of the map? See also notMember.
lookup :: Ord k => k -> Map k a -> Maybe aSource
O(log n). Lookup the value at a key in the map.
And so forth...> Time complexity: O(N) where N is the number of elements to traverse to get to the element at index. This makes asking for the first or the last element of the list O(1).
I agree this is something more documentations should do when possible; it doesn't even have to be big-O notation as far as I'm concerned, just a "this will iterate over all keys" will be fine.
Ruby's delete() doesn't mention any of this, although you can easily see the (C) implementation in the documentation[2]. In principle at least, this doesn't have to be O(n) if the underlying implementation would be a hash for example, which of course has its own downsides but something like PHP may actually do this with their arrays as they're kind of a mixed data structure? Not sure.
[1]: from https://redis.io/commands/lindex
[2]: https://ruby-doc.org/core-3.0.0/Array.html#method-i-delete
Perfect example: Datetimes. In Golang, if you want to convert a string to a datetime (or vice versa), you _need_ to look at the godoc for datetime because it uses very specific permutations of "Jan 2, 2006" that you _have_ to include in your code. This is much more confusing than how this would be done in Python (provide the format of the date using ISO8601 notation) or Ruby (provide the string, get a Datetime, done).
The primary problem that I think the author was trying to get at is that go lacks tools for building abstractions. In a language with generic functions (for instance), it would be possible to implement a delete_at function completely in user code. The fact that you cannot reflects poorly on the language.
Of course it will always be possible to write functions that have arbitrarily slow time complexities. That's something you have to be aware of, with any function; it's not a reason to avoid including a linear-time function to remove an element at a given index of an array.
Abstractions are pretty and distracting but they are not why we write code and each one has a cost, which is often not apparent when introduced.
That’s not to say Go is perfect, there are a few little abstractions it would IMO be improved by having and the slice handling in particular could be more elegant and based on extending slice types or a slices package rather than built in generic functions.
As for a complicated for loop instead of the functional equivalent: I too am a fan of functional patterns in this case, but the for loop equivalent in my opinion is just as readable in 99% of cases. A little more verbose, that's all. Matter of taste.
If code breaks because list is now a hashmap, that seems like an anti-feature.
A range vs a series of filtering operations is an interesting question in terms of which is easiest to write or, crucially, for a beginner to understand after writing - not convinced foldl is easy for beginners. If you like abstractions like that, Go will be abhorrent to you, and that’s ok.
It is easy to build your own filter on a specific type, I’ve only done it a couple of times, it is literally no more than a for loop/range. Where I have written extensions and would appreciate helpers in the std lib is things like lists of ints and strings - contains, filter and friends would be welcome there in the stdlib (I use my own at present).
For filter etc I think I’d use them but am not convinced they would change the code much - the complexity is in what you do to filter not the filtering operation itself which probably adds about 3 lines and is very simple (set up new list, add to list in range, return list). For example in existing code I have lots of operations on lists of items which sit behind a function and for hardly any of them would I delete the function and just use filter in place at the call site, because I have the function to hide the complexity of the filtering itself (all those if conditions), which would not go away. I wouldn’t even bother rewriting the range to use filter because it’s not much clearer.
Very well said. If only more developers would understand this.
Yes I know, creating interfaces, inheriting everything from the void, adding as large amount of layers to create the beautifully constructed reusable structure is a nice drive but it might happen no one will reuse it as doesn't want to dive itself trough infinite level lasagna with rolling ravioli around it (and - thanks god, go doesn't have try/catch - throwing exceptions trough all the layers to force you to go down the guts of the construct to figure out what it means). Or, as it often happens, will be forced to reuse it, complicating his life and is code.
I do understand that inheritance, even operator overloading has its meaning and is extremely useful. But it has another side - everybody are overdoing it as "might come handy later" and the code becomes a case for "How to write unmaintainable code"[2].
When I am coding I am not doing it for philosophical reasons and go has until now succeeding being a very helpful tool without much of "meaning of life" complications. And i would love to see it stay that way.
If you are forcing me to read the documentation / code (presumably I know what I am trying to solve) to be able to use the "beautiful oo construct" and forcing you to read your beautiful design, you have failed making it and I have seen it in java everywhere. I just hope the same people making everything complicated more than it need to be wont skip from java[1] train to go train. I really don't want them anywhere close.
[1]https://github.com/EnterpriseQualityCoding/FizzBuzzEnterpris...
[2]https://github.com/Droogans/unmaintainable-code (and 20 others)
Abstractions are a great idea, and Go has plenty of these (io.Writer is ubiquitous), but most business logic is dead boring and doesn’t need it.
Erlang has had these "popular patterns" for decades. And all languages that ignore them, making devs reimplement them, poorly, with third-party libs and ad-hoc solutions.
Most all of them can be implemented as single lines of code in Python.
I’m sure there were a lot of assembly programs that used the “call stack pattern”. I’m sure there are a lot of C programs that abuse struct packing to do inheritance (the “class pattern”, sometimes with crappy vtables) or the preprocessor to do “templates”. And even in Python, you’ve got to use the visitor pattern due to single dispatch.
[1] https://blog.golang.org/pipelines
It is a very long doc, but that also shows that concurrency has so many patterns one might like.
My own pattern is typically
1. Decide level of parallelism ahead of time and start workers (that many `go X()` invocations 2. Setup sync.Waitgroup for the same count 3. Create two channels, one for each direction. 4. Job itself needs some sort of struct to hold details
Most of my jobs don't support mid-work cancelation, so I don't bother with anything else.
It also uses context (useful for long running, concurrent jobs) and handles mid-work cancellation
Another example of this is how HTTP middlewares chain together, see for example all the middlewares in https://github.com/gorilla/handlers. All of these exhibit one particular quality of idiomatic Go code: a preference for composition over inheritance.
Another quality of idiomatic Go code is that concurrent algorithms prefer channels over locking mechanisms (unless the performance penalty of using channels is too severe). I don't have immediate examples coming to mind on this one though, since the use of channels and mutexes tends to be quite intertwined with the algorithm in question.
> Registry: Keep track of all subclasses of a given class
Go does not have classes.
writing generic functions is difficult, so it's nice if a language allows people to do so, otherwise you get N inefficient and/or buggy reimplementations of the function in every project needing it. (not sure if that is your point)
I was never able to understand what happens, when I looked at a particular snippet of Ruby code, if it was not me who wrote it. With Go, understanding others people code is a trivial task.
list.delete(x)
Is hard to understand if you didn’t write it?You literally can't know what's going on under the hood without popping it and looking for yourself.
Ruby's infatuation with clever magic code is what turned me off it years ago. You can get up to 80% in 20% of the time compared to other languages, but then you spend the 80% of time you have left fighting against the magic to get the last 20% done properly. There's way too much stuff You Just Need To Know.
I came from C# where I have gotten increasingly frustrated with the amount of additions to the language. Like we now have classes, structs and records. We now have async/await but we also have channels. This is what having a huge toolbox causes. Some people are going to write code using channels, me others will use async. Some people will use features that others don't care about.
I think there's huge benefit in a simple language. In Go I know that there's only 2 ways to do concurrency. You either use go routines and mutexes or you use go routines and channels.
Generics will bring a lot of helper functions that weren't possible before which will remove a lot of repetitive code.
But otherwise I am super happy with writing Go code. All my pet peeves of something like modern Java or C# are gone.
Lot's of people have said go is easy. In particular that go code is "easy to read" is an oft-cited benefit of go.
Also, if you go to golang.org and the first thing you see is:
> Go is an open source programming language that makes it easy to build simple, reliable, and efficient software.
(emphasis mine)
I personally do think Go is easy to read. The fact that I don't get bitten in the ass by hidden behaviours and there's no inheritance to step through 5 files of. The fact that everyone's Go code looks more or less the same (unless you're an absolute beginnner) because there's only so many ways to do something. Compared to C# where reading someone else's code means I have to take a shot to calm my nerves and then take a guess at which permutation of language features they decided to use today.
A simple language is easier to read. This doesn't mean it's going to be easy for anyone who isn't a Go developer. There's no claims of that. But given a week to understand syntax and getting used to reading files, I don't think there's many places where you'd get stumped.
>Go is an open source programming language that makes it easy to build simple, reliable, and efficient software.
This still holds true for me. A simple language builds simple software. No noob Go dev is going to have a good time, but that's true in almost every language. Once you understand how everything works, how interfaces work, how channels work, you can simplify down most problems and build them with ease. Needing to write a few extra lines of code doesn't mean you can't write simple, reliable and efficient software.
Neither of those quotes say Go is an all round easy language. It's not like you can throw it at a baby and get back Kubernetes. It's not easy to do custom containers, it's not easy to do manipulate lists, it's not easy to do a bunch of things you'd do in one line in something like C#. But I think these are small hurdles for the mental burden you're relieved of when you see that the rest of the language is equally as simple and benefits from it.
Reading Go means that I keep having to read idioms (as posted in the article) and parse them back into the actual intent, rather than just reading the intent directly. But I can't even trust that, since the nth rewrite of the idiom might have screwed it up subtly.
Reading Go means that I can't easily navigate to the definition of a function, because it implicitly merges folders into a single namespace, so I have to try them one by one.
Reading Go means that I usually don't have tooling that can jump to definition, because there are umpteen different dependency management tools, and Gopls only supports one of them.
Reading Go means that even when I have tooling available, and it feels like working today, jump-to-definition becomes impossible as soon as interfaces are involved, because structural typing makes it impossible to know what is an intentional implementation of that interface.
You are right, c# does too much. But Go makes it far more work to do basic things - list comprehension for example (what is everyones obsession with for loops?). An opinated language with one way to do things sounds great, but not at the expense of basic niceities you get in other languages.
It's become a cliche to bring up brainfuck here, but it really is a direct refutation of these ideas, being a maximally simple language that produces maximally complicated code.
Generally speaking if you're modestly familiar with a language it's rare that it's difficult to read small pieces of code. The hard part of programming on a team is writing code such that high level intent is quickly apparent with APIs that support that intent and make it easy to understand where edge-case handling is happening vs direct feature intent
There are many macro C code bases with tons of function pointer fun (for example when doing OOP) where you have a hard time to find things, till you spent considerable time learning the choices made in that code base.
In fact, a kernel is even easier to read than other programs whose authors worked as hard, because having no external libraries at all also helps.
Arrays vs. slices and standard functions dealing with slices are full of weird behaviour, unnecessarily complex syntax and obscure features. Like, why are there even arrays at all? Which slice functions return a copy and which ones don't? Why is append() so braindamaged to make a copy sometimes? Why do I need make() as often as I do when mostly the language knows all make() would do?
Go still has lots of footguns, even right at the trivial basics.
Because while Go is garbage collected, it also provides modest control over memory layout. Arrays are allocated chunks of contiguous memory for holding a fixed number of things. Slices are something that sit on top of that and let you not worry too much about that.
You almost never want arrays yourself, but they are a legitimate use case that the language has to provide because you can't create them within the language itself. But the right answer for most programmers is to ignore "arrays" in Go entirely.
"Why is append() so braindamaged to make a copy sometimes?"
(Tone: Straight, not snark.) If you understand what slices are and how they are related to arrays, it becomes clear that "append" would be braindamaged if it didn't sometimes make copies. Go is simple, sure, but it was never a goal of Go to try to make it so you don't have to understand how it works to use it properly.
It is a fair point that quite a few tutorials don't make it clear what that relationship is. Most of them try, but probably don't do a good enough job. I think more of them should show what a slice is on the inside [1]; it tends to make it clear in a way that a whole lot of prose can't. "Show my your code and I will remain confused; show me your data structures and I won't need to see your code; it'll be obvious."
For example:
xrefs := []*struct{field1 int}{}
for i := range longArrayName {
longArrayName[i].field = value
xrefs = append(xrefs, &longArrayName[i])
}
Perfectly fine code. Now after a simple refactor: xrefs := []*struct{field1 int}{}
for _,x := range longArrayName {
x.field = value
xrefs = append(xrefs, &x)
}
Much better looking! But also completely wrong now, unfortunately. `defer` is also likely to cause similar problems with refactoring, given its scope-breaking function border.And Go also has several ways of doing most things. Sure, C# has more, but as long as there is more than one the problems are similar.
And I'm sure in time Go will develop more ways of doing things, because the advantage of having a clean way to put your intent into code usually trumps the disadvantage of having to learn another abstraction. This is still part of Go's philosophy, even though Go's designers have valued it slightly less than others. If it weren't, they wouldn't have added Channels to the language, as they are trivial to implement using mutexes, which are strictly more powerful.
range returns index and elements by value. The last example does what you asks of it, it's like complaining something is not returned by reference when it's not. Your mistake. Perhaps some linter could give warnings for it.
A language that has implicit copy/move semantics is easier to write (since it's less constrained), and more difficult to read (since in order to understand the code, one needs to know the rules).
A language that has explicit copy/move semantics, is more difficult to write (since the rules will need to be adhered to), but easier to read (because the constraints are explicit).
Although I don't program in Golang, another example that pops into my mind is that slices may refer to old versions of an array. This makes working with arrays easier when writing (as in "typing"), but more difficult when reading (as in understanding/designing), because one needs to track an array references (slices). (correct me if I'm wrong on this).
In this perspective, I do think that a language that is simpler to write can be more difficult to read, and this is one (two) case where this principle applies.
(note that I don't imply with that one philosophy is inherently better than the other)
EDIT: added slices case.
The lack of any kind of syntactic difference between vastly different semantic operations is at the very least a major impediment to readability. After all, lvalues vs rvalues are one of the biggest complexities of C's semantics, and they have been transplanted as-is into Go.
As a much more minor gripe, I'd also argue that the choice of making the iteration variable a copy of the array/slice value instead of a reference to it is the least useful choice. I expect it has been done because of the choice to make map access be an rvalue unlike array access which is an lvalue, which in turn would have given different semantics to slice iteration vs map iteration. Why they chose to have different semantics for map access vs array access, but to have the same semantics for map iteration vs array iteration is also a question I have no answer to.
xrefs := []*struct{field1 int}{}
var x struct{field1 int}
for i := range longArrayName {
x = longArrayName[i] //overwrite x with the copy
x.field = value //modify the copy in x
xrefs = append(xrefs, &x) //&x has the same value regardless of i
}
The desired refactoring would have been to this: xrefs := []*struct{field1 int}{}
for i := range longArrayName {
x := &longArrayName[i] //this also declares x to be a *struct{field1 int}
x.field = value
xrefs = append(xrefs, x)
}IMO records are a great addition, especially when it comes to point-in-time/immutable data. They also provide an improvement to developer efficiency, as basic data classes can be represented with a single line record definition.
> We now have async/await but we also have channels. Some people are going to write code using channels, me others will use async.
I'm not really sure what you're talking about here. C# has no concept of a channel. If you referring to the "System.Threading.Channels" package, it exists for those in the community that would benefit from it, and still uses familiar async/await syntax. It's also a very niche package that is unlikely to have significant adoption, so there's no real concern of the pattern "segmenting" the community.
These aren't the same thing. Brainfuck is "simple" in the same sense - no hidden behaviors or complex syntax - but it's virtually impossible to tell at a glance what a Brainfuck program does, or say with certainty how it will react to different inputs. Complexity has to live somewhere, and the more restrictive the language, the more complexity is moved to the program structure. Conversely, every special-purpose construct in a language is a place where you don't have to rely on reasoning about an unboundedly complex Turing-complete system, and can instead go and check the documentation for what it does.
I'm always confused by this. Why do people consistency cherry pick specific languages to compare then when those languages offer fundamentally different paradigms? I understand comparing C/C++ vs Go, but isn't obvious that C# is going to be fundamentally different than Go?
C++ is about as far from Go as you can imagine a language. Maybe only Prolog would be a worse comparison point.
C++ is designed with one goal in mind: no special compiler built-ins. If it is possible to do it at all, it can be done in a 3rd party library. C++'s design philosophy is Library first. Bjarne Stroustroup explicitly advocates this: don't write special case code. Write a library that solves the problem, then use that library to achieve your special case.
Go doesn't think writing libraries is a useful endeavor for most programmers: they should be writing application code, and let their betters design the tools they need, build them into the compiler, and stop wasting time designing your own abstractions.
https://commandcenter.blogspot.com/2012/06/less-is-exponenti...
/headscratch
EDIT: I think you're misinterpreting syntax of the language with use case. e.g. low level vs high level programming language
Furthermore, its very likely that the languages people 'cherry pick' are actually languages they use everyday. So they just compare 'new everyday language' to 'previous everyday languages', and just tell you whether they're happier or not when they write code. The point is not really about the language theoretical properties and merits, but how people experience the language in real life.
I don't know if this is a problem. Redundancy is good. If simple languages ruled, we would all be speaking Esperanto.
>Some people will use features that others don't care about. I end up having this argument with product managers who insist that because most people only use 60% of the functionality of a product, we don't need to implement the remaining 40%. You need more than one way of doing the same thing.
But when compared to previous monstrosities in C++ or Java with exceptions cluttered everywhere, deep inheritance trees that are absolutely useless..
then Go code is an absolute breeze to read and work with. The one thing I see frequently and dislike a lot in Go code bases is the frequent usage of interfaces for single structs just for the sake of mocking for unit tests.
Often I see cases where you could just strip the whole layer of crap and unit test the real functions themselves. But nobody seems to think about that. It seems that this "write interfaces for everything and then mock/test this" pattern is dominating currently.
Perhaps part of it is the languages we are comparing to? I'm comparing to languages such as rust and scala, and to some extent python and ruby.
> with exceptions cluttered everywhere
I prefer errors to be part of the return value to exceptions, but I also find repeating
if err != nil {
return nil, err
}
for almost every line of significant code in go functions pretty distracting. I much prefer rust's `?` operator. Or haskell's do notation (or scala's roughly equivalent for/yield).> deep inheritance trees that are absolutely useless.
uh, you can have deep inheritance trees in Go, and not have them in Java or C++, I'm not sure what your point is.
I think you should compare to languages that have a similar purpose / area of usage. In my experience that is C++ and mostly Java. I wouldn't dare compare dynamically typed and interpreted languages with Go.. what's the point? I don't have much experience with Rust so I cannot compare it and additionally it's rarely used in companies. Scala I just don't like personally. For my taste it just "is too much of everything".
> ... err != nil ...
In the beginning I was thinking the same. Over time I got used to it. When I write it I use a snippet. And clearly reading the flow of the error has been beneficial to me a lot of times. Yes it is verbose.
> uh, you can have deep inheritance trees in Go, and not have them in Java or C++, I'm not sure what your point is.
I am sure you know that there is no inheritance in Go so I am not totally sure what you are getting at. My point is that I think OOP by composition is a lot clearer than by inheritance. Also composition is not overused in Go as say inheritance is overused in Java.
Another example is the lack of annotations for validations. In Java or C#, you'd annotate the function argument with something like `@Validated`, and it's taken care of. In golang, calling the validation code would have to be done manually each time that's another 3-4 lines (including error handling).
Yet another example is that golang lacks a construct similar to C#'s `Task`. You can't execute a function that returns a value asynchronously (if you care about that value) without creating a channel and passing it.
golang also lacks pattern matching and discriminated unions (aka algebraic data types). Java and C# are getting both (both have pattern matching, and ADTs are in the works as far as I'm aware).
I just think we also tend to dramatize how much that matters.
I also think Go's benefit really is that it's simple and that you have very few tools that let you do anything other than focusing on solving your problem, and like the author I do think goroutines are the exception to that.
Where I work, we don't even use them. We use plain ol' mutexes instead.
When coming from higher level languages, Go does feel frustrating. In Javascript, you're writing `const [a, b, c] = await Promise.all([x(), y(), z()])`. In Go, you're writing 40 lines of WaitGroup code. It's easy to go ughhh.
But I think a nice way to appreciate Go's conservative middleground is to go back to writing some C/C++ code for a while, like some Arduino projects. Coming from that direction, Go feels like a nice incremental improvement that doesn't try to do too much (with perhaps the exception of goroutines).
Go's performance is also particularly stand-out which makes up for many of its convenience shortcomings. It's fast enough that I've written some code in Go where I would have written C not long ago. And writing C involves quite a bit more concessions than what Go gives you, so in comparison, Go kinda spoils you.
Go has plenty of annoyances too though. Not having any dev vs prod build distinction is annoying. Giving maps a runtime penalty of random iteration in order just to punish devs who don't read docs is annoying. It's annoying to have crappy implementations of things like html templating in the stdlib which steal thunder from better 3rd party libs. Not being able to shadow vars is annoying. `val, err :=` vs `val, err =` is annoying when it swivels on whether `err` has been assigned yet or not, a footgun related to the inability to shadow vars. etc. etc.
But it's too easy to overdramatize things.
I feel just the opposite: Go has very few tools, which forces you to often have to solve language games instead of focusing on your problem.
You want to remove an element from a list? Instead of writing that, you need to iterate through the list and check if the element at position i has the properties you want, and if it does, copy the list from i+1 (if it wasn't the last!) over the original list. This is NOT the problem you were trying to solve.
You want to store a map from some struct to another? Instead of writing that, you have to create a key struct that is comparable for equality from your original struct, which probably involves string concatenation and great care to avoid accidentally making unequal structs have equal keys; and then two maps, one from keys to the key struct and another from keys to the value struct, and of course client code also has to know about this explicitly.
You want to pass around some large structs, or iterate over slices of them? Well, you'd better start using pointers, and start being careful about what is a copy and what isn't, otherwise you'll pay a steep performance price, without any syntactic indication.
You want to work with sets of values? You'll have to store them as map keys, perhaps doing all of the black voodoo described above. And of course, you can't do a reunion of two sets, you have to iterate through the keys of the first map and the second map and add them to a third map.
All of these things are annoying when you are writing them, and even more of a problem when you are reading the code, as you have to understand the intent from the messy internals that Go forces you to expose.
Go is basically how Java was pre-generics. People forget that early Java was much like Go in some respects. Everything casted to object. Not really sure what the object is unless you wrote the code. Lack of common patterns and a rich set of libraries (Guava, Apache Commons).
Early Java was supposed to be safe C++. Converting C++ to Java is still trivial unless there's pointer craziness. Generally copy paste the C++ and change naming conventions.
I hate working on unfamiliar Go projects. Copy pasted code everywhere. And everyone builds their own abstractions because there aren't enough.
I don't understand Go's appeal. To me it feels clunky and stuck in the 90's. Designed by C programmers for C programmers.
Like C it has no generics, bad packaging, and is a pain in the ass to cross compile. Its only killer feature, IMO, is it's fiber threading model and that's quickly being copied by other languages, even ancient Java.
What? You've obviously never seen actual C++ code to make such a ridiculous statement.
- https://blog.stalkr.net/2015/04/golang-data-races-to-break-memory-safety.html
- https://golang.org/doc/articles/race_detector#IntroductionArguably, if the project requires lots of abstractions and copy-pasted code, then the project shouldn't be written in Go. Just because you can, technically speaking, write object-oriented code in Go does not mean that it is ergonomic to do so or a recommended use-case for the language. Projects that benefit from object-oriented design should stick to languages with first-class support for it.
Go modules are pretty good packaging IMHO. What don't you like about them?
As for cross-compiling, are you joking? It's very easy and i literally cannot imagine it being easier. What's painful about it?
The biggest reason why Go modules are a very poor packaging solution is the horrendous stupidity of tying them to source control. This makes it difficult to develop multiple modules in the same repo, it requires your source control system to know Go, it makes internal refactors a problem for all consumers of your code (e.g. if you are moving from github to gitlab, all code consuming your package will have to change their imports).
The versioning ideas of their module system, particularly around the v2+, have made it such a pain that there still isn't a single popular package that uses the proposed solution so far, not even Google ones like the Go protobuf bindings.
`go build -target x.y` could be considered fractionally easier than `GOOS=x GOARCH=y go build`? But yeah, this is a nonsense claim by GP.
But that's not really what the article says. As it mentions, I don't think these are insurmountable issues, and I still like and use Go: it's pretty much my go-to language these days. Overall, I think it did a lot of things right. But that doesn't mean it's perfect or can't be improved.
The blog post does admit "Are these insurmountable problems? No." Learning it in 5 minutes certainly is a stretch as you point out, but I'd still say you can learn it in a weekend to a greater degree than any other language I use.
Though you'll notice my comment after the first sentences is a way to share my own thoughts on Go, an opportunity I'm never going to neglect.
1. Lack of "manual memory management" can be taken as GC (though Rust is neither GC'd nor manual memory managed, it's declaratively memory managed) and a GC is unacceptably expensive for systems programming. Furthermore, until recently GC was also unacceptably bad for FFI. However stable Rust and Haskell with experimental features can nowadays GC across FFI without issue, but I'm still unaware of any other language that can.
2. Lack of "manual memory management" also means lacking direct memory access, etc. This is a problem in systems programming, though you do not need it for all systems programming.
3. Then there's green threads. Early on Rust had them, but they were removed when it was discovered it's theoretically impossible to implement them with acceptably low overhead for a C or even a serious C++ competitor. What's worse, you have to pay the overhead even if you don't use the feature. Nowadays, you can opt in by importing an ecosystem library that provides green threads but mostly people just use futures rather than green threads.
4. Interfaces: Even tinygo's documentation advises avoiding interfaces and tinygo isn't even a C/C++/Rust competitor, it's a micropython competitor.
5. ...
But to their point: why incur that sort of overhead for a language that doesn't necessarily give you enough conveniences to be worth it? Or rather, Rust gives you more conveniences with very little runtime overhead.
It's not exactly damning to be unable to compete with C on a microcontroller with 2K RAM, they were just pointing out the end of my comparison with C/C++.
It'd be nice to have syntactic sugar for splatting structs together, or a library that let you do it in a typesafe and efficient way.
EDIT: I will defend the random map order, though. Golang maps were only predictable up until a set number of items, which caused hard-to-detect bugs where your small test case would be in order but your large real-life data would be out of order.
The next item on my wishlist are algebraic data types. In complex projects I've opted for Rust over Go just because of the abstraction power of being able to represent things like:
Dest = Register (A | B | C)
Src = Register (A | B | C) | Immediate value
OpCode = Nop | Add(Dest, Src) | ...
In Go, I'm so tired of `interface{}`.Edit: To respond to your edit about map iter order, that's related to my complaint of Go's lack of dev vs release build. Randomizing map iter order in dev builds is acceptable to me. Go makes you pay for that in prod.
You can spell out all your types into explicit structs (where you can choose between a simple implementation with a little worse performance or to make the implementation a little more complex) and pass them around.
There are many complex code bases (kernels & databases come to mind) which use C (not C++), and they don't resort to passing void* around the "business logic" parts.
The idea that for a complex project you would choose a different language with so many different characteristics due to a minor detail about the type system (this is not exactly Haskell vs JS...). This kind of decision making would not pass in any reasonable organization...
As someone that was writing Arduino like code in C++ in 1993 (MS-DOS), I fail to see that.
I am fully aware of the last decade of C++, including papers written by Bjarne Stroustrup and other C++ key figures, where they uses the hated C/C++ expression, that so many happen to have issues with.
Which I can gladly point you to.
Given that security is one of my areas, I always keep up to date with C and C++ standards, even if I don't use them every day.
Can you explain what you mean by "runtime penalty?" Last I checked, the iteration is only random wrt its starting offset; after that, it proceeds linearly through the buckets. So you only generate one random integer per `range`, which seems acceptably cheap.
https://github.com/golang/go/blob/db8142fb8631df3ee56983cbc1...
Just like learning to read large C++ codebases, there's clearly a learning curve because a simple idea that would be a one-liner in a high level language is going to be 5-15 lines.
Go also makes the "learn to read" learning curve even steeper when folks pick up and propagate the core team's propensity for single letter variables. I feel like one of the understated benefits of ruby/python/similar languages being relatively readable is that folks are inclined to keep them readable. Whereas when you feel like you're one step away from specifying the register and how hot the welding torch should be to toggle the bits, you're somehow more likely to write `c.T.Client.Send(v)`.
IMO if we accept that most of our work is maintenance & modification of existing code, and most of the work in those tasks is understanding what's already been written, it follows immediately that code that's easy to read has high business value. I know it's too soon in my learning curve, but I'm really hoping I find the patterns that make Go easy to read soon. (and a little tangentially but the descriptive power of a variable name should be proportional to its scope - so imo anything exported or that I might see across multiple files is an awfully strong contender for a multi-word descriptive name.)
Here's my very opinionated view: in scenarios where maintainability and readability are the most important long-term characteristics of a program then the best programming language would be one which separated out the specification of a program's intent from its implementation.
Separating concerns is a design pattern that we've learnt is really important (remember the early days of PHP?). We currently encapsulate logic around reasons for that code to change because we recognize the value of that pattern. Extending that same design pattern to what we know about the long-term maintainability of code means that we should be abstracting intent from implementation to allow both to change independently. Compilers and virtual machines already do some optimizations to this effect at the moment but we could go way further in our programming languages themselves. I think this means that declarative programming is the logical evolution of software design (really hope someone has an intelligent counter argument here because I can't see any myself).
Go makes a great implementation language in this sense (Rust too) but it's bound to loose ground in the application space as soon as we find the right high-level declarative language to describe application software.
1. Golang did right design in terms of OOP
2. They didnt complicate the language by accepting everything.
3. At the same time, they made simple things so complex. Simple logics to removing an element from slice/array and even iterating them without screwing up the data.
I have been programming for 10yrs and I find myself doing those mistakes without proper IDE/golint, while I can easily code in other languages without much help. I can concentrate on solving the business problem rather than fiddling around the language
Go forces writing loop for removing an element from an array even if the programmer knows it's linear time. Ruby gambles that programmer isn't ignorant - or it's not critical - but allows the convenience every time.
Designing languages is hard.
It does not:
copy(a[i:], a[i+1:]) // Shift a[i+1:] left one index.
a[len(a)-1] = "" // Erase last element (write zero value).
a = a[:len(a)-1]
As for leaking Goroutines, yep that's a real thing. I had colleagues working on a Go project where exactly this was exhausting memory and causing the process to be killed. It can happen really easily.
I view this as a fairly fundamental problem with GC languages. GC solves some problems of course but in doing so it creates other problems.
For request-servicing type problems like servicing HTTP requests, I actually think the PHP/Hack model is pretty much ideal: stateless core (so low initialization costs unlike, say, Python), cooperative multitasking (ie no threads per se) and just throwing everything away after you're done. But I digress...
What Go does have going for it is syntax. It's minimal, opinionated without being too opinionated (IMHO) and familiar to anyone who has ever used a C-style language.
I tend to view Go as a better Python.
And fully agree on the GC part. GC doesn't remove the work of resource management. It simplifies it vastly. This can some times lull people into thinking they can ignore resources altogether.
The chances of deleting an element in an array being a bottle-neck are really low. I'd rather save my brain cycles for thinking about disk/network access and wait for profiling to show me the problem.
But you'll find other expensive operations (eg prepending an element to a slice) are much less verbose. At a certain point (of verbosity), you're just adding the potential for bugs.
The argument that writing out the loop is better for readability is just status quo bias. The real reason we don’t have standard functions for such things is a technical limitation that will probably be removed soon.
As far as I'm aware, you simply cannot kill a goroutine from another goroutine, i.e. there is no terminate()/stop() construct, meaning you cannot implement a timeout on top of an existing library without actually modifying said library.
* Expand. Rust doesn't provide an easy way to insert multiple values into the middle of a vector, so I had to do 2 lines: append, then rotate right a subslice starting at the desired insertion point by the length of the insertion.
* Shuffle. No RNG in Rust's stdlib, so I used Rand. I did re-implement the shuffle, but in reality I'd probably just use the shuffle function provided by Rand.
* In place dedup. Needed two lines: one to sort, then I could call dedup.
* Move to front. This is not a function Rust provides, so it's completely implemented. Needed 10 lines. This one needs to search for the item and move that if it exists. In the event it exists, I rotate it left onto the end of the vector, then rotate the entire vector right. Otherwise it inserts at the beginning.
[0] https://play.rust-lang.org/?version=stable&mode=debug&editio...
vec.splice(i..i, std::iter::repeat(0).take(length));
This replaces the values in range `i..i` (an empty range starting at i) with `length` copies of `0`.Don't program when tired, kids.
https://github.com/golang/go/issues/43651
This will make a generic "slices" package much more trivial to write.
(I don't think Rust is the best example of result types; it's missing a lot of tools for working with them because Rust doesn't have HKT and functions aren't quite first-class because of their lifetime checker, so instead of using ordinary functions you end up using ad-hoc macros or language special cases like ?. Since Go is garbage collected they wouldn't have that problem)
CI works, but I have to set it up. For every project. And fix anything that was not developed with golangci-lint.
See:
Problem statement: https://go.googlesource.com/proposal/+/master/design/go2draf...
Proposal one: https://go.googlesource.com/proposal/+/master/design/go2draf...
Proposal two: https://go.googlesource.com/proposal/+/master/design/32437-t...
Would love to take a more serious look later.
Between the two I found I needed much less referencing with Crystal, and when I did, I found it more intuitive, well described and easier to remember and apply the same concepts in other places.
As a non-expert at Go, I would have loved classic OOP + batteries included Python and remove the curly braces. Perfect.
Its main drawback is the small number of native libraries available for it, but wrapping and using existing libraries from other languages is a walk in the park.
That said Nim [1] provides nicer metaprogramming features than Cython and very nice overall ergonomics..I think more ergonomic than Python with, e.g. UFCS. Nim has almost all the safety/speed of Rust with better ergonomics than Python. What is not to love?
- The tiny standard library and low learning curve make it quite easy to get started.
- Concurrency is a first class citizen, through goroutines and channels
- The performance, combined with the low barrier to entry, combined with its popularity make it very desirable for companies
In my experiences this means a lot of companies without deep concurrent programming experience begin to adopt it. In my experiences this means concurrent programming errors....literally EVERYWHERE. I've seen (and created these of course) in every company I've been at and in huge open source go projects.
I wish go would address this through first class tooling or type supported in the language like rust.
I have a hueristic based approach that I've had success with when writing and reviewing code, but writing a static analysis tool for it is a bit out of my experience:
https://medium.com/dm03514-tech-blog/golang-candidates-and-c...
At a prior job we had a lot of code that wasn't structured in a way that made it easy to exercise using the race detector. This was combined with misunderstanding about what the race detector did and didn't do. For example there was one team that ran `-race` on non-concurrent code, and expected it to verify race conditions.
This, exactly. It’s worth contrasting this with Python, which has a reputation for being simple but is actually extremely complex - but it is easy (at least for small programs).
However, I can’t help thinking that the two shouldn’t be in opposition. IMO it should be possible for a language to be both simple and easy to use.
But couple of points about simplicity author missed out:
- deleting from slice is expensive so should be explicit. True that lack of user generics prevents people to write own generic function for that, but that's not the only thing that generics will inevitably bring – are we still talking about complexity?
- Go is still the easiest language to pick up and understand due to smaller basis in the feature vector space. That's what Go authors mean by orthogonality of features. The search for the solution is way more straightforward in Go space – and that's what we call a cognitive load.
Oh well - that being said Go is way simpler than C++18 that I've done a tiny bit of work in. I think the issue is that Go generally competes with Ruby/Python/JavaScript in certain scenarios and just seems way more complex in comparison.
If you compare Go with C++/Java/C# it seems more reasonable and "easy."
^ the good news is that a ton of money is being spent to improve Node performance - whether it'll approach Java/C++ speed is TBD, but the performance gap is shrinking year by year.
C# was one of the first languages I learned after HTML4 and Game Maker, no problem.
Java I learned later, no problem.
I also get C++ though I never used it enough to be really proficient.
And of course I know a bunch of other languages from awk to bash to javascript to php to ruby to sql to vba (not an exhaustive list).
But whenever someone makes something in Go, they seem to expect that the unique patterns and unusual operators for backgrounding tasks etc. to be intuitive to me and I should be able to pick up where they left off in a few minutes... it's not just not.
(Out of curiosity, is this C++17 or C++20? I'd assume 17, and also I'm curious which aspects you find confusing.)
Several coworkers have remarked that it's just easier to reason about correctness of concurrent code in Go vs. C++, while not sacrificing performance as in, say, Python. (There's definitely a performance hit, per the use of a garbage collector.)
One thing that's always bothered me about Go is just the things that are missing from the standard library; I think that contributes significantly to it being "harder" to work with.
(A good number of these likely stem from the lack of generics, e.g. max(a, b int) int -- math.Max takes and returns floats.)
That said, the biggest thing I liked about Go from the first day I used it was the treatment of concurrency as a first-class citizen (goroutines, channels).
Java honestly is close. Data object creation is notably worse unless you're using Lombok or similar. Besides that, modern Java and JS syntax are similar. And Java doesn't have a horrible underlying object model based on prototype chains.
IMO Go seems easy because it's simple. I know Java and JS well and they're about the same complexity these days.
Knowing both I will never touch JS for backend again. Java is better suited for it. Strong typing is light-years easier to follow code paths and refactor. IDE's, debugging, profiling tools, memory usage, and performance are all better in Javaland. Real threading with proper shared memory exists. App size is smaller since Java bytecode is compact and distributed zipped.
My company stopped using JS on backend after the hype wore off. Our Java backends are easier to monitor and maintain.
Go is also works for the backend, but you have to get into the mindset that you're probably going to write a lot of code. That's fine, but it's a shift from many other languages. As someone who has programmed for 20+ years, I like the single way to do most things in Go. There's also a bit of elegance coming full circle through many languages where a for loop is once again just a for loop.
And maybe I'm showing my age, but I also completely agree that backend languages need strong typing. The other option is significant test coverage, but we have all seen how that goes in practice. Code without strong typing always feels like it's meant to be written once and thrown away because it's so hard to read later. And, this style fits with how UIs often work where the life of a product will end up with many UIs on the same backend.
Sometimes I work on 10+ year old areas of codebase and it's fine. Compared to dumpster fire of 10 year old JS/Python/C++ it's paradise.
I use lombok judiciously to avoid getters/setters, the cruftiest part of Java. Newer versions of Java are quite pleasant.
Java is extremely mature. I pull in a library and it "just works" 99% of the time. No worries about the version of Java is was built for or missing native dependencies. Practically every language falls short of the polished user experience in Java.
I don't like Kotlin because the non-Android experience is an afterthought and it's a captured language by Google and Jetbrains. Java has a lot more hands at the wheel.
Kotlin fixes tons of issues in the frankensteined Java 8 of Android. You can use Java 15+ everywhere else and its adopting many features that made Kotlin worth using.
Google is pushing Kotlin so they have control over Android language like Apple does for iPhone. It's just a nice layer over Java in the end since 100% interop is required.
Syntactically it's even more terse than JS and is as fast for most use cases as C. Main downsides are memory usage and needing to understand JVM limitations for latency sensitive codebases (TLDR: Use a modern JVM and ZGC or Shenandoah).
Using the buffer size of a channel to control number of concurrent jobs is just the wrong approach. It's so much easier and cleaner to just use the number of goroutines for that:
const workers = 3
const jobs = 20
jobsChan := make(chan int)
var wg sync.WaitGroup
for i := 0; i < workers; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for work := range jobsChan {
time.Sleep(time.Second)
fmt.Println(work)
}
}()
}
for i := 1; i <= jobs; i++ {
jobsChan <- i
}
close(jobsChan)
wg.Wait()
fmt.Println("done")
One thing about channels in go is that the only time you want to use a buffered channel is the time that you know exactly how many writes (n) you'll have to the channel, while n is finite and reasonably small, so you create a channel with buffer size n to unblock all the writes. An example to that is that if you want to strictly enforce a timeout to a blocking call: const timeout = time.Millisecond*10
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
resultChan := make(chan resultType, 1)
go func() {
result, err := myBlockCall(ctx)
resultChan <- resultType{result, err}
}()
select {
case <- ctx.Done():
return nil, ctx.Err()
case r := <- resultChan:
return r.result, r.err
}
If you are using a buffered channel in other cases, you are likely doing it wrong.The remove-by-value snippet is 8 lines, and it can be wrong in a subtle way: if list contains pointers, then we have a memory leak beyond the slice final length (pointer values still exists within capacity, and objects are not garbage collected).
I wrote several possible implementations here, none of which is as concise and as obviously correct as an hypothetical "list.remove(v)": https://programming-idioms.org/idiom/136/remove-all-occurren...
I disagree. I use Go for our API backend because I can onboard junior developers and have them producing very fast. Because they are familiar with loops and C-syntax, this is very much a guaranteed "pick up in ~5-10 minutes"
The explicitness of Go makes it easy for them to get to work, and easier for me to understand what they are doing when it comes to code review
They won't ever have to use interface{}, nor concurrency, and slice manipulation won't take much time to lookup any "cheat sheet", or copy from another file
Even so have you considered whether this approach has some downsides and risks?
I usually ask them to read gobyexample.com, but they can start working on projects on the first day.
CRUD project with Go makes it easy to add new endpoints and simple features, so we can focus on hiring frontend devs and smoothly turning them into "fullstack CRUD devs"
I am sure this can be achieved with other languages, but for our use case (software-house) Go just fits really well because it provides a low cognitive overhead for juniors and even interns to make sense of the project and contribute
It fits pretty nicely in an "assembly line" style development, but maybe the downside is that it is boring and makes devs want to leave because they feel like they're always hitting the mark and "mastered" development and want to climb to the next level. It seems reasonable for them, but the turnover is tiresome (around 6 months) for me
I do think that we (or me!) hold ourselves to too high a standard in terms of mastering all aspects of a language, especially given some languages have got too big. It's refreshing to hear an approach that focuses on knowing just enough to get the job done!
Running delete on a slice is the same as trying to delete an element of an array. It's the wrong data structure if you need that type of access pattern. That's why it's hard. You can cut a tree down with a hammer, but it's going to take a while and you're going to have some blisters.
Go has built-in linked lists. https://golang.org/pkg/container/list/ and I will agree, they're not as easy to use as other languages due to the lack of generics in go, at present. Using the list.* package requires type conversion to evaluate and pull values out of the structure.
I think it's fair to say that Go needs to do a better job explaining the list package or possibly including it in the "Tour of Go" guide. That might help avoid these misconceptions in the future.
In my experience with Go, a lot of basic functionality is missing and requires you to write your own utility functions. And some things you would expect like mySlice.append(elem) are instead written mySlice = append(mySlice, elem) which isn't as elegant. Plus you have to remember it's not just append(mySlice, elem)
If there's a handful of open source utility function libraries, that will split the Go developer base, kind of like how you used to have both jQuery, Prototype.js, underscore.js and others in Javascript. They should have just standardized all the common functionality you'd want and in an intuitive way.
And most arrays fit in the cache.
However, you're likely right for slices with len < 500 or so due to garbage collector churn on linked lists.
If you keep deleting elements from arrays then maybe, but if you do so once in awhile the cache-friendliness and limited allocations overhead of an array will most likely come out ahead. Especially if you also want arbitrary access.
Removing an element from a linked list is cheap if you’re already at that element (assuming the node itself is exposed), but most operations are way costlier.
I think the article kind of got the idea midway through, at "I think most programmers can figure out what the above does even without prior Go experience". Yes. That's the idea.
Concurrency (less the pattern libs that generics will provide) feels generally hard. Although channel semantics in Go are very annoying (e.g what happens when you close a buffered channel). This is hard in most languages I’ve used. Anyone have an example of what the state of the art hard-to-shoot-yourself-in-the-foot concurrency looks like in another language?
Also pointers are a thing people are afraid of for some reason.
On the other hand i notice that most go code i have written does not need any maintenance at all, which is neat.
To echo others, when Go makes something cumbersome (like an O(n) operation) it’s usually to hint to users that they’re doing something inefficient.
But, agreed you can’t pick up Go in minutes. It’s best approached coming from a background of writing service binaries in C/C++. If you aren’t familiar with strong type systems and concurrency you’re going to have problems.
Go is easy once you internalize its semantics. And this is the kicker of Go, it remains simple even after you have achieved a level of mastery of the language. This is not the case even for a language like Java. Certainly, C++ gurus will have your eyes bleed reading their code.
But this is not the case for Go, for the simple fact that Pike et al have engineered so that you can not do astronautics with Go, without enduring severe pain. So no one does it. Short of Go assembler code, any Go code out there is accessible to an exceptionally wide range of Go programmers. There is something to be said for that.
What are the best ressource online about how to think in a language (RUST/GO/C/C++/JAVA/JS/PYTHON/HASKELL/CAML...) ? Not only limited on about to syntaxically write a correct program !
Go has become the language of cloud infrastructure:
https://hackernoon.com/go-has-indeed-become-the-language-of-...
And there are lots of developers who want to use it:
https://zoolatech.com/blog/why-use-golang-for-your-project-a...
Are there things we would want added to the language? Sure. Generics. Check out the progress:
https://thenewstack.io/this-week-in-programming-go-approves-...
If there are other things you want added, it's open source, and you can join the team working on it.
Don't lose sight of the fact that Go has done alot of things right, and is GREAT the way it is right now. The uptake in the developer community proves it's worth.
I've seen this so much. I did it too. You can almost watch people go through this process in places like r/golang.
We all start off coming from a different language and being used to that, and while Go takes about an hour to learn all the syntax, it's only then that the learning starts. We all then try to write code the way we've always written it, only to find that Go doesn't work like that. Then we bump into the "this standard lib logging library is so shit, let's write a better one" (for the more ambitious, this is a whole framework). Then we learn more, and begin to realise why the logging lib is like that, and why there's no need for some kind of middleware construct, and so on. I'm not sure where this journey ends, because I haven't finished it yet - I suspect my understanding of chans and goroutines is incomplete because I still think they're "a bit clunky".
Simple is hard. But good.
After initial skepticism Go as become my personal go-to tool for backend, cli tools and throw away scripting stuff. At my workplace Go has replaced Spring and Node for standard business backend stuff as the standard stack.
This is *not* because Go is such a fabulous and easy language, but because of its overall characteristics:
- pretty impressive runtime performance and more importantly memory efficiency
- large and very useful standard lib
- useful tooling
- stable and mature
- encourages a package oriented design that enables large scale, however that can be achieved with many other languages as well. In other words: Building your app with packages as if they were microservices.
Despite it's flaws (and it definitely has flaws), Go has replaced Python as my go-to language, not because it is easy but because it is small.
Want to query the db in parallel? Start n goroutines and write the results to a channel, then read from the channel n times. Want to limit your concurrency to m tasks? Start m goroutines reading tasks from a tasks channel and pushing results to a results channel, write n tasks to the tasks channel, read n results from the results channel.
What happens when you write to a closed channel? What about a blocked one? What if you read from a closed one. Can you close a closed channel? Does that panic? What was the non blocking read? What about write?
Fuck that, I use the sync Mutex primitives and good old slices instead.
Mutex code though? 90% correct, easily. It's often dramatically simpler to make correct, especially when more than one construct is needed, or nearly anywhere with error handling. (`defer` is quite nice for locks)
And even if it turns out you don't really like Go: that's okay. The only way for yourself is to actually learn it, and you will be more knowledgable and experienced regardless.
As a beginner, it takes a while to figure out good goroutine design patterns. And you get bit by append to slice copies a few times. And it takes a bit of time to figure out how to structure code efficiently, and get over the fact that the code is not completely DRY.
I'm sure I forgot a few gotchas, but this list is still a miles shorter than all the popular languages out there.
This is probably what the author was looking for https://pkg.go.dev/golang.org/x/sync/semaphore
That is, most things that are "hard" have a reputation as being hard by non practitioners.
Which isn't to say that empirically it is clear some languages make some errors easier to make.
del a[i]
and have the go code auto generated. py2many doesn't do that yet, but you can teach it to.https://medium.com/@gotzmann/so-you-think-you-know-go-c5164b...
> slices.delete(slice, index)
A lot of things about Go are not simple. In my opinion a simple language is not about how simple it is to write a parser or a compiler for that language, but it's about how many different non-trivial and arbitrary pieces of information the developer has to memorize.
This is highly tied to the Principle of Least Astonishment[1] in language design: how many unexpected surprises does the programmer have to deal with?
With Go, you get quite a lot:
1. Go already has generic types. These are the magical maps, slices and channels. Everything else is not.
2. Even if you think #1 was also true for Arrays in Java 1.4 and no one was complaining, Go goes further: it already has generic functions like 'copy', 'len', 'min' and 'append'. Since you cannot properly describe the interface of a magic built-in function like 'append' using the Go language itself, this is not a standard library function, but should be viewed as an entirely new piece of custom syntax, like the print statement in Python 2.x.
3. Nil interfaces and interfaces with a nil pointer are not equal.
4. Multiple return values are a magical beast - they are not tuples and you cannot manipulate them in any useful way.
5. Channel axioms[2]. Possibly one of the more astonishing and painful aspects of Go.
5. Slices are mutable, unless you copy them. This can lead to some very surprising cases where a slice is passed down many layers below and then modified, breaking the caller.
6. Continuing the topic above, Go has neither clear data ownership rules (like Rust), clear documentation tradition on who owns the data passed to functions (like C/C++) nor a way to enforce immutability/constness (like C++, Rust or FP languages). This really pushes a lot of the cognitive overload to the developer.
7. Go modules are a lot better than what we had before, but are quite hard to deal with. The moment you need to move to v2 and above and start creating subdirectories they becomes rather confusing compared to what you would do in other package management system.
8. If a simple language is a language that allows you to write _simple programs_, and you follow Rich Hickey's classic definition of Simple[3], then Go is probably one of the LEAST simple languages available today.
tl;dr: I'm not saying other languages often compared to Go (like Rust or Java) don't have their own share of complexities, but I don't think Go should be viewed as a simple language in the broadest sense. It is a language with a rather simple implementation for our day and age (though Pascal was much simpler if we stretch this definition backwards).
[1] https://wiki.c2.com/?PrincipleOfLeastAstonishment [2] https://dave.cheney.net/2014/03/19/channel-axioms [3] https://www.infoq.com/presentations/Simple-Made-Easy/
But Go itself is comparatively easy. By far the fastest I have ever onboarded new engineers with, even more so than Python (which is terrible but somehow has a good reputation at being easy and friendly).
In my personal experience, I had an easier time getting onboarded on Python than Golang.
Why would you use an array/slice instead of a linked list, but then want to use it like a linked list?
If you need a linked list, use something like this: https://golang.org/pkg/container/list/