12 min read

In this article by Vladimir Vivien author of the book Learning Go programming explains some basic control flow of Go programming language. Go borrows several of the control flow syntax from its C-family of languages.

It supports all of the expected control structures including if-elseswitchfor-loop, and even goto. Conspicuously absent though are while or do-while statements.

The following topics examine Go’s control flow elements. Some of which you may already be familiar and others that bring new set of functionalities not found in other languages.

  • The if statement
  • The switch statement
  • The type Switch

(For more resources related to this topic, see here.)

The If Statement

The if-statement, in Go, borrows its basic structural form from other C-like languages. The statement conditionally executes a code-block when the Boolean expression that follows the if keyword which evaluates to true as illustrated in the following abbreviated program that displays information about the world currencies.

import "fmt"

type Currency struct {
    Name    string
    Country string
    Number  int
}

var CAD = Currency{
    Name: "Canadian Dollar", 
    Country: "Canada", 
    Number: 124}

var FJD = Currency{
    Name: "Fiji Dollar", 
    Country: "Fiji", 
    Number: 242}

var JMD = Currency{
    Name: "Jamaican Dollar", 
    Country: "Jamaica", 
    Number: 388}

var USD = Currency{
    Name: "US Dollar", 
    Country: "USA", 
    Number: 840}

func main() {
  num0 := 242
  if num0 > 100 || num0 < 900 {
    mt.Println("Currency: ", num0)
      printCurr(num0)
  } else {
      fmt.Println("Currency unknown")
  }

    if num1 := 388; num1 > 100 || num1 < 900 {
      fmt.Println("Currency:", num1)
      printCurr(num1)
    }
}

func printCurr(number int) {
  if CAD.Number == number {
    fmt.Printf("Found: %+vn", CAD)
  } else if FJD.Number == number {
    fmt.Printf("Found: %+vn", FJD)
  } else if JMD.Number == number {
    fmt.Printf("Found: %+vn", JMD)
  } else if USD.Number == number {
      fmt.Printf("Found: %+vn", USD)
  } else {
    fmt.Println("No currency found with number", number)
  }
}

The if statement in Go looks similar to other languages. However, it sheds a few syntactic rules while enforcing new ones.

  • The parentheses, around the test expression, are not necessary. While the following if-statement will compile, it is not idiomatic:
    if (num0 > 100 || num0 < 900) {
      fmt.Println("Currency: ", num0)
      printCurr(num0)
    }

    Use instead:

        if num0 > 100 || num0 < 900 {
          fmt.Println("Currency: ", num0)
          printCurr(num0)
        }
  • The curly braces for the code block are always required. The following snippet will not compile:
    if num0 > 100 || num0 < 900 printCurr(num0)
    

    However, this will compile:

        if num0 > 100 || num0 < 900 {printCurr(num0)}
  • It is idiomatic, however, to write the if statement on multiple lines (no matter how simple the statement block may be). This encourages good style and clarity. The following snippet will compile with no issues:
    if num0 > 100 || num0 < 900 {printCurr(num0)}
    

    However, the preferred idiomatic layout for the statement is to use multiple lines as follows:

        if num0 > 100 || num0 < 900 {
          printCurr(num0)
        }
  • The if statement may include an optional else block which is executed when the expression in the if block evaluates to false. The code in the else block must be wrapped in curly braces using multiple lines as shown in the following.
    if num0 > 100 || num0 < 900 {
      fmt.Println("Currency: ", num0)
      printCurr(num0)
    } else {
      fmt.Println("Currency unknown")
    }
  • The else keyword may be immediately followed by another if statement forming an if-else-if chain as used in function printCurr() from the source code listed earlier.
    if CAD.Number == number {
      fmt.Printf("Found: %+vn", CAD)
    } else if FJD.Number == number {
      fmt.Printf("Found: %+vn", FJD)

The if-else-if statement chain can grow as long as needed and may be terminated by an optional else statement to express all other untested conditions. Again, this is done in the printCurr() function which tests four conditions using the if-else-if blocks. Lastly, it includes an else statement block to catch any other untested conditions:

func printCurr(number int) {
    if CAD.Number == number {
        fmt.Printf("Found: %+vn", CAD)
    } else if FJD.Number == number {
        fmt.Printf("Found: %+vn", FJD)
    } else if JMD.Number == number {
        fmt.Printf("Found: %+vn", JMD)
    } else if USD.Number == number {
        fmt.Printf("Found: %+vn", USD)
    } else {
        fmt.Println("No currency found with number", number)
    }
}

In Go, however, the idiomatic and cleaner way to write such a deep if-else-if code block is to use an expressionless switch statement. This is covered later in the section on SwitchStatement.

If Statement Initialization

The if statement supports a composite syntax where the tested expression is preceded by an initialization statement. At runtime, the initialization is executed before the test expression is evaluated as illustrated in this code snippet (from the program listed earlier).

if num1 := 388; num1 > 100 || num1 < 900 {
    fmt.Println("Currency:", num1)
    printCurr(num1)
}

The initialization statement follows normal variable declaration and initialization rules. The scope of the initialized variables is bound to the if statement block beyond which they become unreachable. This is a commonly used idiom in Go and is supported in other flow control constructs covered in this article.

Switch Statements

Go also supports a switch statement similarly to that found in other languages such as C or Java. The switch statement in Go achieves multi-way branching by evaluating values or expressions from case clauses as shown in the following abbreviated source code:

import "fmt"

type Curr struct {
    Currency string
  Name     string
  Country  string
  Number   int
}

var currencies = []Curr{
  Curr{"DZD", "Algerian Dinar", "Algeria", 12},
  Curr{"AUD", "Australian Dollar", "Australia", 36},
  Curr{"EUR", "Euro", "Belgium", 978},
  Curr{"CLP", "Chilean Peso", "Chile", 152},
  Curr{"EUR", "Euro", "Greece", 978},
  Curr{"HTG", "Gourde", "Haiti", 332},
  ...
}

func isDollar(curr Curr) bool {
  var bool result
  switch curr {
  default:
    result = false
  case Curr{"AUD", "Australian Dollar", "Australia", 36}:
    result = true
  case Curr{"HKD", "Hong Kong Dollar", "Hong Koong", 344}:
    result = true
  case Curr{"USD", "US Dollar", "United States", 840}:
    result = true
  }
  return result
}
func isDollar2(curr Curr) bool {
  dollars := []Curr{currencies[2], currencies[6], currencies[9]}
  switch curr {
  default:
    return false
  case dollars[0]:
    fallthrough
  case dollars[1]:
    fallthrough
  case dollars[2]:
    return true
  }
  return false
}

func isEuro(curr Curr) bool {
  switch curr {
  case currencies[2], currencies[4], currencies[10]:
    return true
  default:
    return false
  }
}

func main() {
  curr := Curr{"EUR", "Euro", "Italy", 978}
  if isDollar(curr) {
    fmt.Printf("%+v is Dollar currencyn", curr)
  } else if isEuro(curr) {
    fmt.Printf("%+v is Euro currencyn", curr)
  } else {
    fmt.Println("Currency is not Dollar or Euro")
  }
  dol := Curr{"HKD", "Hong Kong Dollar", "Hong Koong", 344}
  if isDollar2(dol) {
    fmt.Println("Dollar currency found:", dol)
  }
}

The switch statement in Go has some interesting properties and rules that make it easy to use and reason about.

  • Semantically, Go’s switch-statement can be used in two contexts:
    • An expression-switch statement
    • A type-switch statement
  • The break statement can be used to escape out of a switch code block early
  • The switch statement can include a default case when no other case expressions evaluate to a match. There can only be one default case and it may be placed anywhere within the switch block.

Using Expression Switches

Expression switches are flexible and can be used in many contexts where control flow of a program needs to follow multiple path. An expression switch supports many attributes as outlined in the following bullets.

  • Expression switches can test values of any types. For instance, the following code snippet (from the previous program listing) tests values of struct type Curr.
    func isDollar(curr Curr) bool {
      var bool result
      switch curr {
      default:
        result = false
      case Curr{"AUD", "Australian Dollar", "Australia", 36}:
        result = true
      case Curr{"HKD", "Hong Kong Dollar", "Hong Koong", 344}:
        result = true
      case Curr{"USD", "US Dollar", "United States", 840}:
        result = true
      }
      return result
    }
  • The expressions in case clauses are evaluated from left to right, top to bottom, until a value (or expression) is found that is equal to that of the switch expression.
  • Upon encountering the first case that matches the switch expression, the program will execute the statements for the case block and then immediately exist the switch block. Unlike other languages, the Go case statement does not need to use a break to avoid falling through the next case. For instance, calling isDollar(Curr{“HKD”, “Hong Kong Dollar”, “Hong Koong”, 344}) will match the second case statement in the function above. The code will set result to true and exist the switch code block immediately.
  • Case clauses can have multiple values (or expressions) separated by commas with logical OR operator implied between them. For instance, in the following snippet, the switch expression curr is tested against values currencies[2], currencies[4], or currencies[10] using one case clause until a match is found.
    func isEuro(curr Curr) bool {
      switch curr {
      case currencies[2], currencies[4], currencies[10]:
        return true
      default:
        return false
      }
    }
  • The switch statement is the cleaner and preferred idiomatic approach to writing complex conditional statements in Go. This is evident when the snippet above is compared to the following which does the same comparison using if statements.
    func isEuro(curr Curr) bool {
      if curr == currencies[2] || curr == currencies[4], 
          curr == currencies[10]{
        return true
       }else{
         return false
      }
    }

Fallthrough Cases

There is no automatic fall through in Go’s case clause as it does in the C or Java switch statements. Recall that a switch block that will exit after executing its first matching case. The code must explicitly place the fallthrough keyword, as the last statement in a case block, to force the execution flow to fall through the successive case block. The following code snippet shows a switch statement with a fallthrough in each case block.

func isDollar2(curr Curr) bool {
  switch curr {
  case Curr{"AUD", "Australian Dollar", "Australia", 36}:
    fallthrough
  case Curr{"HKD", "Hong Kong Dollar", "Hong Koong", 344}:
    fallthrough
  case Curr{"USD", "US Dollar", "United States", 840}:
    return true
  default:
    return false
  }
}

When a case is matched, the fallthrough statements cascade down to the first statement of the successive case block. So if curr = Curr{“AUD”, “Australian Dollar”, “Australia”, 36}, the first case will be matched. Then the flow cascades down to the first statement of the second case block which is also a fallthrough statement. This causes the first statement, return true, of the third case block to execute. This is functionally equivalent to following snippet.

switch curr { 
case Curr{"AUD", "Australian Dollar", "Australia", 36}, 
     Curr{"HKD", "Hong Kong Dollar", "Hong Koong", 344}, 
     Curr{"USD", "US Dollar", "United States", 840}: 
  return true
default:
   return false
}

Expressionless Switches

Go supports a form of the switch statement that does not specify an expression. In this format, each case expression must evaluate to a Boolean value true. The following abbreviated source code illustrates the uses of an expressionless switch statement as listed in function find(). The function loops through the slice of Curr values to search for a match based on field values in the struct passed in:

import (
  "fmt"
  "strings"
)
type Curr struct {
  Currency string
  Name     string
  Country  string
  Number   int
}

var currencies = []Curr{
  Curr{"DZD", "Algerian Dinar", "Algeria", 12},
  Curr{"AUD", "Australian Dollar", "Australia", 36},
  Curr{"EUR", "Euro", "Belgium", 978},
  Curr{"CLP", "Chilean Peso", "Chile", 152},
  ...
}

func find(name string) {
  for i := 0; i < 10; i++ {
    c := currencies[i]
    switch {
    case strings.Contains(c.Currency, name),
      strings.Contains(c.Name, name),
      strings.Contains(c.Country, name):
      fmt.Println("Found", c)
    }
  }
}

Notice in the previous example, the switch statement in function find() does not include an expression. Each case expression is separated by a comma and must be evaluated to a Boolean value with an implied OR operator between each case. The previous switch statement is equivalent to the following use of if statement to achieve the same logic.

func find(name string) {
  for i := 0; i < 10; i++ {
    c := currencies[i]
    if strings.Contains(c.Currency, name) ||
      strings.Contains(c.Name, name) ||
      strings.Contains(c.Country, name){
      fmt.Println("Found", c)
    }
  }
}

Switch Initializer

The switch keyword may be immediately followed by a simple initialization statement where variables, local to the switch code block, may be declared and initialized. This convenient syntax uses a semicolon between the initializer statement and the switch expression to declare variables which may appear anywhere in the switch code block. The following code sample shows how this is done by initializing two variables name and curr as part of the switch declaration.

func assertEuro(c Curr) bool { 
  switch name, curr := "Euro", "EUR"; { 
  case c.Name == name: 
    return true 
  case c.Currency == curr: 
    return true
  } 
  return false 
}

The previous code snippet uses an expressionless switch statement with an initializer. Notice the trailing semicolon to indicate the separation between the initialization statement and the expression area for the switch. In the example, however, the switch expression is empty.

Type Switches

Given Go’s strong type support, it should be of little surprise that the language supports the ability to query type information. The type switch is a statement that uses the Go interface type to compare underlying type information of values (or expressions). A full discussion on interface types and type assertion is beyond the scope of this section.

For now all you need to know is that Go offers the type interface{}, or empty interface, as a super type that is implemented by all other types in the type system. When a value is assigned type interface{}, it can be queried using the type switch as, shown in function findAny() in following code snippet, to query information about its underlying type.

func find(name string) {
  for i := 0; i < 10; i++ {
    c := currencies[i]
    switch {
    case strings.Contains(c.Currency, name),
      strings.Contains(c.Name, name),
      strings.Contains(c.Country, name):
      fmt.Println("Found", c)
    }
  }
} 

func findNumber(num int) {
  for _, curr := range currencies {
    if curr.Number == num {
      fmt.Println("Found", curr)
    }
  }
} 

func findAny(val interface{}) { 
  switch i := val.(type) { 
  case int: 
    findNumber(i) 
  case string: 
    find(i) 
  default: 
    fmt.Printf("Unable to search with type %Tn", val) 
  } 
}

func main() {
findAny("Peso")
  findAny(404)
  findAny(978)
  findAny(false)
}

The function findAny() takes an interface{} as its parameter. The type switch is used to determine the underlying type and value of the variable val using the type assertion expression:

switch i := val.(type)

Notice the use of the keyword type in the type assertion expression. Each case clause will be tested against the type information queried from val.(type). Variable i will be assigned the actual value of the underlying type and is used to invoke a function with the respective value. The default block is invoked to guard against any unexpected type assigned to the parameter val parameter. Function findAny may then be invoked with values of diverse types, as shown in the following code snippet.

findAny("Peso")
findAny(404)
findAny(978)
findAny(false)

Summary

This article gave a walkthrough of the mechanism of control flow in Go including if, switch statements. While Go’s flow control constructs appear simple and easy to use, they are powerful and implement all branching primitives expected for a modern language.

Resources for Article:


Further resources on this subject:


LEAVE A REPLY

Please enter your comment!
Please enter your name here