If you are thinking of starting to write on Go, this is what you should know.

    Your favorite pet writes on Go and gets more than you, but you are not there yet? Do not waste time ... Such a thought could be born to the reader from the abundance of articles on Go. Some companies even offer to retrain in this language. And, if you ever thought to master a language, then I want to warn you. Rather, show the strange things, try to explain why they are and then you will make your own conclusion whether you need Go.

    Go is a portable C


    Who is this article for?


    The article is primarily intended for those people for whom the expressiveness of the language is important. And at the same time for those who want to touch Go.
    I myself am a C ++ / Python developer and I can say that this combination is one of the best for mastering Go. And that's why:

    • Go is very often used to write backend services and very rarely for everything else. There are two more popular pairs for the same: Java / C # and Python / Ruby. Go, in my opinion, is aimed precisely at taking a share from the Python / Ruby pair.
    • Go inherits its strange behavior precisely from the nuances of the C syntax, which are not clearly hidden in the language. Since there are clear points of rejection in Go to such an extent that sometimes you want to remove the Go compiler and forget, understanding C principles and the fact that Go is in a sense a superset of C allows them to be significantly smoothed out.


    What about the Java / C # pair? Go to her is never a competitor, at least as long as he is young (talking about the Go 1.11 version).

    What will not be in the article


    • We will not say that Go is bad, because it does not have features X, as in the language Y. Each language has its own rules of the game, its own approaches and its fans. Although I am deceiving whom, of course we will have to talk about it.
    • We will not compare directly interpreted and compiled languages.


    What will happen? Only specific cases of discomfort that the language delivers in the work.

    Beginning of work


    A good introduction to the language manual is a short online book, Introduction to Go Programming . Reading that you rather quickly stumble upon strange features. First, we give the first batch of them:

    Odd compiler


    Only Egyptian brackets are supported.
    Only Egyptian brackets are supported , that is, the following code does not compile:
    package main
    funcmain()  // Не компилируется
    {
    }
    

    The authors believe that the programming style should be uniform and compact. Well, the master is the master.

    Multiline transfers must end with a comma.
    a := []string{
    	"q"// Нет запятой, не компилируется
    }
    

    Apparently here fear pull requests, where there will be a change in two lines when adding one line to the end. In fact, this is done specifically to facilitate the writing of third-party tools that code.

    Didn't use a variable? Not compiled!
    No, this is not a joke.
    package main
    funcmain() {
    	a := []string{
    		"q",
    	}
    	// Не компилируется, переменная не использована
    }
    


    Здесь упор идёт на то, что почти всегда это ошибка, связанная или с опечаткой, или спешкой, или кривым рефакторингом. Как бы в конечном коде да, такого быть не должно. Но мы редко пишем сразу конечный код и периодически пробуем запускать промежуточные версии, в которых может быть некоторый задел на будущее. Поэтому данное поведение компилятора напрягает.
    Правда со временем возникает множество ситуаций, когда это уберегло от ошибки. Но это всё-равно напрягает.

    Неиспользуемые параметры приходится заглушать и это смотрится странно, хотя в питоне так тоже можно:
    for _, value := range x {
        total += value
    }
    



    But these are all flowers and even just tastes of developers. We now turn to more heavy things.

    "Safe" language


    And here we must not forget to say a very important thing. The fact is that the language is made precisely so that inexperienced developers are not able to create bad programs.

    Here is a quote from one of the creators of the language:
    “The key point here is that our programmers (comment: googlers) are not researchers. As a rule, they are very young, they come to us after their studies, they have probably studied Java, or C / C ++, or Python. They are not able to understand the outstanding language, but at the same time we want them to create good software. That is why the language should be easy to understand and learn. ”

    Spionereno from here: Why Go design is bad for smart programmers .

    So you speak a safe language?
    var x map[string]int
    x["key"] = 10

    and after starting the program we get:
    panic: runtime error: assignment to entry in nil map
    


    In this innocent example, we “forgot” to allocate memory for ourselves and got a runtime error. So what kind of security can we talk about if you didn’t save me from the wrong manual work of allocating resources?
    Habrayuzer tyderh notes that:
    Security is that when executing, an error is caught, and no undefined behavior occurs that can arbitrarily change the course of the program. Thus, such errors programmers are not able to lead to the appearance of vulnerabilities.


    The following example:
    var i32 int32 = 0var i64 int64 = 0if i64 == i32 {
      }
    

    It will cause a compilation error that seems to be normal. But since there are no templates in Go yet (so far!), They are often emulated via interfaces, which may sooner or later turn into this code:
    package main
    import (
    	"fmt"
    )
    funceq(val1 interface{}, val2 interface{})bool {
    	return val1 == val2
    }
    funcmain() {
    	var i32 int32 = 0var i64 int64 = 0var in int = 0
    	fmt.Println(eq(i32, i64))
    	fmt.Println(eq(i32, in))
    	fmt.Println(eq(in, i64))
    }
    

    This code is already compiled and works, but not as expected by the programmer. All three comparisons will give out false, because the interface type is compared first, but it is different. And if in this case the error is clearly conspicuous, in reality it can be very blurred.

    powerman shared another example of false expectations:
    funcreturnsError(t bool)error {
    	var p *MyError = nilif t {
    		p = ErrBad
    	}
    	return p // Will always return a non-nil error.
    }
    err := returnsError(false)
    if err != nil {
      # Истина
    }
    

    The interface with nil is not equal to just nil, be careful. In the FAQ language this moment is .

    Well, completing the security. The dereferencing in the language was removed, but the special effects, depending on the type of access from access (by pointer or copy) remained. Therefore the following code:
    package main
    import"fmt"type storage struct {
    	name string
    }
    var m map[string]storage
    funcmain() {
    	m = make(map[string]storage)
    	m["pen"] = storage{name: "pen"}
    	if data, ok := m["pen"]; ok {
    		data.name = "-deleted-"
    	}
    	fmt.Println(m["pen"].name) // Output: pen
    }
    

    Print pen . And the following:
    package main
    import"fmt"type storage struct {
    	name string
    }
    var m map[string]*storage
    funcmain() {
    	m = make(map[string]*storage)
    	m["pen"] = &storage{name: "pen"}
    	if data, ok := m["pen"]; ok {
    		data.name = "-deleted-"
    	}
    	fmt.Println(m["pen"].name) // Output: -deleted-
    }
    

    It will output "-deleted-", but please do not scold much programmers when they come to this rake, they were not saved from this in the "safe" language.
    What is the difference in these damn pieces?
    В одном примере:
    m = make(map[string]storage)
    а в другом:
    m = make(map[string]*storage)


    Ha, did you think everything? I thought so too, but unexpectedly ran into another rake:
    Step on the rake
    package main
    import"fmt"var globState string = "initial"funcgetState()(string, bool) {
    	return"working", true
    }
    funcini() {
    	globState, ok := getState()
    	if !ok {
    		fmt.Println(globState)
    	}
    }
    funcmain() {
    	ini()
    	fmt.Println("Current state: ", globState)
    }
    

    Возвращает initial и это верно ибо оператор := создаёт новые локальные переменные. А его мы вынуждены были использовать из-за переменной ok. Опять таки всё верно, но изначально строчка
    globState, ok := getState()
    могла выглядеть как
    globState = getState()

    а потом вы решили добавить второй параметр возврата, IDE подсказал вам, что теперь надо его ловить, и вам пришлось попутно заменить оператор и вдруг вы видите грабли перед лицом.

    А это значит, что теперь нам надо у PVS просить статический анализатор для языка Go.

    Краткий вывод: безопасность присутствует, но она не абсолютна от всего.


    "Uniform" language


    Above, in the strangeness section of the compiler, it was stated that if the code is formatted incorrectly, the compiler will fall. I assumed that this was done for uniformity of code. Let's see how the code is uniform.
    Here, for example, two ways to allocate memory:
    make([]int, 50, 100)
    new([100]int)[0:50]
    

    Well, yes, yes, it's just a feature of the function new, which few people use. Okay, we will consider this not critical.

    For example, two ways to create a variable:
    var i int = 3
    j := 6

    Okay, okay, var is used less often and mostly for backup under a certain type name or for global namespace variables.

    Well, with a stretch we will assume Go to be a uniform language.

    "Sausage" code


    And here's another common problem, the construction of the form:
    result, err := function()
    if err != nil {
        // ...
    }
    

    This is a typical piece of code on Go, let's call it conditionally sausage. The average Go code consists of half of such sausages. In this case, the first sausage is made as result, err: = function () , and all subsequent sausages are result, err = function () . And this would not be a problem if the code was written only once. But the code is a living thing and constantly has to swap sausages or drag part of the sausages to another place and this forces the operator to constantly change : = to = and vice versa, which strains.

    Compact Language


    When you read a book on Go, you never cease to be amazed at the compactness, it seems that all the designs are thought out so that the code takes up as little space as possible in height and width. This illusion quickly collapses on the second day of programming.

    And first of all because of the "sausages", which I mentioned a little higher. It is now November 2018 and all Go programmers are awaiting version 2.0, because there will be a new error handling in it , which will finally put an end to sausages in such numbers. I recommend the article at the link above, in it the essence of the problem of the “sausage” code is explained clearly.

    But the new error handling does not eliminate all the problems of compactness. Still will lack constructions in and not in. At the moment, checking the location of a map value looks like this:
    if _, ok := elements["Un"]; ok {
    }
    

    And the only thing you can hope for is that after compilation it will be skipped before just checking the value, without initializing the associated variables.

    Young language and poor syntax


    Go has a lot of written code. And there are just awesome things. But it’s not rare that you choose between a very bad library and just an acceptable one. For example, SQL JOIN in one of the best ORM in GO (gorm) looks like this:
    db.Table("users").Select("users.name, emails.email").Joins("left join emails on emails.user_id = users.id").Scan(&results)
    

    And in another ORM like this:
    query := models.DB.LeftJoin("roles", "roles.id=user_roles.role_id").
      LeftJoin("users u", "u.id=user_roles.user_id").
      Where(`roles.name like ?`, name).Paginate(page, perpage)
    

    What casts doubt on the need to use ORM at all is because there is simply no normal support for protection against renaming fields everywhere. And due to the compiled nature of the language it may not appear.

    And here is one of the best examples of compact routing on the web:
    a.GET("/users/{name}", func(c buffalo.Context)error {
      return c.Render(200, r.String(c.Param("name")))
    })
    

    Not that there was something bad here, but in dynamic languages ​​the code usually looks more expressive.

    Controversial flaws


    Public functions


    Guess how to make the function public for use in other packages? There are two options: either you knew or never would have guessed. Answer: there is no reserved word, you just need to name the function with a capital letter. You vlyapyvaesya exactly once and then you get used to it. But as a Pythonist, I remember about the “explicit better than implicit” rule and would prefer a separate reserved word (although if we recall the double underscore in python, then whose cow would moo).

    Multistory


    If you need a dictionary of objects, then you write something like this:
    elements := map[string]map[string]string{
    		"H": map[string]string{
    			"name":  "Hydrogen",
    			"state": "gas",
    		},
            }
    

    Frightening, isn't it? The eye wants some brackets so as not to stumble. Fortunately, they are possible:
    elements := map[string](map[string]string){
            }
    

    But this is all that will allow you to go go fmt formatter, which will almost certainly be used in your project to reformat code while saving. All other auxiliary spaces will be cut.

    Atomic structures


    They are not. For synchronization, you must explicitly use mutexes and channels. But the “safe language” will not try to prevent you from writing simultaneously from different streams into standard structures and getting the program crashed.
    helgihabr kindly recalled that at 1.9 sync.Map appeared .

    Testing


    In all not very secure languages, security is well implemented through testing with good coverage. In Go, this is almost all right, except for the need to write sausages in the tests:
    if result != 1 {
        t.Fatalf("result is not %v", 1)
        }
    

    Understanding the weakness of this approach, we immediately found a library on the network that implements assert and refined it to a sane state. You can take and use: https://github.com/vizor-games/golang-unittest .

    Now the tests look like this:
    assert.NotEqual(t, result, 1, "invalid result")
    


    Two type conversions


    In language, the essence of the interface has a special status. They are often used to silence the “poverty” of the syntax of the language. Above was an example with the implementation of templates through interfaces and the implicit harmful special effect generated by this case. Here is another example from the same series.
    You can use the usual C-style construction for type conversion:
    string([]byte{'a'})

    But do not try to apply it to the interfaces, because for them the syntax is different:
    y.(io.Reader)

    And it will confuse you for quite a while. I found for myself the following rule to remember.
    The conversion on the left is called conversion , its correctness is checked during compilation, and in theory for constants it can be produced by the compiler itself. Such a conversion is similar to static_cast from C ++.
    The conversion on the right is called type assertion and is performed when the program is executed. Analogue dynamic_cast in C ++.

    Corrected deficiencies


    Batch manager


    vgo is approved , JetBrains GoLand 2018.2 is supported , for the rest of the IDE, the following command will work as a temporary solution:
    vgo mod -vendor

    Yes, it looks like a small crutch on the side, but it works fine and just implements your versioning expectations. Perhaps in go2, this approach will be unique and native.
    In version 1.11, this thing is already built into the language itself. So go the right way comrades.

    Virtues


    After reading the article, there may be an assumption that the supervisor with a whip is standing over us and makes us write on Go, solely for the sake of our suffering. But it is not, in the language there are chips that significantly outweigh all the above disadvantages.
    • A single binaries - most likely your entire project will be compiled into a single binaries, which is very convenient for packaging in a minimalistic container and sent on the template.
    • A native build - rather, the go build command at the root of your project will compile this very single binary. And you do not need to bother with autotools / Makefile. This will be especially appreciated by those who regularly tinker with compiler C errors. The lack of header files is an added advantage that you appreciate every day.
    • Multithreading out of the box - in the language is not just to make multithreading, but very simple. It is so simple that very often just importing a library into a project and using any of its examples can already contain, explicitly or implicitly, work with multithreading in itself and, in the main project, nothing breaks from this.
    • Simple language - the reverse side of the poverty of syntax - the ability to learn a language in 1 day. Not even in 1 day, but in 1 sitting.
    • Fast language - in view of the compiled nature and limited syntax, it will be difficult for you to survive a lot of memory and CPU time in your programs.
    • Strong typing is very nice when the IDE at any time knows the type of the variable and the transition through the code works like a clock. This is not an advantage of Go, but it also has it.
    • Protection against expanding structures - OOP in Go is emulated by structures and methods for structures, but the rule is that it should be in the same file. And this is very good in terms of analyzing someone else's code, Ruby has a pattern of mixing and sometimes the devil breaks a leg.
    • Delayed deinitialization . An example is best illustrated:
      package main
      import (
          "fmt""os""log"
      )
      funcmain() {
          file, err := os.Open("file.txt")
          if err != nil {
              log.Fatal(err)
          }
          defer file.Close()
        b, err := ioutil.ReadAll(file)
        fmt.Print(b)
      }
      
      Thanks
      defer file.Close ()
      we immediately inform runtime that, regardless of how and where the exit from the function will be, at the end it is necessary to execute a certain code. This immediately partially solves the problem with the absence of destructors and almost completely solves the problem of missing contexts (for example, Python's with).


    Why did it happen


    Go looks like a superset of C. A lot says about it: the syntax is similar and the understanding of how this can be easily transformed into C code. Of course, gorutin, garbage collection and interfaces (and with it RTTI) are atypical for C, but the rest of the code is easily converted almost by regulars.
    And this nature, in my opinion, and dictates almost all the above strangeness.

    Summary


    • Go is great for quick writing of low-cost and fast microservices, while any experienced developers from other languages ​​are suitable for this work. It is in this matter that he has few equal.
    • Go young As it was rightly noted by one of the commentators: “The idea for 5, the implementation for 3”. Yes, as a universal language - by three, but purely for microservices by 4. Plus, the language is developing, it is quite possible to correct half of the described shortcomings and it will become significantly better.
    • The first month of work you will fight with the compiler. Then you will understand his character and the struggle will pass. But this month will have to go through. Half of the language haters did not last a month. This must be clearly understood.
    • Fans of STL need to say that while they have to collect with the world on a string. For while there are three available containers , not counting the built-in map and array. The rest will have to be emulated or searched in third-party libraries.


    Test libraries



    What to read



    Also popular now: