Go lang conditionals, functions

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:
It's a bit shorter
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.
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 int
s and string
s and bool
s.
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 int
s 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:
Loops
If statements
Switch statements
Select statements
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)
}
}
}
Subscribe to my newsletter
Read articles from Rajesh Gurajala directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
