Golang for Python developers

Golang for Python developers

Python is one of my favorite languages. I use it for machine learning and some web development, usually, Python is more than sufficient for most tasks, however, when it comes to high-performance use cases such as building a complex ETL (Extract, Transform, and Load) pipeline service - you need that extra raw performance.

This is where Golang shines, it's not too complicated to learn and use; I see Golang as Python on steroids. The syntax is very minimal and clean, almost English-like.

In this guide, I will give you a general introduction to Golang from the perspective of a Python developer.

Golang is compiled

This may seem quite obvious, but it makes a huge difference in having code compiled versus being interpreted. Besides just compiler optimizations that can improve the performance of your code, compiled code also ensures types are enforced correctly, thus reducing the number of possible bugs that land up in production (Off-course unit testing is also a good idea).

Python is both strongly typed and dynamically typed. Take this example for instance:

def doMath(x, y):
    return x+y

If you had to run this code, Python will not throw any errors, however, try doing this:

def doMath(x, y):
    return x+y

print(doMath(1, "1"))

You should see:

return x+y
           ~^~
TypeError: unsupported operand type(s) for +: 'int' and 'str'

Without unit tests or static analysis in your IDE, Python code can create many runtime errors if you are not careful. In Golang, it's much harder to create these sorts of bugs because the program won't even compile, even if you are not calling that function or block of code.

Golang variables, arrays, maps

In Golang there are two main ways to create variables:

var someInteger int
# OR
someInteger := 10

You can explicitly set the type using "var" or you can let Go figure out the type by using this operator: ":=". Either way, once you have declared the variable, you can not change its type.

In Python, the following will work fine, but Golang will throw a type error:

x = 1
x = "Some string"

Similar to Python, Golang has a couple of container types:

# Python List: items = [1,2,3,4]

# Golang slice
items := []int{1,2,3,4}

#Python dictionary: carData = {"make": "Toyota", "model": "Corolla"}

# Golang maps
carData := make(map[string]string)
carData["make"]  = "Toyota"
carData["model"] = "Corolla"

In Golang there is no "None" type, however you can simply use "nil" instead. Here are some variable types:

var number int
var number int64
var number float64
var text string
... and most of the common types supported by most languages

var anything interface{}

An "interface{}" type is a special type that allows you to store any other type in that variable. It does not follow the same rules as regular types, it's a dynamic type, thus the compiler will not throw type errors for this type if you do something like:

    var anything interface{}
    anything = "Some text"
    anything = 101
    fmt.Println(anything)

This is terrible Go code and you should avoid using the interface type as far as possible. One common use case for the interface type is handling JSON objects, since JSON can be loosely structured where fields could potentially be a number or text or even an object depending on some condition, you can use this type for these dynamic fields so that the JSON is parsed without throwing type errors.

Here's an example of a dynamic JSON object:

{
   "product_name": "D-T42D15 42 Dled Television",
   "sku": "X42310555",
   "price": 2999.00,
   "currency": "ZAR"
}
{
   "product_name": "D-T42D15 42 Dled Television",
   "sku": 18745534,
   "price": 2999.00,
   "currency": "ZAR"
}

As you can see "sku" is a string sometimes and other times a number. This will cause typing errors in Golang because the type has to be a number or string, and cannot be both.

Therefore you can initially use an interface, and then in your code figure out what the current items data type is at runtime:

// For now, lets assume this function returns the
// product in the form of a struct (similar to python objects).
// We'll learn about structs in a bit...
product := getTheProduct()

// check what the type is and then create a new variable with
// the correct type.
theSKU := ""
switch product.SKU.(type) {
    case string:
       theSKU = product.SKU.(string)
    case int:
        theSKU = fmt.Sprintf("%d", product.SKU)
}

fmt.Println(unpackedValue)

Golang loops, if, switch, and range

Golang is very minimal, thus the language doesn't have as many reserved keywords as Python. For example, the "while" keyword does not exist! Instead, Golang's for loop is very flexible and can be used for a wide variety of loops that other languages generally separate into different reserved keywords.

Here are some examples:

start := 1
end := 10

for start < end {
    start += 1
    fmt.Println(start)
}

// this is the most common Loop. Use when you want to iterate
// over lists or maps.
items := []int{1,2,3,4}
for k, v := range items {
   fmt.Println(k,v)
}

sum := 0
for i := 1; i < 5; i++ {
    sum += i
}
fmt.Println(sum)

// GORM db rows: https://gorm.io/docs/sql_builder.html
rows := db.Rows()

for rows.Next() {
   var name string
    rows.Scan(&name)
    fmt.Println(name)
}

"If" statements are very similar to Python:

age := 22

if age < 18 {
   fmt.Println(num, "You're still a kid.")
} else if age > 18 {
   fmt.Println(num, "You can both drink, and drive. 
     Just not at the same time!")
} else {
   fmt.Println(num, "Sorry, but your youth is slowly fading :-(")
}

In early versions of Python (I think before v3.10), there were no switch statements, however, nowadays you can use "match" and "case". Luckily for Go developers, these exist and work similar to most other modern languages:

age := 22

switch age {
    case 18:
        fmt.Println("You're on your way to adulthood.")
    case 65:
        fmt.Println("#Retirement")
}

In Golang the "break" keyword is not required, Go will automatically break out of the switch on the first positive match. The "break" statement is usually used if your code inside the case block has an IF statement or some other logic that can take multiple pathways.

Golang modules

Modules are essential in any language; In Python creating modules is a breeze, you just use the directory and file name in your import statement to reference whatever code you need:

myapp/
   accounts/models.py

# Becomes
from accounts.models import User

Golang has a similar mechanism, however, it's a tad bit more complicated. The Go file name is usually irrelevant, instead in Go you need to declare a "package" directive at the top of every code file. To start a new Go project, you first have to run the following in your terminal inside the projects root folder:

go mod init mycompany.com/ecommerce

The above will create a "go.mod" file that is essentially your project's package information similar to NPM's "package.json" file. This file will also keep track of all the various dependencies you add to your project.

Next, in every folder where you add a ".go" file, you need to add a "package" directive at the top of the file that's the same name as the parent folder:

 myapp/
   accounts/models.go
main.go # the package for main.go will always be "package main"

Inside models.go:

package accounts

import "fmt"

func PrintHelloWorld() {
    fmt.Println("Hello World")
}

To import this function in our main.go file:

package main
import "mycompany.com/ecommerce/accounts"

func main() {
   accounts.PrintHelloWorld()
}

Your function or variable inside these modules must start with a capital letter to be visible outside of the module, thus if I named the function "printHelloWorld", Go will throw an "undefined" compile error in our main function.

ℹ️ If you created another file inside the module folder "accounts" e.g. utils.go with the "package accounts" directive at the top, that code file will still have access to "printHelloWorld".

To compile a go program you can run the following in your terminal:

go run main.go

# Or if you just want to build the binary without running
 go build -o myapp

Packages can also be imported from GitHub:

package main
import "github.com/labstack/echo/v4"

If you have multiple imports, you should group them as follows:

import (
  "github.com/labstack/echo/v4"
  "github.com/labstack/echo/v4/middleware"
  "net/http" # These style packages are from the standard library.
)

You can also provide an alias for a package if the name is too long to type or if you want to prevent collisions between packages with the same name:

import (
  "github.com/labstack/echo/v4"
  "github.com/labstack/echo/v4/middleware"
  comMiddleware "mycompany.com/ecommerce/middleware"
  "net/http"
)

When you add a git URL like the above to your import, you need to run the following go mod command before building or running the program:

go mod tidy

This is similar to pip, however, you do not need to have a "requirements.txt" file or need to specify each dependency individually. Go's package manager will automatically scan all your code files, look up the URL for that package, and download the relevant code.

Dependencies! What dependencies?

One common issue with Python is system libraries; when you run "pip install xyz", this just installs the Python library but you may still need to manage the system libraries (and also have Python installed on the target machine) via APT or whatever software installer is used by the target machine.

Golang on the other hand, usually does not need any dependencies installed on the target machine, you don't even need the Go compiler installed. Go will automatically generate a self-contained binary that contains everything you need to run the application.

If you have static assets like images or HTML templates, you would need to copy those as well to the target machine, however, Go also provides an embedder that can package everything including static assets into a single binary file.

When building Go binaries you can also specify what machine architecture you want to target via a simple ENV:

GOOS=windows go build

OOP is not a thing in Go

Golang does not work like other OOP languages, there are no classes and objects. Instead, Go has a nifty little "container" type called "structs". They are very similar to classes but much more lightweight.

Here's an example of a basic struct:

package main

package main

import "fmt"

type Product struct {
    ID       int64
    Name     string
    Sku      string
    Category string
    Price    float64
}

func main() {
    product := Product{
        ID:       123,
        Name:     "iPhone 14 pro",
        Sku:      "X41233",
        Price:    899,
        Category: "Smartphones",
    }

    fmt.Println(product)
}

ℹ️ Similar to modules, you need to capitalize the first letter of the struct field name otherwise that field will not be usuable on "instances" of the struct.

You can also assign values to struct "instance" one at a time:

    product := Product{}
    product.ID = 123
    product.Name = "iPhone 14 pro"
    product.Sku = "X41233"
    product.Price = 899
    product.Category = "Smartphones"

I keep using quotes around the word instances to emphasize that these are not objects, structs are just containers or collections, they are meant to be lean and efficient.

Structs can also have functions attached to them similar to methods in a class:

package main

import "fmt"

type Product struct {
    ID       int64
    Name     string
    Sku      string
    Category string
    Price    float64
}

func (p *Product) getPrice() float64 {
    return p.Price
}

func (p *Product) updatePrice(newPrice float64) {
    p.Price = newPrice
}

func main() {
    product := Product{}
    product.ID = 123
    product.Name = "iPhone 14 pro"
    product.Sku = "X41233"
    product.Price = 899
    product.Category = "Smartphones"

    product.updatePrice(999)
    fmt.Println(product.getPrice())
}

Note: The same rules apply with function names, if the first letter is not capitalized, the function will not be visible outside of the module it is declared in.

As you build more Go programs, you'll realize that conventional OOP is overrated and Go's minimal approach with structs just makes your code much cleaner and easier to maintain.

You'll notice I skipped past one piece of this code, the "*" character in front of "Product". This essentially is a pointer, instead of copying the product "instance" every time, we pass around a memory location to the original "instance".

Everything in Python is an object, thus the language is already doing this under the hood (passing by reference) so you never need to think about pointers in Python. Golang is just a bit more explicit allowing you to fine-grade control to optimize your code for maximum efficiency.

You can learn more about pointers in Golang and when to use them here.

Concurrency

In Python, "asyncio" can be used to achieve concurrency and speed up your program's execution time. It works great but has a little bit of a learning curve, not too complicated but not as natural as Go's goroutines.

In Golang, you can simply do this:

package main

import "time"

func doSomeWork() {
    time.Sleep(time.Second * 5)
}

func main() {
    go doSomeWork()
}

How simple is that? Just add "go" in front of any function call to make that task run in the background.

Okay, but just one caveat here! If your main program exits before the goroutine finishes, the function will be terminated prematurely. However, usually in Go, your main program would be some kind of background service like an HTTP server, in which case this is not an issue, since the server is always running.

An example HTTP server:

package main

import (
    "fmt"
    "log"
    "net/http"
)

func doSomeWork() {
    fmt.Println("Hello world from goroutine")
}

func main() {
    http.HandleFunc("/api/endpoint", func(w http.ResponseWriter, 
    r *http.Request) {
        go doSomeWork()
        fmt.Fprintf(w, "Hello, World")
    })

    log.Fatal(http.ListenAndServe(":8080", nil))
}

Since "http.ListenAndServe" basically blocks the program from exiting and keeps it in memory forever (unless of course, some fatal error kills the program), you can safely run the goroutine with minimal risk of it not finishing.

If you are running a program that will exit once it finishes all tasks, like a scraper, for example, you will need to stop and wait for all goroutines to finish. You can do this using wait groups:

import (
    "fmt"
    "sync"
)

func doSomeWork(wg *sync.WaitGroup) {
    defer wg.Done()
    fmt.Println("Hello world from goroutine")
}

func main() {
    var wg sync.WaitGroup
    for i := 1; i < 5; i++ {
        wg.Add(1)
        go doSomeWork(&wg)
    }

    wg.Wait()
}

In the above code sample, we first create a wait group:

var wg sync.WaitGroup

Next, in our loop, we "tell" our wait group how many processes we are going to add to the queue, in this case, we just run one goroutine but you can run several more depending on your needs.

wg.Add(1)
go doSomeWork(&wg)

Notice that we also pass "&wg" to the "doSomeWork" function. This is essentially passing the "wait group" by reference so that there is only one "instance" of the "wait group" regardless of how many goroutines we run.

See the power of pointers? We are maintaining a singleton, and thus ensuring the counter is always up to date and accurately matches the number of goroutines we spin up.

func doSomeWork(wg *sync.WaitGroup) {
    defer wg.Done()
    fmt.Println("Hello world from goroutine")
}

In the "doSomeWork" function as expected, we accept a pointer to the "wait group". If you look closely we use "defer" to tell the "wait group" that the function is complete and it should update the counter accordingly.

The "defer" keyword in Golang simply ensures that this line of code is always executed last just before the function finishes, regardless of where you call "defer" in your code.

You can have only one "defer" per function, however, "defer" can also run more than one line of code. Simply wrap your code in a nested function:

func doSomeWork(wg *sync.WaitGroup) {
    defer func() {
      wg.Done()
      fmt.Println("I am done")
    }() // <-- We use () to actually execute the function as
       //      soon as defer is executed.

    fmt.Println("Hello world from goroutine")
}

Coming back to our main code, we call "wg.Wait()" at the end of the main function to stop the program from exiting and wait until all goroutines finish.

func main() {
    var wg sync.WaitGroup
    for i := 1; i < 5; i++ {
        wg.Add(1)
        go doSomeWork(&wg)
    }

    wg.Wait()  // <--------------
}

Handling errors

package main

import (
    "fmt"
    "strconv"
)

func main() {
    var price float64
    price, err := strconv.ParseFloat("hello", 64)
    fmt.Println(price, err)
}

In most languages, you would expect this program to crash with some kind of "NaN" exception, in Python usually you would wrap this sort of logic in a "try...except" block so that you can safely catch the error and gracefully handle the error without crashing the whole program.

Go, on the other hand, does not have a "try...except" block, instead when an error occurs the function simply returns a struct containing more details about the error.

if "err" is nil, then your code executed fine without any errors and you can continue as normal. This may seem odd, but it's actually quite a nice way of handling errors and it keeps your code clean.

You can even choose to ignore the error if you want (although this is usually a bad idea):

package main

import (
    "fmt"
    "strconv"
)

func main() {
    var price float64
    price, _ := strconv.ParseFloat("hello", 64)
    fmt.Println(price, err)
}

The "_" character used in this fashion discards the return value and can be used with any function return type not just errors.

Should you do something silly like this, Go will just refuse to compile the program:

func main() {
    result := 1 / 0
}

Go can throw serious errors known as "panics" which will crash your program, however, these are not common and usually occur when something bad happens outside of your program, e.g. you have an SSD failure and Go cannot write to disk.

Dealing with this kind of error is beyond the scope of this article, but I will cover recovering from serious errors in an upcoming article where we'll take an even deeper dive into the land of Go.

Further learning resources

If you find Golang interesting and want to expand your knowledge further, here are some excellent resources to help you:

  • The official Golang docs are an excellent place to start.

  • Once you’ve mastered the basics, Effective Go is another essential guide I highly recommend.

  • If you are more of a visual learner, I recommend looking at Anything GG’s YouTube channel. He’s a highly experienced developer and has a wide variety of content for both beginners and experienced devs too.

Conclusion

Whoa! I have so much to share but this article has gotten way too long already. Please consider subscribing to my newsletter and you'll get notified as soon as the next article drops where we go deeper into Golang and learn more advanced concepts.

Golang is a beautiful well-designed language, and as you've seen, it's fairly English-like with very little noise, making it an ideal language to grow into coming from a Python background.

Hopefully, you've learned a thing or two from this article and will keep exploring Go further. Happy coding!