DEV Community

Derek D.
Derek D.

Posted on • Updated on

Go v Python A Technical Deep Dive

Go v Python a Technical Deep Dive

Python is well known for its simple, natural language like syntax, and ease of use. Since the official Python language is written in C/C++ you might think Go, a language inspired by C with an emphasis on greater simplicity would have similar syntax to Python. I’ve found, Go is more similar to C++ than it is Python. That's not to say there are no
similarities, but they are fairly high level.

If you're a Python developer wanting to learn Go this is the article for you. My goal is for anyone comfortable with Python, can, by the end of the article, read, write, and understand Go well enough to write simple programs and understand the code
snippets they'll find on sites like Stack Overflow.

Before the technical deep dive, I’m going to briefly talk about the common use cases of each language, fundamental differences and the Go syntax rules that are likely to trip you up as you learn Go.

Use Cases

Python has found its place in scientific computing, data science, web development, artificial intelligence, and machine learning. Go was designed with a narrower set of use cases in mind, primarily distributed systems and applications that rely on running lots of tasks concurrently.

In general if your programming task deals with distributed systems, or heavily relies on concurrency Go is an excellent choice. For more general programming tasks or for analyzing data there is no better choice than python.

Fundamental Differences

The two big fundamental differences are:

  • Go is a compiled language while Python is an interpreted language.
  • Go is a statically typed language while Python is a dynamically typed language.

In case you are not familiar with the differences between statically, and dynamically typed languages here is what I mean when using these terms.

Statically typed languages require a variable’s data type to be declared explicitly in source code. Statically typed languages are often strongly typed, meaning once a variable has been assigned a data type that variable cannot switch data types. Dynamically typed languages on the other hand use type inferencing to determine a variable’s data type at runtime and are often weakly typed meaning the variables data type can change throughout the program.

Not all statically typed languages are strongly typed and not all dynamically typed languages are weakly typed, however in Go’s case it is statically and strongly typed and in Python’s case it is dynamically and weakly typed.

Syntax Differences

  • Go is statically and strongly typed while Python is dynamically and weakly typed.
  • Python allows strings to be surrounded by single quotes, or double quotes while Go requires strings to be surrounded by double-quotes. (i.e. "This is a string in Go" while 'this is a compiler error in Go')
  • Go uses true/false while Python uses True/False.
  • Comments in Go begin with // while in Python they begin with #.
  • Go enforces privacy of functions and properties while Python only supports privacy by convention using underscore for private and double underscore for really private.
  • Go doesn't care about indentation while Python uses indentation to determine scope.

Technical Deep Dive

Your journey into Go starts with the basic data types available in each language. You’ll work your way through variables, lists/arrays, dicts/maps, functions and classes/structs ending with concurrency and packaging.

Data Types

Go has a lot more data types than Python and Go's data types are a lot more specific. The table below shows the equivalent Python to Go data types.

Python Go
bool bool
str string
int int, uint, int8, uint8, int16, uint16, int32, uint32, int63, int64
float float32, float64
list array
dict map

NOTE: When you see a single Python type that corresponds to multiple Go types it means that one Python type can be used to represent any one of the Go types listed.

The takeaway is data types in Go (int8, float32, etc.) hint at the number of bits used to store values. Binary representation of numbers is beyond the scope of this article but there is an excellent write-up here if you are curious. For now, you can use the int, uint, and float64 data types in Go without experiencing many problems.


Declaring variables in Python is simple, and consistent. The variable name goes on the left of the assignment operator, =, and the value goes on the right. Go is a little more verbose because it requires a data type on the left hand sign of the = but all in all the syntax is similar.

a = 12
var a int = 12 // Variable `a` is of type int and has value 12

Go also supports type inferencing which is the mechanic Python uses to determine data types. In Go type inferencing is used when the keyword var replaces an actual data type, or the := operator is used in place of =.

var a = 12
a := 12

The := operator feels the most pythonic to me so I’ll use that in the rest of the examples. Occasionally you'll see the long-hand syntax which is either required or to exaggerate a point.

Assignment is the same between Python and Go.

a = 1337
a = 1337

That is until you want to change the type of the value stored. Python will let you overwrite the original variable with a new data type, but Go requires an explicit type conversion. Type conversions look a lot alike in Python and Go though.

a = 1337
a = 1337.0
# This is an explicit type conversion in Python
a = float(a)
a := 1337
c := float32(a)

It’s Go’s strong typing system that forces the converted value to be stored in a variable of the new type.

Just like Python, there are some conversions, such as converting an int to a list, that cannot be done automatically. The proper way to handle such a conversion is to construct a new instance of the object like type (i.e. list/array or dict/map) and use the converted value as an initial value which is a great lead into arrays v lists.

Arrays v Lists

The closest thing Go has to Python's lists are arrays. Arrays and lists both serve the same purpose, to hold a collection of items but they function very differently. The big differences are.

  1. Arrays can only hold items of the same type while lists can hold items of any type.
  2. Arrays have a fixed length while lists will grow as more items are added.

The first differences you’ll find between Arrays and Lists is that they are instantiated differently. Python is still straight forward having only one way to instantiate a list. Go gets a little bit complicated because arrays have capacity, and size.

shopping = ['coffee', 'milk', 'sugar']
var shopping [3]string
var shopping_v1 = []string {"coffee", "milk", "sugar"}
shopping_v2 := []string {"coffee", "milk", "sugar"}
shopping_v3 := [3]string {"coffee", "milk"}

All of the arrays in the Go example have a capacity of 3, but different sizes. The shopping array is an empty array, shopping_v1 and shopping_v2 have a size of 3, and shopping_v3 has a size of 2.

In Python, you won’t have to think about the difference between capacity and size. A list is always as big as it needs to be and the number of items in the list is the same as the length of the list (i.e. len(shopping)). With Go arrays, you will need to know and understand capacity and size. Capacity is the number of items the array CAN hold, while size is the number of items the array IS CURRENTLY holding. This table breaks down the capacity and size of each array in the previous example.

expressions capacity size
var shopping [3]string 3 0
var shopping_v1 = []string {"coffee", "milk", "sugar"} 3 3
shopping_v2 := []string {"coffee", "milk", "sugar"} 3 3
shopping_v3 := [3]string {"coffee", "milk"} 3 2

The items in a list/array can be retrieved or saved to a list/array by index as shown in the following code snippets. Note arrays and lists both start and index 0.

shopping = ['coffee', 'milk', 'sugar']
print(shopping[0]) # prints coffee
shopping[0] = 'decaf coffee' # Forgive me :-(
print(shopping) # prints ['decaf coffee', 'milk', 'sugar']
shopping := []string {"coffee", "milk", "sugar"}
fmt.Println(shopping[0]); # prints coffee
shopping[0] = "decaf coffee"
fmt.Println(shopping) // prints ["decaf coffee", "milk", "sugar"]

The most common way to add an item to a list in Python is calling the append() method. Go doesn’t allow items to be appended to an array because it would increase the capacity of the array which is static once the array has been declared. That being said if the size is less than capacity any unused space is filled by zero values and those zero values can be overwritten the same way another other value can be overwritten by using an index.

shopping = ["coffee", "milk", "sugar"]
shopping := string[4] {"coffee", "milk", "sugar"}
shopping[3] = "filters" // Arrays start at index 0

Items can be removed from a list in Python using the del keyword but in Go an item is removed by creating two slices of the array that exclude the item to be removed and saving those slices back to the original array.

shopping = ['coffee', 'milk', 'sugar']
del shopping[0] # Again forgive me I love coffee too
print(shopping) # prints ['milk', 'sugar']
shopping := [3]string {"coffee", "milk", "sugar"}
shopping = append(shopping[2], shopping[3:])
fmt.Println(shopping) // prints ["milk", "sugar"]

Slices are a common concept between Python and Go. The syntax is very similar but they behave completely different. In Python, a slice is a list made up of items from another list. It's a complete copy and what happens to the slice does not affect the original list.

In Go, a slice is a window into an array so anything done to the slice affects the underlying array, and changing the indexes of the slice only changes which items are being seen through the window.

Other than that slices have the same syntax.

  • [n] gets the item at the nth index
  • [n:] gets the items from the nth index through the last item and including the last item.
  • [n:m] gets the items from the nth index through the mth index excluding the item at the mth index.
  • [:m] gets the items from the 0th index through the nth index excluding the item at the nth index.

NOTE: Go does not support a third parameter in slices to specify the size of the step between items.

Map v Dict

When you need more than a numerical index into a collection of data the next step up is key, value pair storage. In Python you have dictionaries more commonly referred to as dicts, and in Go you have maps. Maps have the same limitation arrays had in that they are limited to storing items of the declared data type. Maps also have to be constructed using the make() function. make() handles allocating the appropriate amount of memory which is once again a detail you won't have to consider in Python.

ace_of_spades = { "suit": "spades", "value": "A"}
ace_of_spades = make(map[string]string)
ace_of_spades["suit"] = "spades"
ace_of_spades["value"] = "A"

The syntax for creating a map probably looks strange for Python developers. What you’ve seen from arrays can help simplify it though. [3]string, created an array capable of holding 3 strings, each of which could be accessed by an integer index. make(map[string]string) creates a map where each item can be accessed by a string index. The added bonus of maps is that they are not limited to a specific size the way arrays were and will continue to grow as more items are added.

Maps also can be constructed using the := operator when seeding the map with initial values.

ace_of_spades := map[string]string{"suit": "spades", "value": "A"}

You already got a sneak peek of adding a value to a map. In Go it is the same as adding a value to a dict in Python.

ace_of_spades = {"suit": "spades", "value": "A"}
ace_of_spades['numeric_value'] = 1
ace_of_spades = map[string]string{"suit": "spades", "value": "A"}
ace_of_spades["numeric_value"] = "14

Removing an item for a map is also quite similar to removing an item from a Python dict. In Python, you would use del while in Go you use the builtin delete function.

ace_of_spades = {'suit': 'spades', 'value': 'A'}
del ace_of_spades['value']
ace_of_spades := map[string]string{"suit": "spades", "value": "A"}
delete(ace_of_spades, "value")

Pythons dictionaries have the .get() function providing a safe way to check if a key exists. Maps also have a mechanism for checking if a key exists. It looks like this.

item, exists = ace_of_spades["does not exist"]

In this example exists would be false, and item would be an empty string. This same pattern works for all maps. The only thing that changes is what value is assigned to item. When the key exists in the map item is the value associated with that key, and exists is true. When the key does not exist exists is false, and item is the zero value for the data type being stored in the map.


Functions in Python and Go both allow multiple return values, support higher-order functions and use a specific keyword to declare a function. That keyword for Python is def and func in Go. The primary difference between the languages is that Go requires a data type declaration for function parameters and return values, while Python does not. Python’s type hinting is very similar to Go’s data type declarations so if you’ve been type hinting you’ll be one step ahead.

Here is an example showing the required data type declarations for a greeting function in Go and the same function in Python using type hints.

# Hints the function takes a string as a parameter and returns a string
def greeting(name: str) -> str:
    return f'Hello {name}!'

print(greeting('Derek')) # Prints Hello Derek!
print(greeting(12)) # Prints Hello 12!
func greeting(name string) string {
  return fmt.Printf("Hello %s!", name)

func main() {
  fmt.Println(greeting("Derek")) // Prints Hello Derek!
  fmt.Println(greeting(12)) // Cause a runtime error.

This example demonstrates Go’s enforced data typing compared to Python’s type hinting. Python doesn’t mind accepting an integer when the type hint is for a string, where Go encounters a runtime error when calling greeting(12).

In Go if a function will return multiple values both need a data type declaration and both return values need to be saved to their own variable. Python also allows multiple return values, however it will group them into a tuple unless each value is explicitly stored in it’s own variable. Handling multiple return values looks like this.

def greeting(name: str)-> Tuple(str,str):
    return 'Hello', name

result = greeting('World')
print(type(result)) # prints <type 'tuple'>
func greeting(name string) (string, string) {
   return "Hello", name

result_1, result_2 := greeting("World")
fmt.Printf("%T, %T", result_1, result_2) // Prints string, string

Both the Go and Python communities use the convention of storing a return value in a variable named _ can be ignored. It looks like this.

def greeting(name):
  return 'Hello', name

salutation, _ greeting('Derek')
func greeting(name string) (string, string) {
  return "Hello", name

salutation, _ = greeting("Derek")

Time to cover the advanced topics of higher-order functions, callbacks, and closures.

Although Python and Go both support high-order functions Go once again has more explicit and verbose syntax. In Python assigning a function to a variable (closure), passing a function as an argument (callback) and expecting a function as an argument
(higher-order function) is basically the same syntax as any other variable.

def english(name: str) -> str:
  return f'Hello {name}!'

def spanish(name: str) -> str:
  return f'Hola {name}!'

def greeting(lang, name: str) -> str:
    return lang(name)

en = english
es = spanish

print(greeting(en, 'Derek')) # Prints Hello Derek!
print(greeting(es, 'Derek')) # Prints Hola Derek!

In this example the variables en and es are closures, the lang parameter in the greeting function is the callback, and the greeting function itself is the higher-order function. Go has the same capabilities but is very explicit about the data types for
Parameters and return values not only for the function being passed but also the function it is being passed to. It’s easier to understand the data type declarations required by looking at code.

func english(name string) string {
  return fmt.Sprintf("Hello %s!", name)

func spanish(name string) string {
  return fmt.Sprintf("Hola %s!", name)

func greeting(lang func(string)string, name string) string {
  return lang(name)

func main() {
  en := english
  es := spanish

  fmt.Println(greeting(en, "Derek")) // Prints Hello Derek!
  fmt.Println(greeting(es, "Derek")) // Prints Hola Derek!

The same statements about the Python code are true for this code as well. en and es are closures, the lang parameter in the greeting function is a callback, and the greeting function itself is still the higher-order function. The only thing that has changed is the function declaration for greeting has become a monster. It’s stating the greeting function is expecting two parameters. The first is a function that takes in a string and returns a string and the second parameter is a simple string. The function declaration for greeting can be cleaned up using a function pointer.

Function pointers are exactly as they sound. They are pointers to functions. Another way to look at it is function pointers are variables that store references to a specific function, which is exactly what a closure is. The syntax for declaring a new function pointer looks like this.

type PointerNoParamNoReturn func()
type PointerIntParamNoReturn func(int)
type PointerIntAndStrParamsStrReturn func(int, string)string

All of these examples create function pointers, but each creates a new type that can only point to functions that match a specific signature. The last example which is the most complex creates the new type PointerIntAndStrParamsStrReturn which
is a function pointer that can point to any function whose first parameter is an int and whose second parameter is a string and returns a string. Basically PointerIntAndStrParamsStrReturn becomes an alias for func(int, string)string. Here is an example using a function pointer to clean up the monstrous function declaration from the previous greeting example.

type LangPtr func(string)string

def greeting(lang LangPtr, name string) string {
  return lang(name)

func main() {
  // Declares es and en variables of type lang_ptr (i.e. are function pointers)
  var en, es langPtr
  en = english
  es = spanish

  fmt.Println(greeting(en, "Derek")) // Prints Hello Derek!
  fmt.Println(greeting(es, "Derek")) // Prints Hola Derek!

Structs v Classes

Structs are a concept that comes from C, which was not designed to be an Object-Oriented language, so typical OO constructs like classes were omitted from its grammar. There was still a need to group variables together in a single block of memory which C accomplished with structs. C++ came after C adding in classes while still supporting structs. Then Python came after C++ doing away with structs altogether. When using a CPython implementation of Python structs are being used without you knowing it, but that's enough history.

A class in Python can define properties, and methods. Structs can only define properties, however, methods are still supported in the way of bolting a function on to a struct giving that function access to the struct’s properties and that’s basically a method. Here is an example of a simple Class and Struct definition for a Playing Card showing the syntactical differences in Go v Python.

class Card:
  def __init__(self, suit, value):
    self.suit = suit
    self.value = value

ace_of_spades = Card(1, 'S')
type Card struct {
  suit string
  value int

func main() {
  ace_of_spades := Card{1, "S"}

Go allows structs to be instantiated using positional arguments as shown in the above example, or with keyword arguments. When instantiating a struct with positional arguments or object literals as they are called in Go the first value goes into the first property defined, the second value into the second property, and so on. Object literals can take keywords as well which makes them act more like Python's **kwargs. It looks like this.

type Card struct {
  suit string
  value int

ace_of_spades := Card{value: 1, suit: "S"}

In this example the order of the values doesn't matter. Whatever value is provided for the suit keyword argument gets stored in the suit struct property and whatever value is provided for the value keyword argument gets stored in the value struct property.

Regardless if you use positional or keyword arguments to instantiate a struct, if you do not provide a value for one of the struct properties that property will implicitly get set to the zero value for that properties data type. This differs from Python which allows the developer to specify the default value for an unspecified argument.

class Card:
  def __init__(self, suit="", value=0):
      self.suit = suit
      self.value = value

suitless_king = Card(value=13)
valueless_heart = Card(suit="H")
type Card struct {
  suit string
  value int

suitless_king = Card{value: 13}
valueless_heart = Card{suit: "H"}

Once a class/struct has been instantiated properties are accessed the same way in Python and Go.

print(ace_of_spades.suit) # Prints S
fmt.Println(ace_of_spades.suit) // Prints S

In Python a function becomes a method when it is nested under a class declaration as shown below. Go bolts functions to a struct giving that function access to the struct's properties. The code below shows how a fold method can be added to a
Hand class/struct.

class Card:
  def __init__(self, suit, value):
    self.suit = suit
    self.value = value

class Hand:
  def __init__(self, cards: list): = cards

  def fold(self):
      card_1 = f"{[0].value}:{[0].suit}"
      card_2 = f"{[1].value}:{[1].suit}"
      print(f'Folded with hand: {card_1}, {card_2}')

my_hand = Hand([Card("S", 1), Card("H", 2)])
my_hand.fold() # Prints Folded with hand: 1:S, 2:H
type Card struct {
  suit string
  value int

type Hand struct {
  cards [2]Card

func (h Hand) fold() {
  card_1 := fmt.Sprintf("%d:%s",[0].value,[0].suit)
  card_2 := fmt.Sprintf("%d:%s",[1].value,[1].suit)
  fmt.Printf("Folded with hand: %s, %s", card_1, card_2)

func main() {
  ace_of_spades := Card{"S", 1}
  two_of_hearts := Card{"H", 2}
  my_hand := Hand{[2]Card{ace_of_spades, two_of_hearts}}
  my_hand.fold() // Prints Folded with hand 1:S, 2:H

The declaration of the fold function should feel odd to Python developers, however, it becomes a lot more familiar when h is changed to self.

func (self Hand) Fold() {
  card_1 := fmt.Sprintf("%d:%s",[0].value,[0].suit)
  card_2 := fmt.Sprintf("%d:%s",[1].value,[1].suit)
  fmt.Printf("Folded with hand: %s, %s", card_1, card_2)

That looks a lot more like Python. On that note did you know it's only convention to name the first parameter to a method self? Truthfully that parameter can be named anything, this, me, or something totally arbitrary like h. Here is the Python code using h instead of self.

class Hand:
  def __init__(h, cards: list): = cards

  def fold(h):
      card_1 = f"{[0].value}:{[0].suit}"
      card_2 = f"{[1].value}:{[1].suit}"
      print(f'Folded with hand: {card_1}, {card_2}')

Knowing that structs were taken from C, that Python is written partially in C, and how functions are bolted on to structs you should have a decent idea of what the underlying C/C++ code looks like when dealing with self.


Threads, multiprocessing and asynchronous I/O have been supported in Python since 3.4. Each has its own module that can be imported and used to run code asynchronously or in its own thread or process. There are no such modules in Go. It's actually quite the opposite. There is a sync package that helps make code synchronous because Go is inherently multithreaded and asynchronous. Every Go program is executed from a goroutine which is a light-weight thread managed by the runtime.

This is where Go is finally less verbose than Python. To start a new thread all you need to do is call go before a function call.

import threading
def sum(a, b):
    print(a + b)

t = threading.Thread(target=sum, args=(11, 1,))
t.start() # Kicks off a thread that prints 12
func sum(a int, b int) {
  fmt.Println(a + b)

go sum(11, 1) // Prints 12

Getting results from a thread, or passing values between threads has always been difficult. Go solves the problem with channels, abbreviated to chan. Using channels it is simple to pass values between goroutines. In Python, you have to use a ThreadPoolExecutor which isn't bad but is still more verbose.

import concurrent.futures

def sum(a, b):
  return a + b

with concurrent.futures.ThreadPoolExecutor(max_workers=2) as executor:
  future = executor.submit(sum, 11, 1)
func sum(a int, b int, c (chan int)) {
  c <- a + b // Sends the results of a+b to the channel c

func main() {
  int_channel := make(chan int) // Creates a channel for passing integers
  go sum(11, 1, int_channel)
  res := <-int_channel // Gets the value from int_channel and stores it in res
  fmt.Println(res) // Prints 12

When you pass a channel to another goroutine you are literally providing a communication channel between those 2 goroutines. Channels only have the one operator, <-, which states where the data is flowing from and to. Hint data flows in the
direction of the arrow. So in the sum function data is flowing into the channel, and in the main function data is flowing out of the channel into the res variable. At the risk of editorializing, I think this is easier to think about conceptually and looks cleaner too.

Concurrency is handled so differently between the 2 languages I'm going to stop comparing side by side code samples at this point. You should have a good enough grasp of goroutines and channels to prototype an implementation of the publisher/consumer pattern though.


Python and Go both use the project's directory structure for import paths. The concepts will feel familiar. A module is a file ending in .go or .py. A package is a directory on your file system and any module under a package is considered a module of that package. The big differences are

  1. Python allows granular imports of packages, modules, classes, functions, or variables while Go only supports importing a complete package including the modules, classes, functions, and variables exported by that package.
  2. Private is a real concept in Go, unlike Python which uses the _ and __ convention to indicate something is private but still giving access to that thing.
  3. Go requires the package name to be stated at the top of every module.
  4. The package name in the package declaration doesn't have to match the directory name.

Let's look at an example.

  |_ main.go
  |_ messages /
    |_ important.go

Given this directory structure, both Python and Go will use sampleapp as the main package name. The imports of and important.go look very different though. Here is what and main.go might look like.

from sampleapp.messages import important

import "sampleapp/messages"

func main() {

This tells you there is a function named important in and a function named Important somewhere in the sampleapp/messages Go package. Since important.go is the only module under sampleapp/messages ou know the Important function is declared in that module. You can also derive that the package declaration in important.go is package messages because the function is called with messages.Important. Remember the declared package name does not have to match the import path's package name.

There is a small subtlety hidden in this example. The Python function is important and the Go function is Important with a capital I. I've been very intentional to keep casing consistent until now so this example would stand out. I did this because Go by convention only exports functions classes and variables whose names start with a capital letter. Anything starting with a lower case letter is private. It's similar to Pythons convention that _ is private and __ is really private, but with Go, you cannot access private properties or methods.

Distributing Your Package

Real Python already has an excellent tutorial on
Publishing a Package to PyPi. If you want to know more about packaging check it out.

The essentials are, the source code should be stored in a code repository of some kind, and a metadata file describing name, version, dependencies, etc needs to be included. The metadata file in Python is and go.mod in Go. Once the source code is available through a public repository you have published your Go package and there is nothing left to do. With Python, you'll need to build the wheel file from source code, create a PyPi account, and use a tool called Twine to publish the wheel to PyPi.

Downloading & Using Packages

Published packages are easy to get in either language. With Python, it's pip install <package-name> while in Go its go get <package-url>.

Once a package has been downloaded it can be imported similarly to local packages and modules. For example, if you downloaded a package named haiku that generates random haikus. To import the package in Python it would be import haiku. In Go, it
depends where you got the package from. If this package was downloaded from my Github the go get command would be go get and the import would be import "". However, if it was stored on
Test Testoffersons Github the go get command would have been something like go get and the import would be import "". Regardless of where the package came from after being imported, it will be used like any other imported package.


Wow, thanks for sticking with me through all of that. You should be able to go play around with Go and be confident in what you are doing. Google has a self-guided Tour of Go which I highly recommend especially if you just want to play around with what you've learned in this article. Keep in mind the syntax differences from the beginning of this article I said that are likely to trip you up.

I'd love to hear from you! Hit me up @d3r3kdrumm0nd on Twitter with comments, questions, or if you just want to stay up to date with my next project.

Top comments (1)

antnieszka profile image

Python is strongly typed, not weakly :) weakly typed langs are PHP/JS.