Why Go?
In April 2024, I decided it was time for me to learn a new language. I was torn between learning Go or Rust. I chose Go because of three things: speed, simplicity and concurrency.
Published at April 2024
Syntax
Golang has very ‘simple’ syntax, meaning you do not have to know all the language’s features to be able to know what is happening. Take a look at the examples below.
Variables
// To create a variable, you have a couple of options.
var name string
var name string = "Wout"
name := "Wout"
const name = "Wout"
The first and second declarations define a variable name
without and with an initial value, respectively.
The third option is a shorthand version of the second declaration, where Go infers the type from the assigned value.
Lastly, const
is used to define a constant, indicating that the value cannot be changed after initialization.
Functions
// To declare functions we can do the following
func getName(p Person) (string, error) {
if p.Name == "" {
return "", errors.New("Person has no name defined.")
}
return p.Name
}
Here, we declare a function using the func
keyword, followed by the function’s name. Within parentheses, we specify the parameters with their names and types. After the parentheses, we define the return type(s) of the function.
In this example, the function getName
takes a Person
struct as a parameter and returns a string representing the name and an error if the name is empty. If the name is not empty, it returns the name and a nil
error, indicating success.
Structs
type Person struct {
name, lastName string
age int
}
In Go, a struct is a composite data type that groups together variables of different types into a single unit. Here, we define a struct named Person
which has three fields: name
, lastName
, and age
. Each field is followed by its data type.
Structs provide a way to create more complex data structures by combining multiple variables into a single entity. In this example, Person
is a blueprint for creating objects that represent individuals with attributes such as name, last name, and age.
Structs and Methods
We can associate functions, called methods, with structs in Go. Here’s how we can define a method for the Person
struct:
func (p Person) getName() (string, error) {
if p.name == "" {
return "", errors.New("Person has no name defined.")
}
return p.name, nil
}
In this example, getName
is a method associated with the Person
struct. The syntax func (p Person)
before the method name indicates that this function operates on a Person
struct. Inside the method, p.name
refers to the name
field of the Person
struct.
While in languages like C#, similar functionality is achieved with extension methods, in Go, methods are associated directly with types through receiver functions. This approach allows for a more explicit declaration of the relationship between the method and the type.
Here is all the code combined.
type Person struct {
name, lastName string
age int
}
func (p Person) getName() (string, error) {
if p.Name == "" {
return "", errors.New("Person has no name defined.")
}
return p.name, nil
}
func main() {
p := Person {
name: "Wout",
lastName: "Hiemstra",
age: 25,
}
name, err := p.getName()
if err != nil {
fmt.Println(err)
}
fmt.Println("name: ", name)
}
There is much more to the language, but these are some examples that show how some of the basics of Go are used.
Concurrency
One of the best features that Go offers is concurrency. Out of the box, Go has a straightforward way of making your application concurrent. You might be wondering, though, why concurrency is important. Well, imagine that you open up your social media but had to wait for your neighbor’s request to finish. That’s not very great, right? That’s where concurrency comes in.
Take a look at the following code, this is not concurrent yet.
type Pokemon struct {
Name string `json:"name"`
Abilities []struct {
Ability struct {
Name string `json:"name"`
URL string `json:"url"`
} `json:"ability"`
IsHidden bool `json:"is_hidden"`
Slot int `json:"slot"`
} `json:"abilities"`
Types []struct {
Slot int `json:"slot"`
Type struct {
Name string `json:"name"`
URL string `json:"url"`
} `json:"type"`
} `json:"types"`
}
func FetchPokemonNormal(pokemon string) interface{} {
data := Pokemon{}
url := fmt.Sprintf("https://pokeapi.co/api/v2/pokemon/%s", pokemon)
resp, err := http.Get(url)
if err != nil {
fmt.Printf("Error fetching pokemon: %s %s\n", pokemon, err)
return data
}
if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
fmt.Printf("Error parsing pokemon: %s %s\n", pokemon, err)
return data
}
return data
}
Here I created a struct that can deserialize some basic properties that the Pokémon API gives us. (The api can be found here: link) When I run the following code:
func TestFetchNormal(t *testing.T) {
startNow := time.Now()
pokemon := []string{"pikachu", "onix", "machop", "charmander", "squirtle", "bulbasaur", "eevee", "jigglypuff", "snorlax"}
for _, pok := range pokemon {
FetchPokemonNormal(pok)
}
fmt.Println("It took the normal request: ", time.Since(startNow))
}
This a basic test that I set up. It loops through the Pokémon slice, and fetches the data from the API lets take a look at the results.
It took the normal request: 299.962041ms
PASS
ok blog-why-go 0.395s
As the results show, it took around 300ms to make the request for all three Pokémon. Let’s see how we can improve that.
Below you can find the concurrent code:
func FetchPokemonConcurrent(pokemon string, ch chan string, wg *sync.WaitGroup) interface{} {
data := Pokemon{}
defer wg.Done()
url := fmt.Sprintf("https://pokeapi.co/api/v2/pokemon/%s", pokemon)
resp, err := http.Get(url)
if err != nil {
fmt.Printf("Error fetching pokemon: %s %s\n", pokemon, err)
return data
}
defer resp.Body.Close()
if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
fmt.Printf("Error parsing pokemon: %s %s\n", pokemon, err)
return data
}
ch <- data.Name
return data
}
The changes made are:
- Added two new params,
ch
andwg
which are of typechan
andsync.Waitgroup
- below the
data
var declaration I added a defer function. When you defer a function you basically tell the function to wait till the surrounding function returns and then fire the deferred function. - After the API call we also defer the
resp.Body.Close()
function. - We assign the data that we got from the API to the channel.
These are all the changes that are required to make this function concurrent. If we now create a second test that uses the concurrent function like we do in the example below we can see how much faster it is.
func TestFetchConcurrent(t *testing.T) {
startNow := time.Now()
pokemon := []string{"pikachu", "onix", "machop", "charmander", "squirtle", "bulbasaur", "eevee", "jigglypuff", "snorlax"}
ch := make(chan string)
var wg sync.WaitGroup
for _, pok := range pokemon {
wg.Add(1)
go FetchPokemonConcurrent(pok, ch, &wg)
}
go func() {
wg.Wait()
close(ch)
}()
fmt.Println("It took the concurrent request: ", time.Since(startNow))
}
In the test you can see that I created two new variables that are required by the TestFetchConcurrent()
function.
- I adjusted the for loop by adding
wg.Add(1)
What this does is each time it loops through the Pokémon, it adds 1 to the total delta’s of the waitgroup. - I added the keyword
go
before the function this tells Go that this function can be called via go routines. - I created a shadow function which does the following:
- It waits until all requests are finished.
- It closes the channel so that it can no longer receive data.
With these changes made it’s time for the results! Let’s see, how fast the concurrent function is.
It took the normal request: 299.962041ms
It took the concurrent request: 13.75µs
PASS
ok blog-why-go 0.446s
The concurrent version is a lot faster!
I hope that with the given example above it shows that without much changing on the function we can still make it quite a lot faster by making it concurrent.
Speed
With speed, I don’t literally mean that the language is fast (the language is also fast). But rather the speed at which I can get something up and running.
Although I haven’t been coding for an extensive period, my work primarily involves C#. Don’t get me wrong, I enjoy working with C#. However, while working in C#, I often find myself pondering ways to abstract the tasks I’m performing.
In Go, abstraction is possible but differs from the approach in C#. Since I began delving into hobby projects with Go, I’ve noticed that I can quickly develop a working solution. This has been immensely satisfying.
To learn more about go here are some great resources: