14 min read

Language enjoyer, Go

Language enjoyer, Go
Photo by Chinmay B / Unsplash

I want to start a series on the languages I enjoy using and experimenting with. And the learnings from them. My main ecosystem, for a 9-5 job, is the Microsoft stack. And it's most popular language, C#. I do love working in it, especially with the evolution they went through in the recent past. With the move towards dotnet core, I do think it is one of the best ecosystems. For some simple things, it can be a bit "too much", and that is how I end up looking into the alternatives. It could be for the sake of learning, playing around, or just plain simplicity if I need to implement something.

The language I will be kicking off with, as the title says, is the Go. I do enjoy several things in it. That "barebones" approach to writing code, without much synthetic sugar. This simplicity when writing code is satisfying when coming from C# in some regards. You're in control and it requires a bit of thinking about what you need to write and how. It translates into pretty understandable code with a clear flow. Splitting the code into smaller functions feels natural. Organizing your code is left to you and what tickles your fancy.

Another that caught my attention, at the get-go, is error handling. And how to communicate that your function can produce an error. This error makes it nice, in my opinion, to have a nice signature that communicates, for example, validation errors. With panic it is a way to raise the "I don't know what to do here". The exceptions in C# have their use case, but their "side-effect" can't be "documented" in the function signature. Which requires writing documentation to document those. It's not a deal breaker for me, but "clean coders" out there have opinions on code comments.

Another thing is the implementation of gocouroutines. I do enjoy a C# async/await a bit more, while the Go implementation of the concurrency also offers a nice experience. paired with error and channels it makes up for some crisp and clear concurrent code. Add to that defer and things start to piece together fast.

Access modifiers

The access modifiers are a convention, based on the naming and its capitalization. In Go terms, exported vs. non-exported. This took me a bit of getting used to, till I started thinking about how naming in other languages also has a meaning. Even with access modifiers being present in the languages. So less boilerplate, which is fine while not being obvious at first glance when looking at the code.

// Public
type MyType struct {
	// ...
}

// Private
type myType struct {
	// ...
}

The same will apply to the functions as well.

// Public
func MyFunction() {
	// ...
}

// Private
func myFunction() {
	// ...
}

As stated, the naming together with access modifiers, in other languages, also is related. So the "short-hand" here using the convention is not something I am mad at. For example:

private readonly MyType _myType;

Or any naming convention you may be following, I saw a couple of them throughout this short time. I am not bothered with either, I would just configure my editor of choice to help me around with this. The simplicity and convention of Go do make me think about it and how would that look in C# for example.

public class MyType
{
	public void MyFunction () {}

	private void myFunction() {}
}

Something to have a nice debate over at one point, if I ever find it important enough to have a strong opinion about. It would make for a fun experiment, just for the cognitive load as well, when reading the code to understand if the function you're calling internally within the type is a public one or internal/private/protected.

Types

Not going to dwell on the basic types here that are baked into the language. I would like just to say that the "basic" types (here including arrays, slices, maps, channels, and structs) do their job perfectly. There are not many different collection types or concurrent collections, it is just a root that you can use to build upon. Which I find great. Do one thing and do it right.

Type declaration in Go is intuitive. Simple as that. You can define new types from existing ones, enhancing code readability and maintainability. For example, type UserID int defines a new type UserID based on int, allowing for type safety and clearer code. I love that you can "alias" a basic type in this fashion to give more meaning to your code. I love composition in any form and fashion and this, in my opinion, enables you to express yourself clearly.

Another thing that I liked a lot is, for readability's sake, the order of the variable/parameter declaration. type UserId int reads for me nicely. First the name of the parameter that you depend on and then its type. var IsCompleted bool = false as well, it reads nicely and every next piece, going from left to right, provides the next piece of the context.

The shorthand, myType := MyType{"test"}, is also a nice touch and a bit of a synthetic sugar and does also make for a bit less code. It only can be used within the functions, which is also really nice to indicate locally scoped variables.

Interfaces

One of the most powerful features in Go's type system is interfaces. An interface is a set of method signatures, defining behavior rather than concrete data. Interfaces allow for polymorphism and abstraction, letting you write generic and reusable code. Functions and methods can accept interfaces as parameters, enabling them to work with any type that implements the interface methods, thus fostering a flexible and decoupled design.

I can't stress enough how powerful this is, from what I experienced. The level of decoupling you can achieve between the contract and its implementation is awesome. This reminded me of some things I remember from the Linux kernel, where you have a "plug-in" interface to provide functionality for drivers, for example. And it is up to the implementor of such to obey the contract. And in Go, this goes to the next level.

type Speaker interface {
    Speak() string
}

type Dog struct {
    Name string
}

func (d Dog) Speak() string {
    return d.Name + " says Woof!"
}

type Robot struct {
    ID int
}

func (r Robot) Speak() string {
    return fmt.Sprintf("Robot ID %d says Beep Boop!", r.ID)
}

func PerformSpeak(s Speaker) {
    fmt.Println(s.Speak())
}

func main() {
    dog := Dog{Name: "Buddy"}
    robot := Robot{ID: 123}

    PerformSpeak(dog)
    PerformSpeak(robot)
}

A type implements an interface by implementing its methods. There is no explicit declaration of intent; no keyword like implements is needed. This implicit implementation fosters a loose coupling between components and enhances code reusability.

One part, that took me a bit to understand was the concept of empty interfaces. It is sort of like object. Sort of.

package main

import (
    "fmt"
    "reflect"
)

func Print(v interface{}) {
    fmt.Printf("Value: %v", v)
}

func main() {
    Print(42)
    Print("Hello, Go!")
    Print(3.14)
    Print(struct{ Name string }{"GoBot"})
}

// Output:
Value: 42
Value: Hello, Go!
Value: 3.14
Value: {GoBot}

Then you combine that with variadic parameters and you get params object[].

package main

import (
    "fmt"
    "reflect"
)

func Print(items ...interface{}) {
    for _, item := range items {
        fmt.Printf("Type: %s, Value: %v\n", reflect.TypeOf(item), item)
    }
}

func main() {
    Print(42, "Hello, Go!", true, 3.14, struct{ Name string }{"GoBot"})
}

// Output:
Type: int, Value: 42
Type: string, Value: Hello, Go!
Type: bool, Value: true
Type: float64, Value: 3.14
Type: struct { Name string }, Value: {GoBot}

Using empty interfaces (interface{}) allows you to write functions that can accept any type, as every type in Go satisfies an empty interface by default. Sort of like everything in C# has a base of object. They do sacrifice type safety for flexibility. But still, a fun thing to use with care.

Functions

Then it came down to the functions and how they're "associated" with the type. As I came from dotnet this looked like a simplified version of the extension function. I liked it and it makes again for a clear separation of the state and the operations that you may perform on the state.

It also makes a lot of sense for me if I need a helper function, I don't need a type or to "associate" it with the type. It can be left alone and just encapsulate what it will be "helping" with. Simple and clean, in my opinion.

package main

import (
	"fmt"
)

type MyType struct {
	hidden bool
}

func (myType *MyType) IsHidden() bool {
	helper()
	return myType.hidden
}

func helper() {
	fmt.Println("I help with something")
}

func main() {
	myType := MyType{hidden: true}
	fmt.Println(myType.IsHidden())
}

A nice feature that I like about Go functions is the multiple return values from the function. The reason is the error a way to document in a function signature what a caller can expect. Or any other combination of types. And when you combine this with named return values, it becomes pretty simple to understand any "side-effects" you may have.

func divide(dividend, divisor float64) (result float64, err error) {
    if divisor == 0 {
        err = errors.New("Cannot divide by zero")
        return
    }
    
    result = dividend / divisor
    return
}

I draw a line of max 2 parameters here and a second one would be the error. Simply because I would consider having a custom type if it goes beyond result execution and side-effects of it. That is just me and how I think about the code.

Some other features around functions are there as well, like anonymous functions. Or being able to use the function as a value (think higher-order functions). These are not unique to the language, so not going to cover them.

Pointers

This one is something that most people need to get "used" to. If you're coming from some languages that have them like C or C++, then I think Go has a simpler implementation of them. Mostly it takes a bit of practice to make clear if you wish to pass as value or as a reference.

package main

import "fmt"

type MyStruct struct {
    Field int
}

func modifyStructByValue(s MyStruct) {
    s.Field = 10
    fmt.Println("Inside modifyStructByValue:", s.Field)
}

func modifyStructByReference(s *MyStruct) {
    s.Field = 20
    fmt.Println("Inside modifyStructByReference:", s.Field)
}

func main() {
    originalStruct := MyStruct{Field: 5}

    modifyStructByValue(originalStruct)
    fmt.Println("After modifyStructByValue:", originalStruct.Field)

    modifyStructByReference(&originalStruct)
    fmt.Println("After modifyStructByReference:", originalStruct.Field)
}

// Output:
Inside modifyStructByValue: 10
After modifyStructByValue: 5
Inside modifyStructByReference: 20
After modifyStructByReference: 20

As shown above, the signature looks almost the same, with a small * next to it. Pun intended, deal with it. And that is all the differences that it takes to have a side-effect of value being changed in the "original". In the call to modifyStructByReference it passes the reference to the memory address is done using &.

To make an analogy here: You have a passport. It is an important document to you if you ever wish to leave your country. So when you hand it over at passport control and they stamp it, that is passing by reference. The original has been modified. But when you get to your resort and there they ask for a passport, you can hand over a copy of it. And whatever happens on that Xerox of it, like when they scribble something on it or whatever, it has no impact on the original. That is passing by value.

Now, there are some language takes on it that may not be obvious and can cause a head scratch at the start. Till you just learn it and go with it.

package main

import "fmt"

type MyStruct struct {
    Field int
}

func (s *MyStruct) MultiplyValueBy10() {
	s.Field *= 10
}

func main() {
    originalStruct := MyStruct{Field: 5}

    originalStruct.MultiplyValueBy10()
    fmt.Println(originalStruct)
}

// Output:
{50}

I thought of covering this under gimmicks (one of the next chapters) but it is tightly coupled with pointers, so things that change together stay together. So why does this work? Simply, it's a language feature and it will "assume" what you wish to do (&originalStruct).MultiplyValueBy10() and with this small synthetic sugar make your code "cleaner".

I needed to search engine this, and it is called pointer indirection. It works in other ways as well, where if it expects a value language will "correct" it and turn it for example (*originalStruct).SomeMethodWithValue(). As said, a bit of a gimmick for me. Or a synthetic sugar, whatever works for you.

Concurrency

This is one of the parts where it took me a couple of examples to map the async/await pattern I was accustomed to. There are some choices in language design that I may not personally agree with. And I am biased here, based on my preferences as well. I am not saying it is bad, it does with the naming conventions in the language itself which makes things a bit clearer.

While on the other hand, after you get a hang of it, seems pretty straightforward as well. The concept of channels is not something that doesn't exist in other languages, but the implementation of them was one of the things to wrap my head around. This chan<- indicates that you can write into it while <-chan reading from it. Seemed a bit odd at first sight, but then again it's also clear. When paired with how this is done in code, it also looks pretty readable.

package main

import (
	"fmt"
	"sync"
)

func readFile(prefix string, lines chan<- string, wg *sync.WaitGroup) {
	defer wg.Done()
	
	for i := 0; i < 10; i++ {
		lines <- fmt.Sprintf("%s: random string %d", prefix, i)
	}	
}

func printLines(lines <-chan string) {	
	for line := range lines {
		fmt.Println(line)
	}
}

func main() {
	var wg sync.WaitGroup
	lines := make(chan string)

	go printLines(lines)
	
	wg.Add(1)
	go readFile("file1.txt", lines, &wg)
	wg.Add(1)
	go readFile("file2.txt", lines, &wg)
	wg.Add(1)
	go readFile("file3.txt", lines, &wg)
	
	wg.Wait()
	
	close(lines)
}

The goroutines are equivalent of Task in C#. And before "Well, actually..", I am aware they're not the same. There are differences. This is my way of mapping the concepts between the languages in terms of the functionality they provide to achieve a similar task. One thing I found interesting is that you don't need to rewrite the code to make it "awaitable" in Go.

package main

import (
	"fmt"
	"time"
)

func doSomething() {
	fmt.Println("Doing something")
	time.Sleep(time.Second)
}

func main() {
	go doSomething()
	time.Sleep(2 * time.Second)
}

Or:

package main

import (
	"fmt"
	"time"
)

func doSomething() {	
	fmt.Println("Doing something")
	time.Sleep(time.Second)
}

func main() {
	doSomething()
}

Both will work. Yes, it is not the "I am so proud of this code". It is to illustrate the point that any method can be run in any way you see fit. In C# you can do a similar thing, but some things would need to change in method signature to make this happen. And in Go doSomething is oblivious to this and how it is run. Which is a nice touch in my opinion.

Gimmicks

Now, onto some of the language "gimmicks" that I found interesting and fun. Let me kick off with the first one that caught my eye, even before fully understanding it. The defer. I really like this one, even thou it takes a minute to adjust to it and how it is "usually" in the code. The example from the previous part details this.

func readFile(prefix string, lines chan<- string, wg *sync.WaitGroup) {
	defer wg.Done()
	
	for i := 0; i < 10; i++ {
		lines <- fmt.Sprintf("%s: random string %d", prefix, i)
	}	
}

The wg.Done() will be only executed after the readFile returns. The closest analogy in my head would be try...finally in C#, thou not the same. And a bit simpler. I love that you can do a cleanup of your allocations in this way and language will take care of it for you.

package main

import (
    "database/sql"
    "fmt"
    _ "github.com/lib/pq"
    "log"
)

func main() {
    connStr := "user=username dbname=mydb sslmode=disable password=myPassword"

    // Open a database connection
    db, err := sql.Open("postgres", connStr)
    if err != nil {
        log.Fatal("Error opening database: ", err)
    }
    // Defer the close till after the main function has finished executing
    defer db.Close()
    
    // ...
}

This is nice. Clean and simple, while it does take a minute. You can also do stacking of the defer, which opens the door to some interesting implementations.


When it comes to simplicity, I think the one that takes the cake for me is the looping in Go. You only have one language construct, and that is for. It is so simple to reason about it, while it does with some gimmicks of its own. An example is that for is used to implement while that you can find as a construct in other languages. Not a big deal for me, as it is pretty easy to make a reasoning around it. As this naive "health" check implementation indicates.

func checkAPIHealth(w http.ResponseWriter, r *http.Request) {
    apiURL := "http://example.com/api/health"

    ticker := time.NewTicker(2 * time.Second)
    defer ticker.Stop()

    for range ticker.C {
        resp, err := http.Get(apiURL)
        if err != nil {
            log.Printf("Failed to reach API: %v", err)
            continue
        }

        if resp.StatusCode >= 200 && resp.StatusCode <= 299 {
            log.Println("API is healthy!")
        } else {
            log.Println("API is not healthy!")
        }

        resp.Body.Close()
    }
}

The next one was working with arrays and slices. Again, not an unknown construct in other languages. It was just fun playing around with them and they're intuitive.

package main

import (
    "fmt"
    "sync"
)

type Task struct {
    ID        int
    Processed bool
}

func processTasks(tasks []Task, wg *sync.WaitGroup) {
    defer wg.Done()
    for i := range tasks {
        fmt.Printf("Processing task ID: %d\n", tasks[i].ID)
        tasks[i].Processed = true
    }
}

func main() {
    tasks := make([]Task, 10)
    for i := range tasks {
        tasks[i].ID = i + 1 
    }

    var wg sync.WaitGroup

    mid := len(tasks) / 2
    firstHalf, secondHalf := tasks[:mid], tasks[mid:]
    
    wg.Add(1)
    go processTasks(firstHalf, &wg)

    wg.Add(1)
    go processTasks(secondHalf, &wg)

    wg.Wait()

    for _, task := range tasks {
        fmt.Printf("Task ID %d processed: %v\n", task.ID, task.Processed)
    }
}

The slices act as pointers to the allocated range of values within the example array. Which reduces the number of allocations you need to do for an example like this, of a simple batch processing.


The null in Go is nil. Why this name, no clue. So hence in the gimmicky section of this write-up. It does have some "nicer" side effects that I have somewhat of a strong opinion on. Nullable collections. A nil slice has a length and capacity of 0 and has no underlying array (official documentation). I was so happy when I saw this.

Conclusion

This all ties nicely together. With a simple "Hello world" example it was already clear one of the design ideas behind the language. Simplicity and readability. It was refreshing just creating a file and paste the example from official documentation and done. No IDE is needed to scaffold some of the things for you. Don't get me wrong, it is not like it is rocket science to do the same thing with dotnet these days. It has a bit more "fat" around your simple function.

package main

import "fmt"

func main() {
    fmt.Println("hello world")
}

It is fine to talk about all of this, but I would like to contrast it with the real world. And that would be, for most of us out there, a simple API implementation. There are many ways to organize your solution for this in Go, so I will just go with the one that makes sense to me. And things I tried to map my existing knowledge to when learning it. This flexibility enables you to go with any kind of "pattern" in your code organization so just do whatever feels right for you.

package controller

import (
	"net/http"

	"github.com/labstack/echo/v4"
	// Dto, services..
)

type ProductController interface {
	Get(context echo.Context) error
	List(context echo.Context) error
	Create(context echo.Context) error
	Update(context echo.Context) error
	Delete(context echo.Context) error
}

type productController struct {
	product service.ProductService
}

func NewProductController(repository repository.Repository) ProductController {
	return &productController{product: service.NewProductService(repository)}
}

func (controller *productController) Get(context echo.Context) error {
	id := context.QueryParam("id")
	product, err := controller.product.FindByID(id)

	if err != nil {
		return context.JSON(http.StatusBadRequest, err.Error())
	}

	return context.JSON(http.StatusOK, product)
}

func (controller *productController) List(context echo.Context) error {
	query := context.QueryParam("query")
	page := context.QueryParam("page")
	size := context.QueryParam("size")

	product, err := controller.product.FilterByDescription(query, page, size)
	
	if err != nil {
		return context.JSON(http.StatusBadRequest, err.Error())
	}

	return context.JSON(http.StatusOK, product)
}

// Rest of the functions

The example above is a piece of the code I wrote to implement some "dummy" API while learning the Echo. It is one of the web frameworks out there for Go. The first one I tried was Gin. There are others, whatever floats your boat.

This was me playing around and how to have the MVC kind of approach to building an API. There are simpler ways to implement it (think Minimal API in dotnet), which I may enjoy a bit more, but I was trying to see how this kind of pattern could be done within Go. And I must admit, this clicked pretty fast with me. Some things are different, but I also like that side of things.

This simplicity that drew me into the language does go a long way when combining all the pieces. It makes you think about composition and how things tie together to form a solution to a problem. I can just say that I wouldn't mind using this language in my next job. I still have a lot to learn and understand. The language is open and has a growing community and adaptation. Supported by one of the biggest players in tech today.

And with that, until next time gophers.