Cover image
Brian T. Liao
Apr 14, 2019 • 27 min read

Why You Should Use Golang in Interviews

Go Go Gopher!

So I’ve been practicing interviewing for the next recruiting cycle where most of it is LeetCode practice of data structures and algorithms questions. For interviews, Python and Java are two common languages to use. Python is nice because it very concise and has a lot of useful syntactic sugar like slicing or easy to use map and list data structure and Java is used because it is very flexible and well object-oriented abstracted that fits well with building and teaching data structures. Cracking the Coding Interview uses Java for that reason.

Why Go then?

Go is a good blend of the benefits of Python and Java and includes many more tiny perks that make it nice to interview in. Additionally, it was created to be as simple as possible and easy and great to learn!

Let’s get started!

1. Go is Incredibly Simple

Go was designed to be the love child of Python and C. In interviewing, simplicity matters, you often only have 45 to 60 minutes to think, explain, and write an algorithm to solve a problem and that is why Python works so well for it. Go gives you the simplicity and ease of Python while being able to have the finer grain control of C or Java without getting boggle down in unimportant implementation or wrapper details.

Here are some examples:

Hello World:

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

Whoa, simple right?

Two Sum:

func twoSum(nums []int, target int) []int {
    // map from int -> int
    m := make(map[int]int)
    
    // python like enumerate
    for i, num := range(nums) {
        // get value and check existence in map
        // and in if statement where ok is the boolean comparison value
        if index, ok := m[target - num]; ok { 
            // construct array and return values 
            // Go also has multiple returns, but LeetCode requested this format to stay consistent with other languages.
            return []int{index, i}
        }
        // python like set in map
        m[num] = i
    }
    return nil
}
// c like struct
type TreeNode struct {
    Value int
    Left, Right *TreeNode // pointers
}


func DfsInOrder(treeNode *TreeNode) {
    if treeNode == nil {
        return
    }
  
    // implicit dereferencing, c's ->
    // in-order: recursive traverse left, print current node value, recursive traverse right
    DfsInOrder(treeNode.Left)
    fmt.Println(treeNode.Value)
    DfsInOrder(treeNode.Right)
}

/*
      5
     / \
    1   2
        /
       7
DFS In-Order: 1, 5, 2, 7
*/
func main() {
    // get pointers to tree struct
    tree := &TreeNode{5,
        &TreeNode{1, nil, nil},
        &TreeNode{2, &TreeNode{7, nil, nil}, nil}}

    DfsInOrder(tree)
}

2. Go is Good for Data Structures

We’ve shown above how you could make a tree data structure. A linked list would be really similar.

struct LinkNode {
  Value int
  Prev, Next *LinkNode
}

Having pointers in trees and linked lists give you so much more finer-grain control in manipulating these data structures over the pass-by-values and hidden pointers in Python and Java. And it’s way easier to work with than C.

Go also has three minimal built in data structures, maps, arrays (fixed size), and slices (adjustable size like array) for the most expressiveness with the most simplicity. These are similar to Python built in maps and lists, and tuples can be imitated as arrays and sets as a map from a value to itself. Having data structures built in or easy to make makes it so much nicer than verbose Java, especially in a time crunch. Go’s multiple returns in their data structures are nice. For example as seen in the above twoSum:

index, ok := m[target - num]

This makes it extremely easy to handle when index exists (ok is true) and when index doesn’t.

3. Go has Types

Often when practicing interviewing in Python, I get tripped up on the lack of types and those small mess ups are hard to fix and debug in a high stress environment. Some examples are am I slicing a string or a list or is this one character variable a string or a int or a map or I don’t even know. Go has types in the most minimalist way. An example for a function is:

func sumSlice(slice []int) int {
    tot := 0
    for _, val := range slice {
        tot += val
    }
    return tot
}

Python:

def sum_lst(lst): 
    tot = 0
    for val in lst:
        tot += val
    return tot
    # or return sum(lst) ¯\_(ツ)_/¯

Ok, ignoring the Python 1 line-function-er, it’s pretty clean! Here’s some of the magic explained. sumSlice is a function that takes a slice (type []int) and returns an int. tot is type inferred to be an int so we don’t have to write it. range slice is like Python’s enumerate(slice), the type is inferred to be int for _ (ignore index) and int for val. All this magic means you have all the type information you need while still being pretty concise.

4. Go has Pointers

Ask me why pointers are important after you get asked to reverse a linked list

We’ve mentioned before how it can give finer control in manipulating pointer heavy data structures. Additionally, structs are now clearer in how memory is managed and can be more efficiently done when you want to change a struct or don’t want to fully copy a struct. It’s impressive in an interview to have fast and efficient memory management.

5. Go has no Generics

So this is an interesting design choice for Go. Coming from the Java 8 school of generics, generics are my bread and butter to beautiful abstraction (and over-verbose abstraction sometimes). Regardless of what you think of generics, removing them do make the language simpler. And for interviewing, generics usually don’t matter, you are usually manipulating one type such as ints, or strings where it is cleaner and easier to write code without generics. Sometimes over-abstraction isn’t necessary, especially in interviews.

Other Benefits

Most of what was stated above were benefits for classic data structures and algorithm interviews. There are other benefits too!

6. Go Impresses Interviewers

This honestly was what gave me the idea to do interviews in Go. Interviewers see so many people, you wonder how do you stand out? The answer: by using Go! If you’re equal to hundreds of other developers, interviewers probably aren’t going to remember the people that used Python or Java, but they might remember that one weirdo (genius) that used beautiful, succinct Golang.

Maybe after this article, everybody would use this and the advantage will be nullified and I’ll have to do something even more clever, like use OCaml or F#. I shall call this unending cycle the Efficient Interviewing Language Hypothesis.

7. Go has Excellent Concurrency Support

So what separates the 1x interviewees with the 10x interviewees? The answer is they can go beyond solving the algorithm questions and consider other hardware and actual implementation cases to solve the problem more efficiently. For example concurrency, caches, limited memory, multiple machines, etc.

Jane Street describes this in their interview guide in Think about Real Computers: “But for some jobs in systems development we do expect a fair amount of detailed knowledge, and in general it’s a big plus if you can take into account things like cache effects, IO patterns, memory representations, and the capabilities of real CPUs.”

Well it turns out Go was designed with concurrency in mind. Go was designed as a server/systems language, where being able to maximize the CPU’s performance is a huge goal. If you really want to impress in interviews, you could whip up some gorountines and make your algorithm faster!

Here’s an example of goroutines from A Tour of Go Equivalent Binary Trees:

// Walk walks the tree t sending all values
// from the tree to the channel ch.
func Walk(t *tree.Tree, ch chan int) {
    recursiveWalk(t, ch)
    close(ch) // close channel when done
}

func recursiveWalk(t *tree.Tree, ch chan int) {
    if t != nil {
        // in order
        recursiveWalk(t.Left, ch)
        ch <- t.Value
        recursiveWalk(t.Right, ch)
    }
}

// Same determines whether the trees
// t1 and t2 contain the same values.
func Same(t1, t2 *tree.Tree) bool {
    // create channels to message pass
    ch1 := make(chan int)
    ch2 := make(chan int)
    // create goroutines with channels
    go Walk(t1, ch1)
    go Walk(t2, ch2)

    for {
        // receive message from channels concurrently
        v1, ok1 := <-ch1
        v2, ok2 := <-ch2
        // something in the tree isn't the same
        if v1 != v2 || ok1 != ok2 {
            return false
        }
        // got to end with everything equal
        if !ok1 {
            break
        }
    }
    return true
}

Other examples you could try are parallel sums and parallel sorts for some fast theoretical asymptotic speedups.

A Parallel Sum

Interviewers are also impressed with synchronization, as it often leads to many hard bugs and is hard to do right. Go makes it really easy to have safe code, so if you can demonstrate your mutex abilities or make a worker pool, you’ll get some real brownie points right there.

8. Go Helps with Systems Design Questions

Another type of interview question is the system design question, which might ask you how you would organize a system to solve some sort of problem efficiently. Well Go is a server/systems language and makes it really easy to simply describe your architecture and make it efficient. For example, it’s easy to have HTTP endpoints and build an API in Go. If you have to have high performance, goroutines and synchronization are your friend. Go is used a lot in production for systems too, the most famous being Kubernetes.

9. You Have to Think When Writing Solutions from Other Languages

This point is a big help from practicing. Sometimes, I’m stuck and don’t know the answer. Luckily there’s a lot of resources, but most are in Java or Python. Go is pretty similar so it isn’t too hard to rewrite in Go, but having a different language makes you think about what you are rewriting instead of just mindlessly copying. That thinking is key to getting better.

10. You can get a job in Go

Hey, I like Go, it’s an awesome language. You know, Google (hint hint) or somebody, you could give me a job and let me keep using this awesome language.


So why Golang over Python or Java

We’ve gone over why one might use Go for programming interviews, but this doesn’t mean they would choose Go over your classic Python or Java (or JavaScript or C++ or whatever). Summarizing above, Go has the simplicity of Python but the fine grain control of C and the abstraction and structuring of Java. It’s like the company famous for it’s programming interviews designed a language perfect for programming interviews. This combined with the interviewer wow factor make Go a great language to use in programming interviews.


Go LeetCode Examples

Still need convincing? The best way to be convinced is to try it out! Here are some LeetCode examples.

Basic: Two Sum

Given a list (slice) nums, what are the indices of values that sum up to target? Return nil if none exist. This is the defacto interview question so it’s good you can glaze over how amazing Go is at it.

func twoSum(nums []int, target int) []int {
    // map from int -> int
    m := make(map[int]int)
    
    // python like enumerate
    for i, num := range(nums) {
        // get value and check existence in map
        // and in if statement where ok is the boolean comparison value
        if index, ok := m[target - num]; ok { 
            // construct array and return values 
            // Go also has multiple returns, but LeetCode requested this format to stay consistent with other languages.
            return []int{index, i}
        }
        // python like set in map
        m[num] = i
    }
    return nil
}

LeetCode Easy: Remove Duplicates from Sorted List

Given a sorted linked list, delete all duplicates such that each element appear only once. A pretty simple pointer solution.

/**
 * Definition for singly-linked list.
 * type ListNode struct {
 *     Val int
 *     Next *ListNode
 * }
 */
func deleteDuplicates(head *ListNode) *ListNode {
    // check head isn't nil (null)
    if head == nil {
        return nil
    }
    
    // two pointers head, and next node. algorithm is drop next node if it's equal to the first ptr's value.
    for ptr1, ptr2 := head, head.Next; ptr2 != nil; ptr2 = ptr2.Next {
        // equal
        if (ptr1.Val == ptr2.Val) {
            // drop next value
            ptr1.Next = ptr2.Next
            // set second pointer to be traversed to new next node
            ptr2 = ptr1
        } else {
            // traverse ptr1, since it's sorted if the next node is different, there are no more similar nodes.
            ptr1 = ptr1.Next
        }
    }
    // pointer to head
    return head
    
}

LeetCode Medium: Find First and Last Position of Element in Sorted Array

Given an array of integers nums sorted in ascending order, find the starting and ending position of a given target value. If the target is not found in the array, return [-1, -1].

Solve with two binary searches for starting and ending positions.

func searchRange(nums []int, target int) []int {
    // empty nums case
    if len(nums) == 0 {
        return []int{-1, -1}
    }
    // find starting and ending
    return []int{findStarting(nums, target), findEnding(nums, target)}
}

func findStarting(nums []int, target int) int {
    // binary search from 0 to len(nums) - 1
    lo := 0
    hi := len(nums) - 1

    for lo <= hi {
        mid := (lo + hi) / 2
        // < gets starting bound
        if nums[mid] < target {
            // too low, set lo to mid
            lo = mid + 1
        } else {
            // too high, set hi to mid
            hi = mid - 1
        }
    }
    // check lo valid
    if lo >= 0 && lo < len(nums) && nums[lo] == target {
        return lo
    }
    return -1
}

func findEnding(nums []int, target int) int {
    lo := 0
    hi := len(nums) - 1

    for lo <= hi {
        mid := (lo + hi) / 2
        // <= gets ending bound
        if nums[mid] <= target {
            lo = mid + 1
        } else {
            hi = mid - 1
        }
    }
    // check hi valid
    if hi >= 0 && hi < len(nums) && nums[hi] == target {
        return hi
    }
    return -1
}

LeetCode Hard: LRU Cache

Create a LRU Cache with O(1) get and set. Solution is to use a hash map to a linked list.

type LRUCache struct {
    capacity   int
    cache      map[int]*node // map
    head, tail *node         // linked list
}

type node struct {
    prev, next *node
    key, val   int
}

func Constructor(capacity int) LRUCache {
    return LRUCache{capacity: capacity, cache: map[int]*node{},
        head: nil, tail: nil}
}

func (this *LRUCache) Get(key int) int {
    node, ok := this.cache[key]
    // cache miss
    if !ok {
        return -1
    }
    // cache hit on most recently used; do nothing
    if this.head != nil && node.key == this.head.key {
        return node.val
    }
    // cache hit on least recently used; pop to front
    if this.tail != nil && node.key == this.tail.key {
        this.popTail()
        this.insert(node)
        this.cache[node.key] = node
        return node.val
    }
    // pop to front
    if node.prev != nil && node.next != nil {
        node.prev.next = node.next
        node.next.prev = node.prev
        this.insert(node)
    }

    return node.val
}

func (this *LRUCache) Put(key int, value int) {
    // cache has, pop to front
    if this.Get(key) != -1 {
        this.cache[key].val = value
        return
    }

    // cache doesn't have, insert node
    n := &node{key: key, val: value}

    this.cache[key] = n
    this.insert(n)
    // evict least recently used if over capacity
    if len(this.cache) > this.capacity {
        this.popTail()
    }

    return
}

func (this *LRUCache) insert(n *node) {
    // nothing inserted yet
    if this.tail == nil {
        this.tail = n
        this.head = n
        n.next = this.tail
        return
    }
    this.head.prev = n
    n.next = this.head
    this.head = n
}

func (this *LRUCache) popTail() {
    // remove key
    delete(this.cache, this.tail.key)
    // nothing in cache yet
    if this.tail.prev == nil {
        this.tail = nil
        return
    }
    // pop tail
    this.tail.prev.next = nil
    this.tail = this.tail.prev
}
Post by: Brian T. Liao