In this article, we will explore the GO language which is really is a good replacement for many languages for background tasks of your applications or programs. I will also share a resources to help you quickly learn how to perform certain actions in Go programs, like networking, resources and data intercharge.
Introduction and Background
I have spent more than half my decade just exploring what different C-like programming languages have to offer, how they work and how we can have them perform a certain action. Sometimes, they are based too strictly on object-oriented designs, sometimes they are a mixture of all, sometimes pointers and sometimes memory-managed language features impress me. Lately, Go has been widely accepted to be used for their development purposes. Back in the day, when I used to work for a professional firm, I remember we used to have some projects for Go programming language. I admit, I was afraid of change, and did not want to invest any time in the learning phase of this new programming language that did not even have a backing framework. So, in a nutshell, a Go program can be used to easily manage and maintain a backend script for yourself, for a complete solution to be developed in Go is not possible at least in the near future.
From what I believe, Go has many things that are quite similar to what other languages have to offer. But then it builds several language constructs that are either not available in other languages, or are missed by programmers. In a nutshell, I personally think that Go can be a quick and easy replacement for your background tasks. It contains several improvements and language-provided features, such as memory-management, interprocess communication, deferred functions, extensibility for types and much more. Some of the concepts do come from languages like JavaScript, and some concepts are more of Go itself. One way or another, Go itself is a really amazing language and as we will explore, it really is a good replacement for many languages for background tasks of your applications or programs.
By the end of this post, I will share a few more ideas on Go programming, and will share a few more resources to quickly learn how you can perform certain actions in Go programs, like networking, resources and data intercharge.
Environment Setup
Also, one more thing, you guys can easily go ahead and try out the Go programming language in online compilers, but I would be using Visual Studio Code with the Go extension installed—in my opinion, the best way to experience Go language is with Visual Studio Code. The online editor can be found at https://play.golang.org/, you can easily copy-paste most of the codes that I will be presenting in the article to the editor and run them. They would (should!) work they way they are meant to.
However, an ever better experience, and with the support of IntelliSense, and code completion can be used with Visual Studio Code. For this to work, you first need to install the Go compilers and other binaries from Go's official website, once that is done, please do work your way around and add the environment variable for Go language in your system, so that commands like go run
, go build
are available to you throughout the system. This is also required by the extension that we will be using in Visual Studio Code. Go through the following links and set them up as well.
Go as a Programming Language
Go, essentially, is a general purpose programming language, just like C. Most of the features, and constructs ressemble what C had to offer. Whereas in several cases, you will find out that most of the concepts of C language are improved, or at all ignored if not removed in the Go language development. I might want to use Go language for the cases where I only want a certain operation to execute in the background. But this does not prevent the language to be used for full blown web servers, data pre-processors, and cryptographic helpers. And as I have already mentioned the fact that Go is a general purpose programming language, it can be then enhanced to be used for any other purpose as well, who knows Google might invest some time in making this language a machine learning ready language for its concise, succinct and natural structure of programming, you can go and check the Go vs Python3 benchmarking for yourself. But for those, who want a one-liner hello world program, Go is not for you! And, I would argue, Go is definitely not a much academic oriented programming language, at least for beginners.
I will start the post with a good hello world program, written in Go:
package main
import "fmt"
func main() { fmt.Println("Hello, world!")
}
Yeah, so much for a one-liner there, this program is even lengthier—some might use the term self-explanatory—as compared to a similar C program:
#include <stdio.h>
int main() {
printf("Hello, world!");
}
But remember, there is a reason for all this verbosity, and most of these things are added to explain the overall construct of a program. One thing that Ken kept consistent among both languages was, the main
function. A program requires a main function, in order to execute. But Ken went one step ahead with the design of this language in that language now requires us to manually state the package declaration, and only the main package gets to be the entry point in a program. So, this is like a more verbose way of telling the compiler, which of the file gets to be the starting point and then your program continues to proceed from there.
You will also notice that the semi-colons are gone. No, Python programmers, hold your horses, semi-colons are there, they are just not needed to be entered manually. They are added by the compiler later on, and that is one of the most beautiful things in Go; read ahead, and you will find why I loved this feature.
Type System and Built-in Types
Go language comes with several types, all built-in, and Go allows you to easily scale up a type and create your own types, you can even alias a type, just like you would do that in C or C++. Go provides the following types, to be used out of the box:
Type name | Size | Description |
Boolean | 1 byte | Basic boolean type is used in expressions, conditionals and loops. |
String | Variable in size | Go language supports Unicode data points all the way up to UTF-32. |
Signed integers | 1 - 8 bytes | Signed integers range upto 64-bit data. |
Unsigned integers | 1 - 8 bytes | Unsigned integers are also used for pointer type values, and for storage of unsigned numbers. |
Byte-data type | 1 byte (alias for uint8 ) | Bytes are most widely used data types in Go language, in writers, in HTTP libraries, and in data interchange format parsers. |
Floating-point values | 4 - 8 bytes | Seems like Go does not have a preference to call float-64 a double data type. So, they have a float32 , and float64 . |
Complex numbers | 8 - 16 bytes | Numbers are about to get real over here! Go supports complex numbers of 64, and 128 bit size. |
This code sample from Go's source code demonstrates how their sizes are specified:
var basicSizes = [...]byte{
Bool: 1,
Int8: 1,
Int16: 2,
Int32: 4,
Int64: 8,
Uint8: 1,
Uint16: 2,
Uint32: 4,
Uint64: 8,
Float32: 4,
Float64: 8,
Complex64: 8,
Complex128: 16,
}
There are types like int
, uint
, that are generic and more specific in nature than their sized counterparts. On systems, they are guaranteed to have a size of 32 bits, and 64 bits on 64 bit systems. It is recommended by the language developers, to use these types instead of their sized counterparts, unless you have some real reasons to use their sized counterparts. See this link for a bit more of exploration and understanding the type system of Go.
The benefit of these types is that they provide a good amount of interoperability and conversion between all the types, so you can easily do this:
bytes := []bytes("Afzaal Ahmad Zeeshan")
Unlike in other languages, this is clearer to understand, and easier to write and remember.
In Go, we do not have classes. Only type system that is made available to create custom types is the struct
type, and we can create our own contracts for the types, that are the interfaces
. Let's take a look at both of these, one by one.
struct in Go Language
In Go language, struct follow the same rules as C, not even C++, and definitely not C#. You can only include the fields for your structure, and define the properties it has. You cannot add methods to the structure. Go and read the C structures and you can see, that this has its roots defined in C language itself. A basic Go structure looks like this:
type Person struct {
name string
age int
}
Let's break this down, one by one. Unlike C, and many other C-derived languages, in Go, things start to not only just read from right-to-left, but they are also keyword'd from right to left; so in other words, this is purely LTR language in the way of writing, reading and understanding. So, let's try reading the code above, please, join me.
type Person is a structure {
name is a string
age is an integer
}
If this does not make much sense, in reading, we will take a look at other cases of the similar thing as well. Now we can go ahead and create some variables and understand how they work. Write the code that I have provided above, in the Go compiler and try to compile it. Several things you would learn over here:
- Go does not have explicit access modifiers.
- Most Go linting and vetting tools give you a warning, that exported types must have a comment.
These are the ways in which Go programs easily control how your types are accessed. In Go, every type with a capital first word is public, and others are private; even if rest of the word is upper cased. So the valid program, in Go, would be either to lower case the type, and everything is package-private. Otherwise, a good comment can be added.
type Person struct {
name string
age int
}
Do you find anything more that needs your attention? Go ahead, I will give you a moment to think about it, before I proceed to the next section of functions.
Variables, parameters, and other types of field initialization mostly take place automatically, unless you really need to create the types and zero-initialize them.
func main () {
var person Person
person.name = "Afzaal Ahmad Zeeshan"
age = 23
}
That works just like any other structure in any other programming language. The main concept to learn here is, that again, you cannot embed a struct
in itself, but you can easily embed a pointer to the type in itself. We will explore the pointers in the section ahead. One last thing I would like to demonstrate before ending this, is, the type aliasing:
type Name string type People []Person type Port int
How does that feel now? Don't they feel intuitive, while you read them out. This is equivalent to what you would be doing in C, or C++, as:
typedef string Name;
typedef Person People[]; typedef int Port;
Rest is quite the same almost in all the languages for C-family.
Interface Types in Go Language
Well this is new, to C programmers, and as a special keyword to C++ programmers. We have had this concept in C#, and Java for a really long time. The concept is, to create contract types, that are essentially guaranteed to have a certain behavior in them. In Go, the language basically follows the same practices, that mostly languages like JavaScript have, to implement an interface, one only has to have the functions in it, no need for an additional operator for implementation. Quite simple though. So for example, the following code:
type Works interface {
work() error
}
This is an interface that needs to be implemented by types that have to act as a type that Works
. Now the implementation process takes a bit of more understanding of how functions work in Go language. If, for instance, we say we have a function that takes an instance of a worker, and makes it do some work, we cannot pass the person in the function:
func giveWork(work Works) {
}
If we pass the person type, it will complain saying that type does not have the function work()
in the body. This error specifies that our actual type does not fulfill the requirements of being an argument.
So, that is what we do in the next section, let's head over and see for ourselves.
Functions in Go
Just the way, the structures are designed in similar fashion, the functions are also quite similar in nature and their behavior in Go language. What you have in Go is:
func name (params) returns {
}
You must remember from our previous code block, how we created the main function, we used the func
keyword for that. This keyword creates the function, and later part defines the name, parameter list, and return types for this function. Let's try creating a basic function that takes the person, and prints the name of that person.
func printname(person Person) {
fmt.Print(person.name)
}
This is the point where I feel it is important to explain the overall concept of semi-colons, in Go language, semi-colons are added before your code is compiled, so you can omit them, but do not consider that they are not important. To try that out, change the function, bring the braces down.
func printname(person Person)
{
fmt.Print(person.name)
}
This way, Go will add semi-colon after the Person)
, and that will cause it to break the compilation process. Thus, it is a must to have the {
bracket on the same line, and that was the reason why I mentioned earlier that I love the way Go enforces some coding styles in the language itself. This way, all of your code will be written following the same bracket notations and styling convention. If one of your engineers does not follow it, their code fails to build. We can also return errors from Go language functions, they are returned as same variable values that are to be returned if the function succeeds.
func printname (person Person) error {
if person.name == "" {
return errors.New("Name is empty")
}
fmt.Println(person.name)
}
In Go, we can return multiple values from a function. And, even better is that these values can be assigned to by the code and then next steps can be taken to return from the function.
func namelength (person Person) (count int, problem error) {
if person.name == "" {
return 0, errors.New("Name is empty")
}
return len(person.name), nil
}
That is pretty much it for how tuples are created. And, Go goes a bit beyond than this, in that, it lets you assign the values for these variables separately too. The function from above can be rewritten in Go as:
func namelength (person Person) (count int, problem error) {
if person.name == "" {
count = 0
problem = errors.New ("Name is empty")
} else {
count = len(person.name)
problem = nil
}
return
}
Most of you who have worked in C++ or C# languages would know that this kind of a behavior is quite obvious in other languages, and is known as, pass-by-reference.
void namelength (Person person, ref int count, ref Exception problem) {
if(string.IsEmptyOrNull(person.name)) {
count = 0;
problem = new Exception ("Name is empty");
} else {
count = person.name.Count;
problem = null;
}
}
void namelength (Person person, int& count, std::exception& problem) {
if(person.name == "") {
count = 0;
problem = std::exception("Name is empty");
} else {
count = person.name.size();
}
}
So far, we have explored how functions behave in Go language, now let us take a look at the methods. We already have seen that Go language does not contain classes, and that means no object-orientation. At least not the way other languages have implemented and supported them. In Go, you have to attach the functions to the types, after they have been declared—not inside the body. Attachment of the function happens something like this:
func (type) name (params) returns {
}
What this suggests is that the functions require to know the type of the variables they can work on, and then Go language can inject those types and variables inside the functions scope. So, let's rewrite our namelength
function that we have been using since a few sections now.
func (person Person) namelength() (count int, problem error) {
if person.name == "" {
count = 0
problem = errors.New ("Name is empty")
} else {
count = len(person.name)
problem = nil
}
return
}
This can be easily called on the type to get the value for it, like:
person := Person { name: "Afzaal Ahmad Zeeshan", age: 23 }
count, problem = person.namelength()
So this behaves in a way like an object-oriented code, that your types now hold data and they can perform some certain actions, and contain states. Quite useful, a property of the Go language as seen here. So this now helps us to solve the problem of interface implementation, now if you recall, we saw that a type is said to be implementing an interface in Go, if it contains a function as such. So, we can go ahead and implement our Works
interface:
func (person Person) work() error {
}
This way, now we can use the Person
type anywhere a type of Works
interface is required by the program. It is even simpler than it sounds, all you need to do is, provide the functions to the type, and there you have it then. Now if we pass the person in the function above, it will work, because person type does not contain a function work
of the same signature.
Now there is one thing left that requires to be discussed, that is, how do we reflect the changes from a function in the variable itself. If you execute the code above, and try to modify a field, you will realize that the actual type remains the same. How do we overcome that? Read the Go and Pointers section ahead, and you will find out. :)
Go and Pointers
One thing that needs to be spoken something about is the pointer in Go language. They are the same as they were in C language, but way too much managed than the old days. Go pointers are used to create references to the memory locations, and make changes to, read from, update the same memory location that is created instead of passing around the variables in and out of functions in a program. This makes the program look like a lot of mess, and little peace. Pointers solve the problem by granting access to the same memory location, and enabling the developers to update the same variable without having to pass-in, or get anything out of the function.
Some points to remember about pointers in Go are that they don't just die as you exit the function, do this would survive:
func getperson (pName string) *Person {
return &Person{ name: pName }
}
Unlike in most other languages, these types would die as soon as the scope goes out. Pointers are of enough size to hold an address (32-bit systems, 64-bit systems...), and they do not support arithmetic. The reason behind this behavior is that they are not arrays. Unlike what you were told about arrays in C, that they are pointers, and indexing is done to check the next address. In Go, that does not happen, and arrays in Go are also value-types, not reference-types, but we can leave that for another time. However, you can still access the pointer's actual data using *
, and you can even assign the value to the variable being pointed to, using *
operator, see this link for example, https://tour.golang.org/moretypes/1.
Passing something to a function, takes a pointer type for that. Passing pointers, and receiving pointers is safe in Go, and does not involve leaking. In fact, a new type is created and passed along to the next scope.
func main () {
gottenPerson := getPerson()
fmt.Printf("gottenPerson exists at address %x\n", &gottenPerson)
}
func getPerson() Person {
p := Person{}
fmt.Printf("p exists at address %x\n", &p)
return &p
}
p exists at address &{61667a61616c 0}
gottenPerson exists at address c042004030
So, they are not the references to the same object that was created in the second function (first one is a zygote type, second one is an allocated variable). But, a safe way to share around the same object, and expect the changes to reflect outside the scope as well, unlike with value-types.
At this spot, we get to introduce the two more important, and interesting points, the make
and new
operators in Go language. These operators are used to allocate memory for the types, and return their pointer (as in the case for new
) or the variable (as in the case of make
). They both have their own purpose in the language. The make
operator is used to initialize the reference types, such as, slice
, maps
, etc. We will take a look at them in their own post. Let's take a quick look at the new
keyword, and see how that differs in the behavior of this program, and how that helps us in understanding how pointers work.
func getPerson() Person {
p := new(Person) fmt.Printf("p exists at address %x\n", &p)
return p
}
Function changes a bit now, we get a pointer to the type—quite similar to what we get in C++ using new
, and using malloc
in C. That also makes us change the return type, from &p
to p
itself, because of the type of the return of our function. Getting the address of p
would return the address where our pointer is stored, not where the object is stored. Rest of the concepts of the functions are same, as they are in other languages, passing a pointer and modifying that, would ultimately cause a change in the actual value to be changed too. For example:
func (person Person) rename (newName string) {
person.name = newName
}
This will not have any effect on the actual instance of our Person
type, the reason being the fact that this is being passed in the function as a value type. In order to overcome this situation, we need to pass this in as a pointer:
func (person *Person) rename (newName string) {
person.name = newName
}
This will change the instance data too, and now our structure will contain the updated value for the name. This is quite useful, and you can see that you did not have to modify anything, unlike C or C++, Go does not require you to use other operators like ->
to access data from pointers. Data from pointers is accessed in the same fashion, as it is accessed from normal variables.
Panic Recovery in Go
Exception handling in Go, follows the same patterns as they had in C language. There is no try...catch
block in Go language, you raise an error, and you handle in other procedures, by checking what has been setup in external modules, or your own module. You must have seen how we tried to return the error from a function, based on how it was provided the arguments. In case, where your function cannot proceed, then you need to call panic
, and pass the value indicating the problem. For example, imagine you are trying to download the data for the Person
type, and the network is unavailable, then you must panic, there is no way to recover from this error, same is the case if you are loading the data from a file, and somehow you can either not file the file, or file cannot be created/accessed.
func namelength(buffer NetworkBuffer) (count int, problem error) {
if buffer == nil {
panic("Network buffer is nil.")
}
var person Person
person.name = buffer.Readstring()
return len(person.name), nil
}
Panicking starts to close the stack of your program, until there is no module left. Within those functions, and modules, you can resolve anywhere. You use the recover
function call in Go program, that will help you to get the problem that was raised, and you can try to recover from there, if not possible then you can proceed onwards. The way to do this is by using the defer
functions, these functions are called once the function that encloses them finishes executing. Like this:
func crashingfunc () {
defer func () {
problem := recover()
if problem != nil {
fmt.Println(problem)
}
}()
panic("Calling panic function call to be caught in deferred function")
}
This is the way that you can perform post-execution actions, like exception tracing, closing any resources that you are currently holding access to. The deferred functions are always called, think of them like try...finally
calls, where finally
will always be called even if code crashes.
Remaining Topics...
Go has so many other topics that should be covered, topics like concurrency, packages, networking, data interchange and so many more topics need to be discussed in Go language, and they require a separate post of their own. I am planning to write a bit more posts on this topic, and then will move onwards to some more advanced concepts like containerization of Go program, or how to perform cryptographic operations on the data.
Until then, consider checking out the code that I have supplied with this post, and explore them, create your own type alias, create functions, work around with pointers and non-pointer types. One thing I should ask from you, try exploring the arrays, and try to see where arrays fail.
History
- 28th November, 2018: Initial version