
Why Go is a Poorly Designed Programming Language
- Transfer
This is a translation of article yuzerneym tucnak to Medium, which was an extensive discussion on reddit.com/r/programming .

Okay, the headline is really a little loud, I admit. I’ll say more: I’m coming from high-profile headlines, all because of attention. In this blog post I will try to prove the fact that Go is a terribly thought-out language (spoiler: it is). I have already been playing with Go for several months now, the first helloworld put together, it seems, in June. I’m not a mathematician, but since then something has passed about 4 months and I even managed to upload it to GithubMultiple repositories and collect some stars! It is also worth mentioning that I have absolutely no experience with Go in production, so any of my words about “code support” or “deploy” should not be taken as the only true truth.
I love Go, I loved it the first time I tried it. I spent several days accepting the idiom, reconciling myself to the lack of generics, sorting out the frankly strange way of handling errors, and you know, all these classic problems that are somehow related to Go. I read Effective Go , a lot of blog articlesDave Cheney, followed all the news from the world of Go. I can even say that I am quite an active participant in the community! I love Go and I can not do anything about it - Go is just wonderful. However, I believe that Go is a terrible poorly thought out language that does absolutely not what it “sells”.
Go is considered a simple programming language. As saidRob Pike, they “removed everything that could be removed from the language”, which made its specification simply trivial. This side of the language is simply amazing: you can learn the basics in minutes, start writing real code right away, and in most cases Go will behave exactly as you expect. You will be furious a lot, but fortunately, everything will work cool. In reality, everything is a little different; Go is far from a simple language, rather just a bad one. Now look at a few arguments that support my words.
Slices (these are such sophisticated arrays) are very cool, I really like the concept and the implementation itself. But let's imagine for one second that at some point we want to write some source code with them, maybe just a little bit. Slices are the heart of the language; this is one of those concepts that makes Go cool. But still, let's imagine that somehow, all of a sudden, in between conversations about “concepts,” we will want to write a bit of real code. Here is what Go offers us in this case:
Believe it or not, but that’s how gophers transform slices every day. And since we don’t have any of these generics for you, writing a beautiful insert () function that will hide all this horror simply will not work. I uploaded this working example to the playground , so you should not believe me: you can check it with your own hands.
They tell us that “errors in Go are more than just strings” and that we should not treat them like strings. For example, spock13 from Docker said this during an excellent performance on “7 common mistakes in Go and when to avoid them.”
They also say that we should always return errors through the error interface (uniformity, readability, and so on). This is exactly what I am doing in the code snippet below. You will probably be surprised, but this program really does say hello to Mr. Pike, but is everything the way it should be?
Yes, I am aware of why this is happening, since I read a sufficient amount of specialized literature on the device interfaces in Go. But you must admit, for a novice gopher, such a big top will be like a butt on the head. This is actually a famous trap . As you can see, Go is such a straightforward and easy to learn language without bad distracting features that sometimes it thinks that the null interfaces are not very null;)
If you are not aware of what it is, then let me quote Wikipedia : “the concealment of variables occurs when a variable defined in a certain context (conditional block, method, inner class) has the same name as the variable in an external context. ” It sounds sensible, sort of like a fairly well-known practice, most programming languages support hiding and all right. Interestingly, Go is no exception, but here things are a little different: we have an operator: =, which adds fun. Here's how variable hiding works in Go:
Yes, I am aware that the operator: = creates a new variable and sets it to a value of the type on the right and what acc. with a language specification, this is absolutely correct behavior. But you see, it’s worth removing the inner context (curly braces) and the code will work exactly as we expect (“after 42”). Interesting girls are dancing, right? In another case, you will have to play with wonderful hiding variables.
It should be noted that this is not just a fun example that I came up with for dinner, it is a real trap into which people fall from time to time through no fault of their own. This week I refactored some code on Go and met this problem twice. Compilers don't care, linters don't care, everybody doesn't care, but the code doesn't work correctly.
The interfaces are cool, Pike & Co. they continue to say that they are what Go is: interfaces solve the problem of generics, we use interfaces for tests, through them polymorphism is built in Go. I tell you, I loved the interfaces while reading Effective Go and I continue to love them. Nevertheless, in addition to the problem “this zero interface is not completely zero”, which I spoke about a little earlier, there is another unpleasant problem that will make me think that Go does not have full support for interfaces . In simple words, you simply cannot pass a slice of structures (that satisfy a particular interface) to a function that accepts a slice of that interface:
It is not surprising that this is a known problem that is not even considered a problem. This is just another fun thing about Go, okay? I highly recommend reading the related Wiki article on the topic, it will become clear why the trick of passing the [] struct as the [] interface will not work. On the other hand, think about it! We can do this, there is no magic, this is just a compiler problem. Look, right here, a little higher in the code, I did it manually. Why can't the Go compiler do this big top for me? Yes, explicit is better than implicit, but still?
I just can’t stand the way people look at shit like this, which is simply full in the language and continue to say "well, yes, that's normal." This is not the norm. This is what makes Go an awful programming language.
UPD: This is a somewhat far-fetched problem when viewed from the side of the language. And if you don’t look, it’s all the same, because we are talking about schools of the language itself, and not of documentation and ecosystems.
This is generally the first problem I have ever encountered in Go. Okay, here we have a for-range loop that lets you walk through slices and listen to channels. It is used everywhere and it’s okay. Here is another minor, but not very pleasant problem that most inexperienced neophytes face: the range loop supports iteration only “by value” , it just copies the values and you can do nothing about it - this is not foreach for you from C + +.
Please note that I do not swear that Go does not have range-loops "by reference", I swear that these range-loops are rather unobvious. The verb “range” seems to say “walk through the elements”, but does not really say “walk through the copies of the elements”. Let's look at the For section of “Effective Go”, it doesn’t say a word about the fact that range loops copy the values of a slice, it just isn't written about it. I agree that this is a very insignificant problem, in addition, at one time I managed to get over it very quickly (within minutes), but an inexperienced gopher can spend some time debugging a bunch of code, not understanding why the values in the array do not change. It would be necessary at least a little, but to outline this case in “Effective Go”.
As I could say earlier, Go is considered to be a clean, simple and readable programming language with a strict compiler. For example, you cannot compile a program with unused import. Why? Just because Mr. Pike thinks it should be. You can believe it, but you can not, but unused import is far from the end of the world and people can survive it, especially during active debugging. I completely agree that this is not very correct and the compiler should just give some kind of warning, but for heaven’s sake, what is the point of stopping the compilation because of such a trifle? Unused imports, seriously?
Go1.5 got us a cool changeLanguage: Finally, you can specify dictionary entries without explicitly specifying the type of stored value. It took the boys from the Go team a little over five years to figure out that an explicit type indication might be a little redundant.
Another great thing I'm going to go to from Go is commas. You see, in Go we have the so-called multi-line declaration blocks (import / var / const):
Okay, but as soon as it comes to “readability,” Rob Pike decides that he needs to suddenly add commas. At some point after adding commas, he decides that it is worth leaving a comma on the last line of the list. Thus, instead of writing like this:
We are forced to write like this:
I still wonder why we can’t just do without commas on import / var / const blocks and certainly not on lists or dictionaries. In any case, Rob Pike definitely fumbles better than me , so everything is OK. Long live the readability!
Firstly, I have nothing against code generation. For a poor language like Go, this is probably the only way to avoid copy-paste for generic pieces. In any case, go: generate - the tool for code generation in Go, which is used by gophers around the world, simply sucks. Well, to be completely honest, the tool itself is ok, I like it. The problem is not in tools, but in the approach itself. Look, in order to generate some kind of code, you need to use a special magic comment. Yes, a magic sequence of bytes somewhere in the comments of your code can generate code! Just look at the spectacular big top we see below:
Comments should explain the code, not generate it. In any case, magical commentary is an ordinary working practice in modern Go. It’s interesting, but everyone doesn’t care, everyone is happy with everything, it’s okay . It seems to me personally that this is a lot more evil than the damn unused imports that are felling the compilation.
As you can see, I did not swear about generalizations / error handling / sugar and other classic issues that are talked about in the context of Go. I agree that generics are not mastheads at all, but then give us a normal generation, not magic circuses in the comments, which sort of generate code. If you take away the exceptions from us, then please give us the opportunity to normally check the zero interfaces with zero! And if you take away “ambiguous in terms of readability” sugar, then give us the opportunity to write code that works as expected, without a “hop” of hiding variables.
Anyway, I will continue to use Go. There is a good reason for this: I love him. I hate language: it is just disgusting, but I adore the community, tools and the lion's share of the concepts that language offers, the whole ecosystem.
Pssss, man, you wanna fork Go?

Okay, the headline is really a little loud, I admit. I’ll say more: I’m coming from high-profile headlines, all because of attention. In this blog post I will try to prove the fact that Go is a terribly thought-out language (spoiler: it is). I have already been playing with Go for several months now, the first helloworld put together, it seems, in June. I’m not a mathematician, but since then something has passed about 4 months and I even managed to upload it to GithubMultiple repositories and collect some stars! It is also worth mentioning that I have absolutely no experience with Go in production, so any of my words about “code support” or “deploy” should not be taken as the only true truth.
I love Go, I loved it the first time I tried it. I spent several days accepting the idiom, reconciling myself to the lack of generics, sorting out the frankly strange way of handling errors, and you know, all these classic problems that are somehow related to Go. I read Effective Go , a lot of blog articlesDave Cheney, followed all the news from the world of Go. I can even say that I am quite an active participant in the community! I love Go and I can not do anything about it - Go is just wonderful. However, I believe that Go is a terrible poorly thought out language that does absolutely not what it “sells”.
Go is considered a simple programming language. As saidRob Pike, they “removed everything that could be removed from the language”, which made its specification simply trivial. This side of the language is simply amazing: you can learn the basics in minutes, start writing real code right away, and in most cases Go will behave exactly as you expect. You will be furious a lot, but fortunately, everything will work cool. In reality, everything is a little different; Go is far from a simple language, rather just a bad one. Now look at a few arguments that support my words.
Reason # 1. Slice manipulation is just disgusting
Slices (these are such sophisticated arrays) are very cool, I really like the concept and the implementation itself. But let's imagine for one second that at some point we want to write some source code with them, maybe just a little bit. Slices are the heart of the language; this is one of those concepts that makes Go cool. But still, let's imagine that somehow, all of a sudden, in between conversations about “concepts,” we will want to write a bit of real code. Here is what Go offers us in this case:
// Давайте сделаем немного чисел...
numbers := []int{1, 2, 3, 4, 5}
log(numbers) // 1. [1 2 3 4 5]
log(numbers[2:]) // 2. [3 4 5]
log(numbers[1:3]) // 3. [2 3]
// Интересный факт: отрицательные индексы не работают!
//
// numbers[:-1] из Python не прокатит. Взамен нам предлагают
// делать что-то вроде этой циркопляски с длиной контейнера:
//
log(numbers[:len(numbers)-1]) // 4. [1 2 3 4]
// “Потрясная” читабельность, Мистер Пайк! Хорош!
//
// А теперь давайте допишем шестерку:
//
numbers = append(numbers, 6)
log(numbers) // 5. [1 2 3 4 5 6]
// Самое время удалить из слайса тройку:
//
numbers = append(numbers[:2], numbers[3:]...)
log(numbers) // 6. [1 2 4 5 6]
// Хочется вставить какое-то число? Ничего страшного,
// в Go есть общепринятая best practice!
//
// Мне особенно нравится это шапито с троеточиями ...
//
numbers = append(numbers[:2], append([]int{3}, numbers[2:]...)...)
log(numbers) // 7. [1 2 3 4 5 6]
// Чтобы скопировать слайс, ты должен будешь написать это:
//
copiedNumbers := make([]int, len(numbers))
copy(copiedNumbers, numbers)
log(copiedNumbers) // 8. [1 2 3 4 5 6]
// И это еще не все.
Believe it or not, but that’s how gophers transform slices every day. And since we don’t have any of these generics for you, writing a beautiful insert () function that will hide all this horror simply will not work. I uploaded this working example to the playground , so you should not believe me: you can check it with your own hands.
Reason number 2. Null interfaces are not always zero :)
They tell us that “errors in Go are more than just strings” and that we should not treat them like strings. For example, spock13 from Docker said this during an excellent performance on “7 common mistakes in Go and when to avoid them.”
They also say that we should always return errors through the error interface (uniformity, readability, and so on). This is exactly what I am doing in the code snippet below. You will probably be surprised, but this program really does say hello to Mr. Pike, but is everything the way it should be?
package main
import "fmt"
type MagicError struct{}
func (MagicError) Error() string {
return "[Magic]"
}
func Generate() *MagicError {
return nil
}
func Test() error {
return Generate()
}
func main() {
if Test() != nil {
fmt.Println("Hello, Mr. Pike!")
}
}
Yes, I am aware of why this is happening, since I read a sufficient amount of specialized literature on the device interfaces in Go. But you must admit, for a novice gopher, such a big top will be like a butt on the head. This is actually a famous trap . As you can see, Go is such a straightforward and easy to learn language without bad distracting features that sometimes it thinks that the null interfaces are not very null;)
Reason number 3. Funny hiding variables
If you are not aware of what it is, then let me quote Wikipedia : “the concealment of variables occurs when a variable defined in a certain context (conditional block, method, inner class) has the same name as the variable in an external context. ” It sounds sensible, sort of like a fairly well-known practice, most programming languages support hiding and all right. Interestingly, Go is no exception, but here things are a little different: we have an operator: =, which adds fun. Here's how variable hiding works in Go:
package main
import "fmt"
func Secret() (int, error) {
return 42, nil
}
func main() {
number := 0
fmt.Println("before", number) // 0
{
// meet the shadowing
number, err := Secret()
if err != nil {
panic(err)
}
fmt.Println("inside", number) // 42
}
fmt.Println("after", number) // 0
}
Yes, I am aware that the operator: = creates a new variable and sets it to a value of the type on the right and what acc. with a language specification, this is absolutely correct behavior. But you see, it’s worth removing the inner context (curly braces) and the code will work exactly as we expect (“after 42”). Interesting girls are dancing, right? In another case, you will have to play with wonderful hiding variables.
It should be noted that this is not just a fun example that I came up with for dinner, it is a real trap into which people fall from time to time through no fault of their own. This week I refactored some code on Go and met this problem twice. Compilers don't care, linters don't care, everybody doesn't care, but the code doesn't work correctly.
Reason number 4. You cannot pass [] struct as [] interface
The interfaces are cool, Pike & Co. they continue to say that they are what Go is: interfaces solve the problem of generics, we use interfaces for tests, through them polymorphism is built in Go. I tell you, I loved the interfaces while reading Effective Go and I continue to love them. Nevertheless, in addition to the problem “this zero interface is not completely zero”, which I spoke about a little earlier, there is another unpleasant problem that will make me think that Go does not have full support for interfaces . In simple words, you simply cannot pass a slice of structures (that satisfy a particular interface) to a function that accepts a slice of that interface:
package main
import (
"fmt"
"strconv"
)
type FancyInt int
func (x FancyInt) String() string {
return strconv.Itoa(int(x))
}
type FancyRune rune
func (x FancyRune) String() string {
return string(x)
}
// Удовлетворяет любой структуре с методом String().
type Stringy interface {
String() string
}
// Строка, состоящая из строковых представлений всех элементов.
func Join(items []Stringy) (joined string) {
for _, item := range items {
joined += item.String()
}
return
}
func main() {
numbers := []FancyInt{1, 2, 3, 4, 5}
runes := []FancyRune{'a', 'b', 'c'}
// Такое не прокатит!
//
// fmt.Println(Join(numbers))
// fmt.Println(Join(runes))
//
// prog.go:40: cannot use numbers (type []FancyInt) as type []Stringy in argument to Join
// prog.go:41: cannot use runes (type []FancyRune) as type []Stringy in argument to Join
//
// Взамен они предлагают нам заниматься вот такой циркопляской:
//
properNumbers := make([]Stringy, len(numbers))
for i, number := range numbers {
properNumbers[i] = number
}
properRunes := make([]Stringy, len(runes))
for i, r := range runes {
properRunes[i] = r
}
fmt.Println(Join(properNumbers))
fmt.Println(Join(properRunes))
}
It is not surprising that this is a known problem that is not even considered a problem. This is just another fun thing about Go, okay? I highly recommend reading the related Wiki article on the topic, it will become clear why the trick of passing the [] struct as the [] interface will not work. On the other hand, think about it! We can do this, there is no magic, this is just a compiler problem. Look, right here, a little higher in the code, I did it manually. Why can't the Go compiler do this big top for me? Yes, explicit is better than implicit, but still?
I just can’t stand the way people look at shit like this, which is simply full in the language and continue to say "well, yes, that's normal." This is not the norm. This is what makes Go an awful programming language.
Reason number 5. Non-obvious “by value” cycles
UPD: This is a somewhat far-fetched problem when viewed from the side of the language. And if you don’t look, it’s all the same, because we are talking about schools of the language itself, and not of documentation and ecosystems.
This is generally the first problem I have ever encountered in Go. Okay, here we have a for-range loop that lets you walk through slices and listen to channels. It is used everywhere and it’s okay. Here is another minor, but not very pleasant problem that most inexperienced neophytes face: the range loop supports iteration only “by value” , it just copies the values and you can do nothing about it - this is not foreach for you from C + +.
package main
import "fmt"
func main() {
numbers := []int{0, 1, 2, 3, 4}
for _, number := range numbers {
number++
}
fmt.Println(numbers) // [0 1 2 3 4]
for i, _ := range numbers {
numbers[i]++
}
fmt.Println(numbers) // [1 2 3 4 5]
}
Please note that I do not swear that Go does not have range-loops "by reference", I swear that these range-loops are rather unobvious. The verb “range” seems to say “walk through the elements”, but does not really say “walk through the copies of the elements”. Let's look at the For section of “Effective Go”, it doesn’t say a word about the fact that range loops copy the values of a slice, it just isn't written about it. I agree that this is a very insignificant problem, in addition, at one time I managed to get over it very quickly (within minutes), but an inexperienced gopher can spend some time debugging a bunch of code, not understanding why the values in the array do not change. It would be necessary at least a little, but to outline this case in “Effective Go”.
Reason # 6. Doubtful compiler rigor
As I could say earlier, Go is considered to be a clean, simple and readable programming language with a strict compiler. For example, you cannot compile a program with unused import. Why? Just because Mr. Pike thinks it should be. You can believe it, but you can not, but unused import is far from the end of the world and people can survive it, especially during active debugging. I completely agree that this is not very correct and the compiler should just give some kind of warning, but for heaven’s sake, what is the point of stopping the compilation because of such a trifle? Unused imports, seriously?
Go1.5 got us a cool changeLanguage: Finally, you can specify dictionary entries without explicitly specifying the type of stored value. It took the boys from the Go team a little over five years to figure out that an explicit type indication might be a little redundant.
Another great thing I'm going to go to from Go is commas. You see, in Go we have the so-called multi-line declaration blocks (import / var / const):
import (
"fmt"
"math"
"github.com/some_guy/fancy"
)
const (
One int = iota
Two
Three
)
var (
VarName int = 35
)
Okay, but as soon as it comes to “readability,” Rob Pike decides that he needs to suddenly add commas. At some point after adding commas, he decides that it is worth leaving a comma on the last line of the list. Thus, instead of writing like this:
numbers := []Object{
Object{"bla bla", 42}
Object("hahauha", 69}
}
We are forced to write like this:
numbers := []Object{
Object{"bla bla", 42},
Object("hahauha", 69},
}
I still wonder why we can’t just do without commas on import / var / const blocks and certainly not on lists or dictionaries. In any case, Rob Pike definitely fumbles better than me , so everything is OK. Long live the readability!
Reason number 7. Go code generation is just a crutch
Firstly, I have nothing against code generation. For a poor language like Go, this is probably the only way to avoid copy-paste for generic pieces. In any case, go: generate - the tool for code generation in Go, which is used by gophers around the world, simply sucks. Well, to be completely honest, the tool itself is ok, I like it. The problem is not in tools, but in the approach itself. Look, in order to generate some kind of code, you need to use a special magic comment. Yes, a magic sequence of bytes somewhere in the comments of your code can generate code! Just look at the spectacular big top we see below:
func Blabla() {
// code...
}
//go:generate toolname -params -blabla
// code...
Comments should explain the code, not generate it. In any case, magical commentary is an ordinary working practice in modern Go. It’s interesting, but everyone doesn’t care, everyone is happy with everything, it’s okay . It seems to me personally that this is a lot more evil than the damn unused imports that are felling the compilation.
Epilogue
As you can see, I did not swear about generalizations / error handling / sugar and other classic issues that are talked about in the context of Go. I agree that generics are not mastheads at all, but then give us a normal generation, not magic circuses in the comments, which sort of generate code. If you take away the exceptions from us, then please give us the opportunity to normally check the zero interfaces with zero! And if you take away “ambiguous in terms of readability” sugar, then give us the opportunity to write code that works as expected, without a “hop” of hiding variables.
Anyway, I will continue to use Go. There is a good reason for this: I love him. I hate language: it is just disgusting, but I adore the community, tools and the lion's share of the concepts that language offers, the whole ecosystem.
Pssss, man, you wanna fork Go?