When I first read about Go and what it offers I decided to start and learn it. After some time I realized it has some amazing potential, and I wanted to share it here with you. Note that this is just a short blog post to show and explain what Go is and how it works, it's not supposed to be a Wikipedia article π
π About Go
Go is a statically typed and compiled programming language. Go is designed at Google by Robert Griesemer, Rob Pike, and Ken Thompson. Syntactically Go is very similar to C, but it has memory safety, garbage collection, structural typing and many other advantages. It's an easy language for developers to learn quickly.
π€ Why has Go been created?
Go was intended as a language for writing server programs that would be easy to maintain over time. It's now being used for writing light-weight microservices and is being used for generating APIs that will later on interact with the front-end. So in short words I could say that it was mainly created for APIs, web servers, frameworks for web applications, etc.
ποΈ Why is Go "so good"?
Its easy concurrency is really easy to make and include in your projects. As you may know, network applications are really dependent of concurrency, and that's why Go's networking features makes it even easier and better.
When declaring interfaces in Go, you don't need to add like in other language the keyword implements
or anything similar. Which means we can create an interface and simply make another object implement it, we simply need to make sure that all the methods from the interface are in the object implementing it, just like any other language.
When looking at Go's standard library we can see that we don't always specifically need to get a third-party library. It goes from parsing flags when executing your program to testing.
Since Go get compiled into machine code, we need a compiler. For Go, the compilation time is quite fast compared to some other languages, one reason for that is that Go does not allow unused imports. If you want a comparison to another language, Go is significantly faster than C++ at compilation time, however it also results in Go's binary to be bigger in terms of size.
And as always, who does not like programming languages that are cross-platform? Well, Go got you covered for that, from Android to Linux to Windows, just run go tool dist list
in your terminal to see it.
π€― How hard is Go?
This is a question you often get asked when you learn a new programming language and other people you know may want to learn it. For Go, it's a simple answer because the language itself is simple. Go's syntax is quite small compared to many other languages and therefore easier to remember. Most of the things can be remembered quite easily and this means you won't need to spend a lot of time at looking things up. You can start taking a look at Go here.
π A "Hello world!" in Go
A hello world is often included when you explain what a language is, so here it is:
package main
import "fmt"
func main() {
fmt.Println("Hello world!")
}
π What are packages?
Packages are a way to organize your code and to make it reusable. You can import packages from the standard library or from third party libraries. To import a package you can use the following syntax:
import "fmt"
This will import the package fmt
from the standard library. You can also import multiple packages at once:
import (
"fmt"
"math"
)
You can also import packages with a different name:
import (
"fmt"
m "math"
)
This will import the package math
but instead of using math
you can use m
to call the functions from the package.
Installing packages
To install a package you can use the following command:
go get <link>
The <link>
is the link to the package you want to install. For example, if you want to install the https://github.com/kkrypt0nn/spaceflake package you can use the following command:
go get github.com/kkrypt0nn/spaceflake
Usually the way to download the package is written in the README.md file.
π Did I hear web server?
Correct! Go is widely used for web servers and/or APIs. This can go from a very basic REST API to using Websockets. In comparison to other languages, Go does not need some overcomplicated web framework, Go's standard library already has this implemented and ready for us. Of course there are third party libraries that implements some additions.
To create a very basic web server we need to import two default libraries like the following:
import (
"fmt" // This is to give output when requesting a page (also used in the hello world example)
"net/http" // This is needed to handle requests, etc.
)
Now we can create a simple handler that will listen to the port 1337
and register a new /hello
route.
func helloWorld(response http.ResponseWriter, request *http.Request) {
fmt.Fprintf(response, "Hello from the Gopher side!")
}
func main() {
http.HandleFunc("/hello", helloWorld)
http.ListenAndServe(":1337", nil)
}
Now run the code with go run main.go
and go to 127.0.0.1:1337/hello.
In around 3-7 lines of code we managed to start a web server and create a route that will display some text.
Now it's up to you to make your creative projects :)
π₯ What is Go's concurrency?
Goroutines
Python developers may know what coroutines are, however to include them in your project you need an additional library called asyncio. Yes that's not a big deal as most of the Python features relies on libraries anyways.
The difference with Go, is that you can easily create a so called Goroutine to make functions run concurrently. Here's a very easy example on how to create a Goroutine:
package main
import (
"fmt"
"time"
)
func echo(s string) {
for i := 0; i < 5; i++ {
time.Sleep(100 * time.Millisecond)
fmt.Println(s)
}
}
func main() {
go echo("world")
echo("Hello")
}
As you may notice, I just had to add the keyword go
before calling the function to turn the function into a Goroutine. And of course I can call the echo()
function without having to mark it as a Goroutine. After running the code, here is a sample output I got:
Hello
world
world
Hello
world
Hello
Hello
world
world
Hello
You can clearly see, that the echo("Hello")
method runs without having to wait for the Goroutine to end, this is concurrency.
Channels
What makes Go's concurrency different and unique from different languages are so called channels. You can see channels as being pipes that transfer data. This is used to send values and data from one goroutine to another one, or just to get data back from a goroutine.
Take this example:
package main
import "fmt"
func sum(list []int, channel chan int) {
sum := 0
for _, value := range list {
sum += value
}
channel <- sum // Here we send the sum to the channel
}
func main() {
list1 := []int{1, 2, 3}
list2 := []int{9, 8, 7}
channel := make(chan int) // Here we create a channel that accepts integers
go sum(list1, channel)
go sum(list2, channel)
x := <-channel // Here we receive data from the channel
y := <-channel
fmt.Println(x, y, x+y) // Output: 24 6 30
}
This small example simply sums the numbers from a slice and puts the work between two goroutines so that they are being calculated concurrently and is therefore faster. Once both goroutines gave their output, it will calculate the final result and print it in the console. The arrows such as <-
simply describe from where to where the data goes, so either from the channel to the variable or from the variable in the channel.
π³ Does Go have objects?
Of course! There's just one slight difference here. Go doesn't directly have a type object
, but they have a type that matches the definition of a data structure that integrates both code and behavior. It's called a struct
. Let me show you a very simple example.
Structs
package main
import "fmt"
type Rectangle struct {
Width int
Height int
}
// The '(rect *Rectangle)' before the method name shows to which object the method will operate, in this case the 'rectangle' object.
func (rect *Rectangle) Area() int {
return rect.Width * rect.Height
}
func main() {
rect := Rectangle{Width: 10, Height: 5}
// As expected, the output is 50.
fmt.Println("Area: ", rect.Area())
}
Implicit inheritance
package main
import "fmt"
type Shape interface {
Area() float64
}
type Rectangle struct {
Width float64
Height float64
}
func (rect Rectangle) Area() float64 {
return rect.Width * rect.Height
}
func main() {
var s Shape = Rectangle{Width: 10, Height: 50}
fmt.Println(s.Area())
}
As you can see from the example above, the struct Rectangle
implements the interface Shape
but nowhere in the code it's clearly written, that it implements it.
πͺΒ Hello Jason!
Jason? Yes, JSON. JSON is widely used in web development and is a very easy way to send data between a client and a server. Go has a built-in library to handle JSON, so let's see how to use it.
Unmarshalling
Unmarshalling is the process of converting a JSON string into a Go object. So let's suppose we have an API that returns content like the following:
{
"name": "Gopher",
"age": 5,
"hobbies": [
"coding",
"eating",
"sleeping"
]
}
We can easily parse this JSON into a Go struct like the following:
package main
import (
"encoding/json"
"fmt"
)
// Gopher represents a gopher structure that will be parsed from JSON.
type Gopher struct {
Name string `json:"name"` // The 'json:"name"' is used to tell the JSON parser to use the 'name' field from the JSON.
Age int `json:"age"`
Hobbies []string `json:"hobbies"`
}
func main() {
jsonStr := `
{
"name": "Goophy",
"age": 5,
"hobbies": [
"coding",
"eating",
"sleeping"
]
}
`
// This variable will hold the parsed JSON data.
var gopher Gopher
// Here we parse the JSON string into the 'gopher' variable.
json.Unmarshal([]byte(jsonStr), &gopher)
fmt.Println(gopher) // {Goophy 5 [coding eating sleeping]}
fmt.Println("Name:", gopher.Name) // Name: Goophy
fmt.Println("Age:", gopher.Age) // Age: 5
for _, h := range gopher.Hobbies {
fmt.Println("Hobby:", h) // Hobby: coding/eating/sleeping
}
}
We can pretty much handle any JSON data with this method, as long as the JSON data matches the struct.
Marshalling
But what if your application doesn't need to interact with an external API because it is an API, you most likely want to convert your Go struct into JSON. This is called marshalling, and this is where the json.Marshal()
method comes in handy. Let's see how to use it:
package main
import (
"encoding/json"
"fmt"
)
type Gopher struct {
Name string `json:"name"` // The 'json:"name"' is used to tell the JSON marshaller to specifically use for the field name 'name' and not something random.
Age int `json:"age"`
Hobbies []string `json:"hobbies"`
SecretInformation string `json:"-"` // The '-' tells the JSON marshaller to ignore this field, we can still use it within our code. Alternatively, we can simply lowercase the field name, which will also tell the JSON marshaller to ignore it, though the field will not be exported.
}
func main() {
// Here we create our struct.
gopher := Gopher{
Name: "Goophy",
Age: 5,
Hobbies: []string{"coding", "eating", "sleeping"},
SecretInformation: "I am a secret agent for the Gopher Nation.",
}
// Here we convert the 'gopher' struct into JSON.
json, _ := json.Marshal(gopher)
fmt.Println(string(json)) // {"name":"Goophy","age":5,"hobbies":["coding","eating","sleeping"]}
}
Now that we have the JSON, we can repond to the API request with it.
𧬠Generics
As of Go 1.18 we now have generics support available, which is a great addition to the language. It's not as powerful as other languages, but it's a great start.
Here is some code that works perfectly fine without generics.
package main
import "fmt"
func getSumIntList(list []int) int {
sum := 0
for _, value := range list {
sum += value
}
return sum
}
func getSumFloatList(list []float64) float64 {
sum := 0.0
for _, value := range list {
sum += value
}
return sum
}
func main() {
myListOfInts := []int{1, 2, 3, 4, 5}
intSum := getSumIntList(myListOfInts)
fmt.Println(intSum) // 15
myListOfFloats := []float64{1.1, 2.2, 3.3, 4.4, 5.5}
floatSum := getSumFloatList(myListOfFloats)
fmt.Println(floatSum) // 16.5
}
Now this is all not so wonderful because we pretty much copy paste the exact same logic, just for two different data types. This is where generics come in handy. Let's rewrite the code to use generics.
package main
import "fmt"
func getSumList[T int | float64](list []T) T {
var sum T
for _, value := range list {
sum += value
}
return sum
}
func main() {
myListOfInts := []int{1, 2, 3, 4, 5}
intSum := getSumList(myListOfInts)
fmt.Println(intSum) // 15
myListOfFloats := []float64{1.1, 2.2, 3.3, 4.4, 5.5}
floatSum := getSumList(myListOfFloats)
fmt.Println(floatSum) // 16.5
}
We got the exact same output!
To explain that very easily, we first kind of define a new type T
which can be either an int
or a float64
- this is what [T int | float64]
does. We can of course go on and say we want to allow the type any
but that isn't ideal when using +=
like in the example and won't work as well.
The second part is where we say what type we want to have in our parameter - (list []T)
- this will make sure we provide a list that contains elements of the type T
which is, either int
or float64
.
Finally, we have the return value of the function to be of type T
as well.
That way we can reuse our function for different data types and don't have to write the same logic over and over again.
π Pointers
Similarly to C, Go has pointers as well. They are used to point to a memory address of a variable. This is useful when you want to change the value of a variable inside a function, but you don't want to return the value. You can simply pass the pointer to the variable and change the value inside the function. Let's take a look at an example without using pointers:
package main
import "fmt"
func changeValue(value int) {
value = 10
}
func main() {
x := 5
changeValue(x)
fmt.Println(x) // 5
}
As you can see, it will still print 5 and not 10, some people might expect it to print 10 though. The reason for that is because we didn't change the value of x
but instead we changed the value of the variable value
inside the function. To change the value of x
we need to use pointers.
package main
import "fmt"
func changeValue(value *int) {
*value = 10
}
func main() {
x := 5
changeValue(&x)
fmt.Println(&x) // 0x14000014090
fmt.Println(x) // 10
}
To understand it a bit better, I've added a print of &x
. This will print the memory address of x
which is 0x14000014090
. The *
in front of value
is called a dereference operator and it will get the value of the variable that the pointer points to. So in this case, it will get the value of x
and change it to 10. The &
in front of x
is called a reference operator and it will get the memory address of the variable x
. This is what we pass to the function so that we can change the value of x
inside the function, as it points to the memory.
We can also use pointers with custom structures as well.
package main
import "fmt"
type Person struct {
Name string
Age int
}
func changePersonAge(person *Person) {
person.Age = 10
}
func main() {
person := Person{
Name: "John",
Age: 20,
}
changePersonAge(&person)
fmt.Println(person.Age) // 10
}
The entire pointer story might be a bit confusing at first, hence I recommend to play around and see how it works.
π Popular open-source projects
As you might have expected, there are some really popular projects that are using Go. Here is just a small list among a lot of other projects:
π« How do I get started?
I'm glad you're interested in learning more about Go by yourself!
Installation
The installation is really easy regardless of the operating system you have, simply go to the download page and follow the instructions. Oh and remember, Go works on every operating system!
Learning resources
I don't really have specific learning resources but these are the things I've used to start to learn and code in Go:
- A tour of Go
- Codecademy
- Effective Go
- Go's Documentation
- Go by Example
- Sololearn (My favorite as you get a better overview of concurrency)
I'd be happy to see you become part of the Gophers!
Top comments (0)