Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / Go

Exploring Go as a C-Family Developer

5.00/5 (5 votes)
28 Nov 2018CPOL19 min read 8.5K  
Go language and discuss of it as a C-family developer
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.

Image 1

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:

MC++
package main 

import "fmt"

func main() { // This is more equivalent to void 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:

C++
#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:

C
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:

C
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:

C
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.

C
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.

C
// Person is a custom structure defined to explain Go language.
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.

C
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:

C
type Name   string    // Type name is a string
type People []Person  // Type people is an array of type person
type Port   int       // type port is an integer

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:

C
typedef string   Name;
typedef Person   People[]; // You get it
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:

C
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:

C
func giveWork(work Works) {
    // code
}

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:

C
func name (params) returns {
    // code ... 
}

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.

C
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.

C
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.

C
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.

C
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:

C
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.

C#
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; // Redundant call
    }
}
C++
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();
        // problem = null; // references cannot be null in C++
    }
}

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:

C
func (type) name (params) returns {
    // code
}

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.

C
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:

C
person := Person { name: "Afzaal Ahmad Zeeshan", age: 23 }
count, problem = person.namelength()

// other code

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:

C
func (person Person) work() error {
    // Now person works
}

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:

C
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.

C
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
}

// Output
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.

C
func getPerson() Person {
   p := new(Person) // returns *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:

C
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:

C
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.

C
func namelength(buffer NetworkBuffer) (count int, problem error) {
    // Just imagine, please.
    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:

C
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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)