Let’s go with the Go programming language

Let’s go
with the Go
programming
language

Programming Language

We’ll share our perspectives on Go, the language of choice for some of Avenga’s innovative products.

In our other articles, we analyzed other promising languages, such as Julia, Rust, and Lua. Here, Jacek Chmiel, Director of Avenga Labs, delves into the Go language, exploring its purpose, applications, and the pros and cons that come with it.

The Go language

Go is an open-source general-purpose language that is currently ranked 9th on the TIOBE index. Since its first release in 2012, Go has seen consistent updates, with significant releases twice a year, ensuring it remains relevant and efficient for modern development needs.

Primarily designed for Linux, Golang powers many of the most robust and well-known applications running on various Linux distributions. It also supports MacOS and Windows. However, some file system-related issues persist on the latter.

The purpose of the language

Go language was created to address common problems with the C and C++ languages when writing server-side services. It was designed for complex and reliable large projects utilizing multiprocessor and multicore architectures of modern data centers and workstations.

Go language is focused on code readability; it is designed in a way that makes it easy for large teams to work on projects together. It is always compiled for assembly and then for native binaries of the target operating system, which makes Go binaries very fast with no overhead of Just-In-Time compilers.

This language is statically compiled, and the current version of standard libraries and other dependencies are included. Of course, the price of that approach is the increased binary size, but in return, there’s a guarantee it will work. Unlike many other languages, there are almost no issues with library dependencies or version conflicts.

Examples of applications

Very famous tools such as Docker, Kubernetes, Terraform, OpenShift, Cloudflare, Couchbase, InfluxDB, and Soundcloud are developed in the Go language.

→ Our take on Kubernetes dropping Docker runtime support – what it means for enterprises

Our entire cloud, including native cloud environments, runs on runtimes and tools written in Go code. There’s no doubt about its usefulness. It’s not an exotic language, and it’s a great tool.

Our own API management tools have been rewritten in the Go language, so there’s no question that it can also be used to manage and create various APIs (i.e., REST, gRPC). There are examples of Go applications for desktops using Qt and other GUI libraries. They are not very popular, but doable.

Applications that are written in such a fast and efficient language as Go can be compiled to webasm and executed in any modern browser.

Easy to start

The philosophy behind Go programming can be described as Keep It Simple. Anyone with a background in C and C-derived programming languages (C++, Java, C#) should be able to learn Go language quickly.

It’s not to say that Go isn’t very different from them, as it is. The main difference is a minimal set of language features from one side and features baked directly into the language (like collections, maps, concurrency, arrays, slices, etc.) instead of adding them to external libraries (contrary to most other languages).

Go comes with a great tutorial on its main webpage, which enables developers to discover language features interactively online. Microsoft Visual Studio’s code has excellent language support for Go with plugins, which are suggested automatically when opening .go files.

Discover how JasperLabs leveraged Avenga’s expertise to enhance its data processing capabilities. By partnering with Avenga, JasperLabs significantly reduced their processing time, enabling real-time analytics and setting new standards in data management. Success story

Figure 1. One of the Go source files from one of Avenga’s projects.

Likes

  1. Static typing and strict type control. Most of the errors can be detected at compile time, reducing the need for tedious debugging sessions.
  2. Simplified variable declaration. The := operator allows for type inference, making variable declaration easier while maintaining control.
  3. Performance. Go applications are significantly faster than Python and generally perform comparably to C, C++, and Rust applications. Recent benchmarks show Go’s performance continues to improve with each release.
  4. Memory consumption. Go has much lower memory consumption than Python and is similar to C, C++, and Rust. This efficiency makes it suitable for high-performance applications.
  5. Multiple return values. Functions in Go can return multiple values, including simple types and structs, simplifying error handling and improving code readability.
  6. Native concurrency. Goroutines, marked with the go keyword enables native concurrency for functions, making concurrent programming straightforward and efficient. This feature is particularly beneficial for developing scalable web services.
  7. Channels. These are language constructs for sending and receiving messages between different goroutines. They can also be used as synchronization tools. Channels help manage communication and synchronization in concurrent programming, enhancing the robustness of applications.

Here is a fragment of code from one of our products:

func TestHealth_ServeHTTP(t *testing.T) {
	type fields struct {
		path       string
		shutdownCh chan struct{}
	}

	tests := []struct {
		name       string
		fields     fields
		req        *http.Request
		wantStatus int
	}{
		{"healthy check", fields{shutdownCh: make(chan struct{})}, httptest.NewRequest(http.MethodGet, "/", nil), http.StatusOK},
		{"healthy check /w nil chan", fields{}, httptest.NewRequest(http.MethodGet, "/", nil), http.StatusOK},
		{"unhealthy check", fields{shutdownCh: make(chan struct{})}, httptest.NewRequest(http.MethodGet, "/", nil), http.StatusInternalServerError},
	}
  1. Functions as primary constructs. Functions in Go enable functional programming, making them a core part of the language.
  2. Concurrency features. Go’s concurrency features, such as goroutines and channels, are often compared to those in more complex languages like Haskel. While Go’s features are more limited, they are cleaner, easier to read, and more straightforward to learn.
  3. Simplified code. Go lacks many complex elements like templates, extensive dynamics, and lambdas, which makes the code simpler to read and more challenging to write.
  4. Focus on readability. Go is focused on the ease of reading code more than on how easy it is to write code. This comes from the experience of Google and other teams working in large teams and creating complex solutions. Readability comes first in the case of the Go language application.
  5. Effective garbage collection. Go removes unused objects using a garbage collector, but contrary to Java and other languages, it is considered very effective and with the much-needed low latency.
  6. Forward compatibility. Go promises forward compatibility with newer versions of the language and libraries; code written in older versions is supposed to be recompiled into newer versions. Of course, there’s always a slight change, and something won’t work, but still, it’s one of the key promises of the Go team.
  7. Open-source nature. Go is free and open-sourced, and no licensing issues are expected at any time.
  8. Container suitability. Go is well-suited for containerized environments, as it does not rely only on memory and CPU-intensive virtual machines or interpreters. It runs efficiently on platforms like Docker and Kubernetes.

Interesting facts

  1. There’s only one loop – for can work as for and while depending on parameters (example below of a for loop from one of our project’s source code).
for _, tc := range []testCase{
		{"name", "user", "pass", "", "Basic", "", false},
		{"name", "user", "", "", "Basic", "", false},
		{"name", "", "pass", "", "Basic", "", false},
		{"name", "", "", "", "Basic", "", false},
		{"name", "user", "pass", "testdata/htpasswd", "Basic", "", false},
		{"name", "john", "pass", "testdata/htpasswd", "Basic", "", false},
		{"name", "user", "pass", "file", "Basic", "open file: no such file or directory", true},
		{"name", "user", "pass", "testdata/htpasswd_err_invalid", "Basic", "basic auth ht parse error: invalidLine: testdata/htpasswd_err_invalid:1", true},
		{"name", "user", "pass", "testdata/htpasswd_err_too_long", "Basic", "basic auth ht parse error: lineTooLong: testdata/htpasswd_err_too_long:1", true},
		{"name", "user", "pass", "testdata/htpasswd_err_malformed", "Basic", `basic auth ht parse error: malformedPassword: testdata/htpasswd_err_malformed:1: user "foo"`, true},
		{"name", "user", "pass", "testdata/htpasswd_err_multi", "Basic", `basic auth ht parse error: multipleUser: testdata/htpasswd_err_multi:2: "foo"`, true},
		{"name", "user", "pass", "testdata/htpasswd_err_unsupported", "Basic", "basic auth ht parse error: notSupported: testdata/htpasswd_err_unsupported:1: unknown password algorithm", true},
	} {
  1. defer marks the code that will always be executed
func loadHTTPBytes(timeout time.Duration) func(path string) ([]byte, error) {
	return func(path string) ([]byte, error) {
		client := &http.Client{Timeout: timeout}
		req, err := http.NewRequest("GET", path, nil)
		if err != nil {
			return nil, err
		}
		resp, err := client.Do(req)
		defer func() {
			if resp != nil {
				if e := resp.Body.Close(); e != nil {
					log.Println(e)
				}
			}
		}()
		if err != nil {
			return nil, err
		}

		if resp.StatusCode != http.StatusOK {
			return nil, fmt.Errorf("could not access document at %q [%s] ", path, resp.Status)
		}

		return ioutil.ReadAll(resp.Body)
	}
}
  1. Semicolons ; are not necessary, except in rare scenarios
  2. Tabs are used instead of N spaces.
  3. Capitalized names are a convention to mark functions that are to be exported from the package (Println from fmt package as an example)
  4. There’s no try, catch, finally, or no exceptions – seriously.
  5. struct is used to define a complex type
  6. There’s no class equivalent as data and operations are separated, in a more functional-esque style than an object-oriented paradigm.
  7. The Go team selected a gopher as the official mascot of the language.
  8. Google created it, but they don’t own it, and it’s not a proprietary technology.

Compiler focus

The Go compiler is quite strict and cautious, which is a pain for fast prototyping, but great for large projects.

For instance, if you declare variables and you don’t use them it’s a compiler error . . . not a warning, but an error!

func F() int {
	var f = 5  
	return 0
}

The main features of Go

Figure 2. The main features of Go

Go unboxing

After installing Go, multiple elements are included out of the box: compiler, testing framework, build chain, lint for code formatting, and a static code analysis tool.

Command go get downloads external libraries by an URI; i.e.,
go get -u github.com/gogo/grpc-example

You can run the formatter to improve the code layout with the built-in command fmt
go fmt mysourcefile.go 

On the other hand, developers have voiced their disappointment with the lack of a more advanced package management system such as npm (from node) or pip from Python.

Goodbye object orientation

Go has structs and functions, so no classes combine methods and encapsulate data. Go means no inheritance, polymorphism, virtual methods, object constructors, etc. However, simplicity and performance mean trade-offs in language capabilities and richness.

Types can be derived (not inherited) from base types, but only the value members will be there, not functions. There’s no such thing as methods inherited from the base type.

package main

import (
	"fmt"
	"strconv"
)

type person struct {
	name      string
	birthYear int
	socialID  int
}

// function for Person type
func (p person) description() string {
	return ("Name: " + p.name + " born " + strconv.Itoa(p.birthYear) + " socialID=" + strconv.Itoa(p.socialID))
}

// type almost equal, almost means no access to "description" function from "base type"
type employee person

func main() {
	myPerson := person{name: "Jacek", birthYear: 1995, socialID: 23234234}
	fmt.Println("Person data: ", myPerson.description())

	var myEmployee employee
	myEmployee.name = "Jacek"
	myEmployee.birthYear = 1995
	myEmployee.socialID = 782834

	// will the method from other type work? NO! it won't compile
	fmt.Println("Employee data: ", myEmployee.description())
}

Go is focused solely on composition. Types can only contain primitive members of their own and other types.

Types can be embedded, as the example below shows:

package main

import (
	"fmt"
	"strconv"
)

type person struct {
	name      string
	birthYear int
	socialID  int
}
// this time we use composition of types
type employee struct {
	person
	employmentYear int
	salary         int
}

// function for Person type
func (p person) description() string {
	return ("Name: " + p.name + " born " + strconv.Itoa(p.birthYear) + " socialID=" + strconv.Itoa(p.socialID))
}

func main() {
	myPerson := person{name: "Jacek", birthYear: 1995, socialID: 23234234}
	fmt.Println("Person data: ", myPerson.description())

	var myEmployee employee
	myEmployee.name = "Jacek"
	myEmployee.birthYear = 1995
	myEmployee.socialID = 782834
	myEmployee.employmentYear = 2019
	myEmployee.salary = 1100

	// will the method from other types work? YES!
	fmt.Println("Employee data: ", myEmployee.description())
}

Please note that there is no implements keyword equivalent known from other languages. There’s no keyword at all. Instead, duck typing is used to match the implicit interfaces.

Go is a statically typed language. While it includes reflection and some type of inference, it does not support prototypes or dynamic classes. Simplicity is a core principle of Go.

Go language is strongly criticized for lacking generics, but workarounds like empty interfaces exist. Addressing this issue is one of the major changes planned for future versions of Go.

There’s no NULL problem, there’s a NIL problem

Unfortunately, the eternal problem of no values and nulls is not fixed in Go. NULL issues apply to Go, which is sad and disappointing because the language is much younger than its NULL-infested predecessors.

No exceptions

Instead of an exception, there’s an error return type that carries the information about the improper execution of the routine. It requires writing code explicitly dealing with the errors, which could be annoying for newcomers, but it helps to maintain discipline because errors cannot be simply ignored. Infamous empty try catch finally, etc., do not exist here.

Niche

The Go developer community is approximately 13.5 percent of the global population of developers; they are experienced and active. Still, it’s harder to find information online compared to Python, Java, JavaScript or C#.

Why Go for the future of our products?

Avenga’s leadership highlights some critical reasons for choosing Go for product development. Go has a standard library covering HTTP, TLS, networking, and parsers. Those allow you to replace code clusters with a single line of code, making coding more lightweight and efficient. It’s a convenient and intelligent way of programming concurrency like C++ and Java, which is a viable benefit to leverage complex applications.

An interesting fact: two of the Go language co-inventors, Robert “Rob” C. Pike and Kenneth “Ken” L. Thompson, were a part of the original team that had developed Unix OS.

Looking further, it is versatile, and its ease of use and speed of the software development process, with simple functions-based syntax, are significant advantages of Go programming for server-side requirements. What is also worth mentioning is that Go applications are a suitable choice for a Docker container, as fat binaries allow for slim Docker scratch images. Although not without faults, being minimalist, clean, and readable makes it a language of choice for developers.

Avenga switched to Go language for all custom-built components, handling real-time USR traffic for one of our products. Go offers high performance and resource utilization on multi-core systems, especially for IO and network-bound tasks. The concurrency primitives give outstanding control over asynchronous processes. The standard library offers perfect support to programs on the network level and allows us to fine-tune the behavior of the HTTP and TLS stack. Last but not least, the built-in profiling tools helped us find bottlenecks and optimize.

Also, the Go language has been a natural choice for us when developing another Avenga product because it was designed for concurrent server processing, and the product is used in many docker-based customer setups. The small footprint of the Go binary offers well-controlled operations behavior.

Summary

There’s a world outside Java, Python, JavaScript, and C#. Go programming language is not expected to replace all those enterprise languages but it is an excellent choice when speed and a memory footprint are essential, and C/C++ complexities are to be avoided.

There is Rust, which also aims at simplifying things, but it is much more complex and harder to learn than Go. Go is more accessible and built with team collaboration in mind. Its code is readable and understandable. We appreciate its minimalism and letting go of almost all OOP legacy.

If you’d like to learn more about Go and how it could help achieve your business goals, contact us for a free consultation!

Other articles

or

Book a meeting

Call (Toll-Free*) +1 (800) 917-0207

Zoom 30 min

* US and Canada, exceptions apply

Ready to innovate your business?

We are! Let’s kick-off our journey to success!