Go lang conditionals, functions

Rajesh GurajalaRajesh Gurajala
13 min read

Conditionals:

if height > 6 {
    fmt.Println("You are super tall!")
} else if height > 4 {
    fmt.Println("You are tall enough!")
} else {
    fmt.Println("You are not tall enough!")
}

The Initial Statement of an If Block :

An if conditional can have an "initial" statement. The variable(s) created in the initial statement are only defined within the scope of the if body.

if INITIAL_STATEMENT; CONDITION {
}

It has two valuable purposes:

  1. It's a bit shorter

  2. It limits the scope of the initialized variable(s) to the if block

For example, instead of writing:

length := getLength(email)
if length < 1 {
    fmt.Println("Email is invalid")
}

We can do:

if length := getLength(email); length < 1 {
    fmt.Println("Email is invalid")
}

In the example above, length isn't available in the parent scope, which is nice because we don't need it there - we won't accidentally use it elsewhere in the function.

Here are some of the comparison operators in Go:

  • == equal to

  • != not equal to

  • < less than

  • > greater than

  • <= less than or equal to

  • >= greater than or equal to

Switch

Switch statements are a way to compare a value against multiple options. They are similar to if-else statements but are more concise and readable when the number of options is more than 2.

    var creator string

    switch os {
    case "linux":
        creator = "Linus Torvalds"
    case "windows":
        creator = "Bill Gates"
    case "mac":
        creator = "A Steve"
    default:
        creator = "Unknown"
    }

Notice that in Go, the break statement is not required at the end of a case to stop it from falling through to the next case. The break statement is implicit in Go.

If you do want a case to fall through to the next case, you can use the fallthrough keyword.

func getCreator(os string) string {
    var creator string
    switch os {
    case "linux":
        creator = "Linus Torvalds"
    case "windows":
        creator = "Bill Gates"

    // all three of these cases will set creator to "A Steve"
    case "macOS":
        fallthrough
    case "Mac OS X":
        fallthrough
    case "mac":
        creator = "A Steve"

    default:
        creator = "Unknown"
    }
    return creator
}

The default case does what you'd expect: it's the case that runs if none of the other cases match.


Functions

Functions in Go can take zero or more arguments.

To make code easier to read, the variable type comes after the variable name.

For example, the following function:

func sub(x int, y int) int {
    return x-y
}

Accepts two integer parameters and returns another integer.

Here, func sub(x int, y int) int is known as the "function signature".

Multiple Parameters

When multiple arguments are of the same type, and are next to each other in the function signature, the type only needs to be declared after the last argument.

Here are some examples:

func addToDatabase(hp, damage int) {
  // ...
}
func addToDatabase(hp, damage int, name string) {
  // ?
}
func addToDatabase(hp, damage int, name string, level int) {
  // ?
}

Declaration Syntax

Developers often wonder why the declaration syntax in Go is different from the tradition established in the C family of languages.

C Style Syntax

The C language describes types with an expression including the name to be declared, and states what type that expression will have.

int y;

The code above declares y as an int. In general, the type goes on the left and the expression on the right.

Interestingly, the creators of the Go language agreed that the C-style of declaring types in signatures gets confusing really fast - take a look at this nightmare.

int (*fp)(int (*ff)(int x, int y), int b)

Go Style Syntax

Go's declarations are clear, you just read them left to right, just like you would in English.

x int
p *int
a [3]int

It's nice for more complex signatures, it makes them easier to read.

f func(func(int,int) int, int) int

Passing Variables by Value

Variables in Go are passed by value (except for a few data types we haven't covered yet). "Pass by value" means that when a variable is passed into a function, that function receives a copy of the variable. The function is unable to mutate the caller's original data.

func main() {
    x := 5
    increment(x)

    fmt.Println(x)
    // still prints 5,
    // because the increment function
    // received a copy of x
}

func increment(x int) {
    x++
}

Ignoring Return Values

Remember if a function returns only a single value, it is simply written as

func f() int {}

But if it is returning multiple values, we write return types in brackets like,

func f () ( int, float64 ) {}

A function can return a value that the caller doesn't care about. or if a value is returned by a function but unused later, go throws an error that there is an unused variable in the program.

So we can explicitly ignore variables by using an underscore.

For example:

func getPoint() (x int, y int) {
    return 3, 4
}

// ignore y value
x, _ := getPoint()

Named Return Values & Naked return statements

Return values may be given names, and if they are, then they are treated the same as if they were new variables defined at the top of the function. These return values are known as “Named” return values.

A return statement without arguments returns the named return values. This is known as a "naked" return. Naked return statements should be used only in short functions. They can harm readability in longer functions.

Use naked returns if it's obvious what the purpose of the returned values is.

func getCoords() (x, y int) {
    // x and y are initialized with zero values

    return // automatically returns x and y
}

Is the same as:

func getCoords() (int, int) {
    var x int
    var y int
    return x, y
}

In the first example, x and y are the return values. At the end of the function, we could simply write return to return the values of those two variables, rather than writing return x,y.

The Benefits of Named Returns

→ Good Documentation of return values

Named return parameters are great for documenting a function. We know what the function is returning directly from its signature, no need for a comment.

Named return parameters are particularly important in longer functions with many return values.

func calculator(a, b int) (mul, div int, err error) {
    if b == 0 {
      return 0, 0, errors.New("can't divide by zero")
    }
    mul = a * b
    div = a / b
    return mul, div, nil
}

Which is easier to understand than:

func calculator(a, b int) (int, int, error) {
    if b == 0 {
      return 0, 0, errors.New("can't divide by zero")
    }
    mul := a * b
    div := a / b
    return mul, div, nil
}

We know the meaning of each return value just by looking at the function signature: func calculator(a, b int) (mul, div int, err error)

nil is the zero value of an error.

If there are multiple return statements in a function, you don’t need to write all the return values each time, though you probably should.

When you choose to omit return values, it's called a naked return. Naked returns should only be used in short and simple functions.

Explicit Returns

General case :

func getCoords() (x, y int) {
    return // implicitly returns x and y
}

Even though a function has named return values, we can still explicitly return values if we want to.

func getCoords() (x, y int) {
    return x, y // this is explicit
}

Using this explicit pattern we can even overwrite the return values:

func getCoords() (x, y int) {
    return 5, 6 // this is explicit, x and y are NOT returned
}

Early Returns

( also known as Guard clauses )

Go supports the ability to return early from a function. This is a powerful feature that can clean up code, especially when used as guard clauses.

Guard Clauses leverage the ability to return early from a function to make nested conditionals one-dimensional. Instead of using if/else chains, we just return early from the function at the end of each conditional block.

func divide(dividend, divisor int) (int, error) {
    if divisor == 0 {
        return 0, errors.New("can't divide by zero")
    }
    return dividend/divisor, nil
}

Error handling in Go naturally encourages developers to make use of guard clauses.

When I started writing code , I was frustated to see these many nested conditionals.

Let’s take a look at an exaggerated example of nested conditional logic:

func getInsuranceAmount(status insuranceStatus) int {
  amount := 0
  if !status.hasInsurance(){
    amount = 1
  } else {
    if status.isTotaled(){
      amount = 10000
    } else {
      if status.isDented(){
        amount = 160
        if status.isBigDent(){
          amount = 270
        }
      } else {
        amount = 0
      }
    }
  }
  return amount
}

Instead we can simply write same logic by:

func getInsuranceAmount(status insuranceStatus) int {
  if !status.hasInsurance(){
    return 1
  }
  if status.isTotaled(){
    return 10000
  }
  if !status.isDented(){
    return 0
  }
  if status.isBigDent(){
    return 270
  }
  return 160
}

The example above is much easier to read and understand. When writing code, it’s important to try to reduce the cognitive load on the reader by reducing the number of entities they need to think about at any given time.

In the first example, if the developer is trying to figure out when 270 is returned, they need to think about each branch in the logic tree and try to remember which cases matter and which cases don’t. With the one-dimensional structure offered by guard clauses, it’s as simple as stepping through each case in order.


Functions As Values

Go supports first-class and higher-order functions, which are just fancy ways of saying "functions as values". Functions are just another type -- like ints and strings and bools.

Let's assume we have two simple functions:

func add(x, y int) int {
    return x + y
}

func mul(x, y int) int {
    return x * y
}

We can create a new aggregate function that accepts a function as its 4th argument:

func aggregate(a, b, c int, arithmetic func(int, int) int) int {
  firstResult := arithmetic(a, b)
  secondResult := arithmetic(firstResult, c)
  return secondResult
}

It calls the given arithmetic function (which could be add or mul, or any other function that accepts two ints and returns an int) and applies it to three inputs instead of two. It can be used like this:

func main() {
    sum := aggregate(2, 3, 4, add)
    // sum is 9
    product := aggregate(2, 3, 4, mul)
    // product is 24
}

Anonymous Functions

Anonymous functions are true to form in that they have no name. They're useful when defining a function that will only be used once or to create a quick closure.

Let's say we have a function conversions that accepts another function, converter as input:

func conversions(converter func(int) int, x, y, z int) (int, int, int) {
    convertedX := converter(x)
    convertedY := converter(y)
    convertedZ := converter(z)
    return convertedX, convertedY, convertedZ
}

We could define a function normally and then pass it in by name... but it's usually easier to just define it anonymously:

func double(a int) int {
    return a + a
}

func main() {
    // using a named function
    newX, newY, newZ := conversions(double, 1, 2, 3)
    // newX is 2, newY is 4, newZ is 6

    // using an anonymous function
    newX, newY, newZ = conversions(func(a int) int {
        return a + a
    }, 1, 2, 3)
    // newX is 2, newY is 4, newZ is 6
}

Another example

/*
Complete the printReports function.
 It takes as input a sequence of messages, intro, body, outro.
 It should call printCostReport once for each message.

For each call of printCostReport, give it an anonymous function that returns 
the cost of a message as an integer. Here are the costs:

Intro: 2x the message length
Body: 3x the message length
Outro: 4x the message length

*/

package main

import "fmt"

func printReports(intro, body, outro string) {
    printCostReport(func(s string) int { return 2*len(s)} , intro)
    printCostReport(func(s string) int { return 3*len(s)} , body)
    printCostReport(func(s string) int { return 4*len(s)} , outro)
}

// don't touch below this line

func main() {
    printReports(
        "Welcome to the Hotel California",
        "Such a lovely place",
        "Plenty of room at the Hotel California",
    )
}

func printCostReport(costCalculator func(string) int, message string) {
    cost := costCalculator(message)
    fmt.Printf(`Message: "%s" Cost: %v cents`, message, cost)
    fmt.Println()
}

Defer

The defer keyword is a fairly unique feature of Go. It allows a function to be executed automatically just before its enclosing function returns. The deferred call's arguments are evaluated immediately, but the function call is not executed until the surrounding function returns.

Deferred functions are typically used to clean up resources that are no longer being used. Often to close database connections, file handlers and the like.

For example:

func GetUsername(dstName, srcName string) (username string, err error) {
    // Open a connection to a database
    conn, _ := db.Open(srcName)

    // Close the connection *anywhere* the GetUsername function returns
    defer conn.Close()

    username, err = db.FetchUser()
    if err != nil {
        // The defer statement is auto-executed if we return here
        return "", err
    }

    // The defer statement is auto-executed if we return here
    return username, nil
}

In the above example, the conn.Close() function is not called here:

defer conn.Close()

It's called:

// here
return "", err
// or here
return username, nil

Depending on whether the FetchUser function errored. (We'll cover errors later).

Defer is a great way to make sure that something happens before a function exits, even if there are multiple return statements, a common occurrence in Go.

Another example :

/*
Complete the bootup function. 
Notice that it potentially returns in three places. 
No matter where it returns, it should print the following message just before it returns:
"TEXTIO BOOTUP DONE"
*/

func bootup() {
    // ?
    ok := connectToDB()
    defer fmt.Println("TEXTIO BOOTUP DONE")
    if !ok {
        return
    }
    ok = connectToPaymentProvider()
    if !ok {
        return
    }
    fmt.Println("All systems ready!")
}

Block Scope

Unlike Python, Go is not function-scoped, it's block-scoped. Variables declared inside a block are only accessible within that block (and its nested blocks). There's also the package scope. We'll talk about packages later, but for now, you can think of it as the outermost, nearly global scope.

package main

// scoped to the entire "main" package (basically global)
var age = 19

func sendEmail() {
    // scoped to the "sendEmail" function
    name := "Jon Snow"

    for i := 0; i < 5; i++ {
        // scoped to the "for" body
        email := "snow@winterfell.net"
    }
}

Blocks are defined by curly braces {}. New blocks are created for:

It's a bit unusual, but occasionally you'll see a plain old explicit block. It exists for no other reason than to create a new scope.

package main

func main() {
    {
        age := 19
        // this is okay
        fmt.Println(age)
    }

    // this is not okay
    // the age variable is out of scope
    fmt.Println(age)
}

Fix the error here :

func splitEmail(email string) (string, string) {
    {
        username, domain := "", ""
    }
    for i, r := range email {
        if r == '@' {
            username = email[:i]
            domain = email[i+1:]
            break
        }
    }
    return username, domain
}

Closures

A closure is a function that references variables from outside its own function body. The function may access and assign to the referenced variables.

In this example, the concatter() function returns a function that has reference to an enclosed doc value. Each successive call to harryPotterAggregator mutates that same doc variable.

func concatter() func(string) string {
    doc := ""
    return func(word string) string {
        doc += word + " "
        return doc
    }
}

func main() {
    harryPotterAggregator := concatter()
    harryPotterAggregator("Mr.")
    harryPotterAggregator("and")
    harryPotterAggregator("Mrs.")
    harryPotterAggregator("Dursley")
    harryPotterAggregator("of")
    harryPotterAggregator("number")
    harryPotterAggregator("four,")
    harryPotterAggregator("Privet")

    fmt.Println(harryPotterAggregator("Drive"))
    // Mr. and Mrs. Dursley of number four, Privet Drive
}

Example 2

package main

import (
    "fmt"
)

func getFilterFunc(name string) func(string) bool {
    return func(input string) bool {
        return input == name
    }
}

func main() {
    // Create a filter function that matches "docker"
    filter := getFilterFunc("docker")

    // Test the filter
    inputs := []string{"kubernetes", "docker", "containerd"}

    for _, input := range inputs {
        if filter(input) {
            fmt.Printf("Matched: %s\n", input)
        } else {
            fmt.Printf("Not Matched: %s\n", input)
        }
    }
}

10
Subscribe to my newsletter

Read articles from Rajesh Gurajala directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Rajesh Gurajala
Rajesh Gurajala