DEV Community

jefferson otoni lima
jefferson otoni lima

Posted on

GoLang — Simplifying Complexity “The Beginning”

There are countless programming languages ​​and each one was born with a purpose: “solve problems”. Languages ​​are tools and we will have to know how to use them at the right time. Speaking “as a developer” the more polyglot you can be the better it will be for your professional career and for a better understanding and understanding of the diversity of this ecosystem.

The goal of this post is to introduce what the Go language is and why it is so powerful. Introducing some concepts and important points about the Go language that are often mistakenly expressed in several articles found on the internet and discussion groups of programming languages. I believe that the weaknesses and problems generated by several other programming languages ​​over these decades made Go become what it is today. And to explain how this is possible we will go back to the beginning of everything, where everything began.

package main

func main() {
    println("Hello all folks!")
}
Enter fullscreen mode Exit fullscreen mode

Less is exponentially More

The Go development team says that its creation is an attempt to make programmers more productive. Improving the software development process on Google. The first goal was to create a better language to address the challenges of scalable concurrency, ie software that handles many concerns simultaneously, an example would be coordinating a thousand back-end servers sending network traffic all the time.

Keeping the Go language small allows for more important goals. Being small makes Go easier to learn, easier to understand, easier to implement, easier to re-implement, easier to ** debug**, easier to tweak, and easier to evolve. Doing less allows for more. It's an expression used by the Go development team: “Do Less. Enable More” would be the balance between the universe of existing problems and what Go can help solve such problems well. Go explicitly wasn't designed to solve every problem instead they've done enough for us to create our own custom solutions with ease, but making it very clear that Go can't do everything.

Go was designed by Google in 2007 to improve programming productivity in an era of multicore, network machines and large codebases. The designers wanted to address criticisms of other languages ​​in use at Google, but keep their useful features.

The creators Rob Pike, Ken Thompson and [Robert Griesemer](https:/ /de.wikipedia.org/wiki/Robert_Griesemer) kept the syntax of Go similar to C. In late 2008 Russ Cox joined the team and helped move the language and libraries from prototype to reality.

The Go language was released in 2009 with the purpose of facilitating problem-solving in areas such as network layers, scalability, performance, productivity, and most importantly, concurrency. Rob Pike himself stated that "Go was designed to address a set of software engineering problems we faced when building large server software".

Go was influenced by various programming languages and different paradigms, including Alef, APL, BCPL, C, CSP, Limbo, Modula, Newsqueak, Oberon, occam, Pascal, Smalltalk, and Crystal. It is evident that they used the best of what they had and created something new and lean, with the minimum necessary to solve the proposed problems without losing its simplicity. I believe this can be called innovation. Go innovated by breaking the paradigms of programming languages and implementing something new in a simple and very powerful way.

Controversies

Several features that are present in other modern programming languages were deliberately left out in Go, going against the trend. These features include immutability declarations, pattern-matching data types, generics, exception handling, inheritance, method overloading, and many other points. All of these features were criticized and pointed out to the creators of Go, who responded: "These omissions were simplifications that contribute to Go's strength." There are many things in the Go language and libraries that differ from modern practices, it was a decision made by the Go development team: "Sometimes it's worth trying a different approach."

Everyone is invited to help and contribute if they wish, they can submit proposals for new features and anything related to the language's ecosystem. The Go source code is available on GitHub. The documentation on how you can contribute to the language is provided here.

Goroutines instead of threads

One of the main goals of the Go programming language is to make concurrency simpler, faster, and more efficient.

Goroutines are lightweight and take advantage of all available processing power. Goroutines exist only in the Go runtime virtual space and not in the operating system.

A goroutine is a method/function that can be executed independently along with other goroutines. Each concurrent activity in the Go language is generally called a goroutine.

One of the most relevant and important points is the work with concurrency, which innovated by breaking the traditional model of threads and its usage by creating a new model, the goroutines, in Go. Goroutines are responsible for performing asynchronous executions in Go. They are very powerful and a simple machine with 1GB of RAM could run thousands of them.

Goroutines are part of making concurrency easy to use. Goroutines can be very cheap: they have little overhead beyond memory for the stack, which is only a few kilobytes. To make stacks small, the Go runtime uses resizable and bounded stacks. A newly launched goroutine gets a few kilobytes, which is almost always enough. When it's not, the runtime increases (and shrinks) the memory to store the stack automatically, allowing many goroutines to live in a modest amount of memory. The CPU overhead averages three cheap instructions per function call. It is practical to create hundreds of thousands of goroutines in the same address space. If goroutines were just threads, system resources would run out at a much smaller number.

The design of Go was heavily influenced by C.A.R. Hoare's 1978 article "Communicating sequential processes".

Simultaneity in CSP Ideas

One of the most successful models for providing high-level language support for concurrency is Hoare's Communicating Sequential Processes or CSP. Occam and Erlang are two well-known languages that derive from CSP. Go's concurrency primitives derive from a different part of the genealogical tree whose main contribution is the powerful notion of channels as first-class objects. Experience with several earlier languages showed that the CSP model fits well into a procedural language framework.

The biggest difference between Go and the CSP model, besides syntax, is that Go models communication channels explicitly as channels, while the processes of Hoare's language send messages directly to each other, similar to Erlang.

In order to achieve this result, Go has once again received criticism for the adopted model. Go incorporates a variant of CSP (Communicating Sequential Processes), a formal language for describing interaction patterns in concurrent systems with first-class channels. A single-write approach was not adopted to enhance the semantics in the context of concurrent computing, as is done in Erlang, instead, they adopted something practical that resulted in something powerful, allowing for simple and safe concurrent programming, but does not prevent incorrect programming. And the created motto was: "Do not communicate by sharing memory, share memory by communicating". The simplicity and support for concurrency offered by Go generate robustness.

Concurrency is different from Parallelism

One of the famous phrases is: "Concurrency is about dealing with many things at once. Parallelism is doing many things at once." Concurrency is the composition of independent execution calculations. Concurrency is a way of structuring software and is definitely not parallelism, although it allows for parallelism. If you only have one processor, your program can still be concurrent, but it cannot be parallel. On the other hand, a well-written concurrent program can be efficiently executed in parallel on a multiprocessor. I suggest checking out this talk by Rob Pike, "Concurrency is not Parallelism".

Concurrency in Go is very powerful and also simple to code, which was the intention of the engineers who developed Go. Solving many problems is much more efficient using concurrency, and this is the power of Go, which is why it has become a deity when it comes to concurrency. Therefore, problems encompassed in this universe will be solved very efficiently and, most importantly, with very few computational resources.

When we talk about concurrency, we need to highlight that there are some patterns, which I describe below, and there are some types of workloads that will serve as our compass to determine our balance point when we have to solve problems involving concurrency.

Workloads: CPU-Bound & IO-Bound

A thread can perform two types of workloads (Workloads): CPU-Bound and IO-Bound.

CPU-Bound: This workload will never create a situation where the thread can be put into waiting states. It will be constantly performing calculations. A thread calculating pi to the eighteenth power would be limited by the CPU. (Fun fact: Emma Haruka Iwao is a Japanese computer scientist and cloud developer engineer at Google. In 2019, Haruka Iwao calculated the most accurate value of pi in the world, which included 31.4 trillion digits). This type of workload, CPU-bound work, requires parallelism to take advantage of concurrency. More Goroutines will not help and will not be efficient, potentially further delaying the workloads to be executed, due to the latency cost (the time spent) of moving Goroutines in and out of the operating system's thread.

IO-Bound: In this type of workload, threads enter waiting states. A good example would be a request for accessing a resource over the network or making calls to the operating system. A thread that needs to access a database, synchronization events (mutex, atomic), all of these examples make the thread wait, so we could say they are of the IO-Bound type of workload. In this type of workload, you do not need parallelism to use concurrency, a single physical core would be sufficient for the proper execution of several Goroutines. The Goroutines are entering and exiting waiting states as part of their workload. In this type of workload, having more Goroutines than physical cores can speed up execution because the latency cost of moving Goroutines in and out of the operating system's thread is not creating an event. Its workload is naturally interrupted, allowing a different Goroutine to take advantage of the same physical core instead of allowing it to remain idle.

Goroutine and Pattern

There are some patterns for concurrency, among them: Fan-In and Fan-Out.

Fan-Out: Multiple functions can read from the same channel until that channel is closed, this is called fan-out. This provides a way to distribute work among a group of workers to make use of CPU and I/O in a concurrent manner.

Fan-In: It is the way to read from multiple inputs and proceed until all of them are closed, multiplexing the input channels into a single channel that is closed when all the inputs are closed. This is called fan-in.

Pipeline: There is no formal definition of a pipeline in Go, it is just one of the many types of concurrent programs. Informally, a pipeline is a series of stages connected by channels, where each stage is a group of goroutines running the same function. At each stage, the goroutines:

  • Receive values from upstream via input channels;

  • Perform some function on that data, usually producing new values;

  • Send values downstream via output channels.

Each stage has several input and output channels, except for the first and last stages, which have only input or output channels, respectively.

To give you an idea, on a 1 CPU and 1 GB RAM machine, we can handle 5k to 6k requests per second with a simple Go API, with few lines of code, using either RPC or native TCP protocols and writing to a relational database like PostgreSQL, without crashing it and consuming an average of only 60Mb to 70Mb of memory and 8% to 15% of CPU. To achieve this, we use a "Worker Poll" that is a Fan-out.

If you want to delve further into concurrency, you can check out this link. If you want to test and see some examples, you can click here. We'll create a separate post to talk about Goroutines, which is a very detailed and interesting topic. We know that Go's support for concurrency makes it a godsend for problems that are best solved using concurrency.

Go is a compiled language, a C-like language, very focused on productivity. For simple or complex projects, Go will always be an excellent alternative, as it makes everything simpler and leaner, easy to code and maintain.

package main

import "fmt"

func main() {
    fmt.Printf("Hello, I love Go!\n")
}
Enter fullscreen mode Exit fullscreen mode

There is no silver bullet

There is no solution or tool to solve all problems yet, of course. What we know is that there are countless solutions developed in various languages, each with a purpose and each with its own characteristics. For this reason, there are programming paradigms, which is a way of classifying languages based on their functionalities, making it easier to understand the context in which they will be better inserted, and, of course, giving us a view of their structure and execution of the languages.

It is becoming increasingly common for languages to support multiple paradigms. We know that there are different classes in computing when it comes to solving problems, and the closer we are to these subjects, the more comfortable we will be to choose a certain language to solve problems, and thus know which tool to use at the right time. In practice, this task is not so easy, but what we know is that the silver bullet has not yet been created to solve all problems at once with just one language, if it existed, there would no longer be P versus NP and all problems could be solved in polynomial time.

The Good Things About Go

  • It does not have generics;

  • It does not have operator overloading;

  • It does not have pointer arithmetic;

  • Thread management is not explicitly managed;

  • It has type inference;

  • Dependencies are clear;

  • The syntax is clean;

  • The semantics are clear;

  • Composition over inheritance;

  • Native GC (Garbage collector);

  • It has Goroutines;

  • Native support for concurrent programming;

  • First-class functions (we can pass functions as parameters);

  • Cross-platform;

  • Open Source;

  • Very low learning curve, meaning it is easy to learn;

  • Highly productive;

  • Solves scalability problems;

  • It can drastically reduce server costs;

  • Various problems thought from the Go perspective become much easier to handle and solve;

  • It has only 25 reserved keywords;

  • Multi-paradigm: concurrent, functional, imperative, object-oriented;

  • Memory Safe.

We know that various points above will be criticized, but in practice and over time, we realized that all the omitted simplifications made Go an even more powerful language.

When we didn't have Generics

When we didn't have Generics There are strong reasons why Go didn't implement Generics early in the project, and there's a discussion in the general communities about this topic. Go left out several features, including “Generics”, which in its conception would directly compromise the performance and purpose that the Go language was intended to solve.

Complainants are mainly developers who come from languages that use this feature. On November 29, 2018, the Go source code maintainers opened up the possibility to submit proposals for new features with all the details so that they can analyze them and possibly implement them in the Go 2.0 version, but we know that this happened before in March 2022 version 1.18 was released. To get a good idea, read this file by by clicking here.

In Go 1.18, the language introduced a new feature called generic types that had been on the wish list of Go developers for some time. In programming, a generic type is a type that can be used in conjunction with many other types. Normally in Go, if you want to use two different types for the same variable, you need to use a specific interface, like io.Reader, or use interface{}, which allows you to use any value. Using the {} interface can make working with these types difficult because you have to translate between several other possible types to interact with them. Using generic types allows you to interact directly with your types, leading to cleaner, easier-to-read code.

Example:

Imagine a method that receives an array of type int and prints it on the screen.

Imagine now needing to do the same with the String type and the float type, you would have to create new methods or functions several times, repeating the code.

Solution: To avoid creating several methods, only one method is created as a generic to be able to receive any type.

type iMyinterface interface {
    int | int64 | int32
    ~float64
    ~string
    ~[]byte
    MyMethod()
}
Enter fullscreen mode Exit fullscreen mode
func NoGenericFuncStrs(a, b []string) bool
func NoGenericFuncInts(a, b []int) bool
func NoGenericInterface(a, b interface{}) bool
Enter fullscreen mode Exit fullscreen mode
func GenericsFuncStrs[T comparable](a, b []T) bool
Enter fullscreen mode Exit fullscreen mode

Note that this is only a problem in languages that need to declare types of variables or functions, etc. In dynamic languages like PHP, for example, this is not necessary. I won't say strongly or weakly typed languages because it's a bit more complex than that, and that would lead to another discussion.

In Go, we can solve all problems of this nature using reflect and other interfaces and now we have Generics to make our work even easier 😎.

Below are three examples with three proposals to solve the problem in this way, and you will be able to understand when we talk about Generics. 😊

Copying and Pasting Functions with Specific Types

package main

// Takes a slice of any type and reverses it.
func Reverses(list []string) []string {
    i := 0
    j := len(list) - 1
    for i < j {
        list[i], list[j] = list[j], list[i]
        i++
        j--
    }
    return list
}

// Takes a slice of any type and reverses it.
func Reversei(list []int) []int {
    i := 0
    j := len(list) - 1
    for i < j {
        list[i], list[j] = list[j], list[i]
        i++
        j--
    }
    return list
}

func main() {
    s := []string{"otoni","jeff","Go"}
    Reverses(s)
    println(s[0], s[1], s[2])

    i := []int{2020,2012,2009}  
    Reversei(i) 
    println(i[0], i[1], i[2])
}
Enter fullscreen mode Exit fullscreen mode

Using Interface

package main

// Takes a slice of any type and reverses it.
func Reverse(in interface{}) interface{} {
    switch in.(type) {
        case []string:
        list := []string(in.([]string))
        i := 0
        j := len(list) - 1
        for i < j {
               list[i], list[j] = list[j], list[i]
           i++
           j--
            }
            return list

         case []int:
        list := []int(in.([]int))
        i := 0
        j := len(list) - 1
        for i < j {
           list[i], list[j] = list[j], list[i]
           i++
           j--
            }
            return list
    }
    return nil
}

func main() {
    s := []string{"otoni","jeff","Go"}
    Reverse(s)
    println(s[0], s[1], s[2])

    i := []int{2020,2012,2009}  
    Reverse(i)  
    println(i[0], i[1], i[2])
}
Enter fullscreen mode Exit fullscreen mode

Using Generics

// https://go2goplay.golang.org/
package main

import (
    "fmt"
)

// The playground now supports parentheses or square brackets (only one at
// a time) for generic type and function declarations and instantiations.
// By default, parentheses are expected. To switch to square brackets,
// the first generic declaration in the source must use square brackets.

func Print[type T] (s []T){
    for _, v := range s {
        fmt.Print(v)
    }
    fmt.Println("")
}

func Reverse[type T](list []T) {
    i := 0
    j := len(list) - 1
    for i < j {
        list[i], list[j] = list[j], list[i]
        i++
        j--
    }
}

func main() {
        var i []interface{}
        i = append(i, "jeffotoni")
        i = append(i, " ano de 2020 ")
        i = append(i, []string{"otoni", "jeff", "Go"})
    i = append(i, []int{2020, 2012, 2009})
        Print(i)

    s := []string{"otoni", "jeff", "Go"}
    Reverse(s)
    Print(s)

    intt := []int{2020, 2012, 2009}
    Reverse(intt)
    Print(intt)
}
Enter fullscreen mode Exit fullscreen mode

Composition over Inheritance

The approach adopted by Go departs from the norm and is unusual for object-oriented programming, allowing methods of any type, not just classes, but without any form of type-based inheritance, such as subclasses. This means that there is no type hierarchy. This is not a bug, not a disadvantage, nor an error, but an intentional design choice by the developers of Go. Although type hierarchies have been used to build successful software, it is the stance and opinion of the Go development team, that they preferred to take a step back instead of doing the same repetitive model in various languages.

Interfaces are implicit

Instead, Go has interfaces, which are just a set of methods and nothing more.

Example:

go
Enter fullscreen mode Exit fullscreen mode
type Geometric interface {
  area() float64
  perim() float64
}
Enter fullscreen mode Exit fullscreen mode

All data types that implement these methods satisfy this interface implicitly, there is no "implements" in the declaration. Interface satisfaction is checked statically at compile time, therefore, despite the decoupling interfaces, they are type-safe.

A type usually satisfies many interfaces, each corresponding to a subset of its methods. This fluidity of interface satisfaction encourages a different approach to software construction, breaking the existing paradigm.

Why aren't interfaces explicit?

Object-oriented programming provides a powerful view: that the behavior of data can be generalized independently of the representation of that data. The model works best when a behavior (set of methods) is fixed, but once you insert a subclass into a type and add a method, the behaviors are no longer identical. If instead, the set of behaviors is fixed, as in interfaces statically defined in Go, the uniformity of behavior allows data and programs to be composed uniformly, orthogonally, and safely. Type hierarchies result in fragile code. Go stimulates composition and not inheritance, using simple interfaces, usually of one method, to define trivial behaviors that serve as clear and understandable boundaries between components and eliminating the type hierarchy also eliminates one form of dependency hierarchy. The designs are not like hierarchical methods inherited from subtypes. They are more loose, organic, decoupled, independent and therefore scalable.

go
Enter fullscreen mode Exit fullscreen mode
package main

import "fmt"
import "math"

type Geometric interface {
    area() float64
    perim() float64
}

type rect struct {
    width, height float64
}

type circle struct {
    radius float64
}

func (r rect) area() float64 {
    return r.width * r.height
}

func (r rect) perim() float64 {
    return 2*r.width + 2*r.height
}

func (c circle) area() float64 {
    return math.Pi * c.radius * c.radius
}

func (c circle) perim() float64 {
    return 2 * math.Pi * c.radius
}

func measure(g geometry) {
    fmt.Println(g)
    fmt.Println(g.area())
    fmt.Println(g.perim())
}

func main() {
    r := rect{width: 3, height: 4}
    c := circle{radius: 5}

    measure(r)
    measure(c)
}
Enter fullscreen mode Exit fullscreen mode

Duck typing

Go supports "Duck typing", a typing style in which the methods and properties of an object determine the valid semantics, rather than its inheritance from a particular class or implementation of an explicit interface. The concept's name refers to the duck test, attributed to James Whitcomb Riley. This makes Go look like a dynamic language.

Go uses "Structural Typing" in methods to determine the compatibility of a type with an interface. There are no type hierarchies, and "Structural Typing" is an interesting alternative to classic inheritance in statically typed languages. It allows you to write generic algorithms without obscuring the definition of a type in a sea of interfaces. Perhaps more importantly, it helps statically typed languages capture the feel and productivity of dynamically typed languages.

"Duck typing" occurs at runtime and "Structural Typing" occurs at compile time. Go does not support object orientation as implemented in languages like C#, Java, or even C++, it has no inheritance and honestly, I am very happy with this, but it does offer some features such as composition and interfaces.

go
Enter fullscreen mode Exit fullscreen mode
package main

import (
    "fmt"
)

type family interface {
    dados() string
}

type pai struct {
    Nome  string
    Idade int
    Cpf   string `json:"campo-x"`
}

func (p pai) dados() string {
    return fmt.Sprintf("Nome: %s, Idade: %d", p.Nome, p.Idade)
}

type filho struct {
    pai
    email string
}

func (f filho) dados() string {
    return fmt.Sprintf("Nome: %s, Idade: %d, Email: %s", f.Nome, f.Idade, f.email)
}

func mostraDados(membro familia) {
    fmt.Println(membro.dados())
}
func main() {
    pai := new(pai)
    pai.Nome = "Jeff"
    pai.Idade = 50
    pai.Cpf = "00.xxx.xxx-xx"

    filho := new(filho)
    filho.Nome = "Arthur"
    filho.Idade = 20
    filho.email = "arthur@gmail.com"

    mostraDados(pai)
    mostraDados(filho)
 }
Enter fullscreen mode Exit fullscreen mode

Strong third-party library support

In the current scenario, Go has taken off as a programming language and all major, medium, and small existing players are developing their libraries, SDKs, clients, etc. in the Go language. There are thousands of initiatives and many third-party libraries. Everything contributes to this, the Go community is very strong, it grows every day, it has a very high acceptance curve, and with this, it facilitates its entry into companies and its use by developers.

The result of this is mass dissemination on the internet with libraries written in Go. There are also libraries that are abandoned, as in any other language, but functional. But the number of libraries written in Go is very, very large. Several companies are rewriting their platforms in Go, which is a trend that continues to grow. And day by day the number of tools for DevOps grows, and with this, the acceptance and increase of new adherents to Go grows in the DevOps culture and community.

After the implementation of version control VGO in Go, we now have the possibility of not needing our projects to be under $GOPATH, we can now create our projects in any directory outside $GOPATH. This feature is only possible due to version control, when we use "go mod" our project workspaces are controlled in a new way, just run the command "go mod init yourapp" and a file named "go.mod" will be created containing the location of your path and the version in which it is located. Very simple and beautiful. Every time we run our Go project, we can use the environment variable "GO111MODULE=yes|no" to determine whether we will use "go mod" or not.

Garbage collector (GC)

GCs are complex pieces of software, decades of research and implementations, and various attempts by computer scientists to make an improved and reliable GC. It is not a simple task, and due to this, there is a huge chain on the internet describing the pros and cons of a GC. What we have to take into consideration is that regardless of the result, if we didn't have a GC, we would have to build one to solve the allocation problem.

There are usually two ways to allocate memory in Go: on the stack and on the heap. Generally, developers are familiar with the stack, just write a recursive program and you will notice a stack overflow if you don't do it correctly. The heap, on the other hand, is a pool of memory that can be used for dynamic allocation.

Stack allocations are great because they only live during the lifetime of the function to which they belong. Heap allocations, however, will not be deallocated automatically when they go out of scope. To avoid the heap becoming dissociated, we must deallocate it explicitly or, in the case of memory-managed programming languages (like Go), rely on the garbage collector to locate and delete unreferenced objects.

In general, in languages with a GC, the more you can store on the stack, the better, since these allocations are never seen by the GC. Compilers use a technique called escape analysis to determine whether something can be allocated on the stack or should be placed on the heap.

In practice, writing programs that force the compiler to allocate only on the stack can be very limiting, and thus, in Go, we take advantage of its wonderful GC to do the work of keeping our heap clean.

If you want to take a look and delve deeper into the subject, check out this link written by Richard L. Hudson: https://blog.golang.org/ismmkeynote.

Some types of applications implemented in Go

Go is a general-purpose language. In this link, you can see various applications implemented in Go and their respective categories.

Some types:

. Web backend (with various frameworks available)

. Web Assembly (one of them is vugu framework)

. Microservices (some frameworks: Go Micro, Go Kit, Gizmo, Kite)

. Fragments services (Term mentioned by @jeffotoni in a microservices discussion group)

. Lambdas (FaaS example)

. Client Server

. Terminal applications (using the tview lib)

. IoT (some frameworks)

. Bots (some here)

. Client Applications using Web technology

. Desktop using Qt+QML, Native Win Lib (example Qt, Qt widgets, Qml)

. Network Applications

. Protocol applications

. REST Applications

. SOAP Applications

. GraphQL Applications

. RPC Applications

. TCP Applications

. gRPC Applications

. WebSocket Applications

. GopherJS (compiles Go to JavaScript)

Top comments (1)

Collapse
 
artydev profile image
artydev

Thank you