This repository has been archived by the owner. It is now read-only.
Permalink
Switch branches/tags
Nothing to show
Find file Copy path
8b5061e Jan 23, 2018
4 contributors

Users who have contributed to this file

@Unknwon @sbinet @muhfaris @LarryBattle
517 lines (372 sloc) 17.1 KB

2.3 Control statements and functions

In this section, we are going to talk about control statements and function operation in Go.

Control statement

The greatest inventions in programming language is flow control. Because of them, you are able to use simple control statements represent complex logic. There are three categories, conditional, cycle control and unconditional jump.

if

if is the most common keyword in your programs. If it meets the conditions then does something, does something else if not.

if doesn't need parentheses in Go.

if x > 10 {
	fmt.Println("x is greater than 10")
} else {
	fmt.Println("x is less than 10")
} 

The most useful thing of if in Go is that it can have one initialization statement before the conditional statement. The scope of variables which are defined in this initialization statement is only in the block of if.

// initialize x, then check if x greater than 
if x := computedValue(); x > 10 {
	fmt.Println("x is greater than 10")
} else {
	fmt.Println("x is less than 10")
}

// the following code will not compile
fmt.Println(x)

Use if-else for multiple conditions.

if integer == 3 {
	fmt.Println("The integer is equal to 3")
} else if integer < 3 {
	fmt.Println("The integer is less than 3")
} else {
	fmt.Println("The integer is greater than 3")
}

goto

Go has goto, be careful when you use it. goto has to jump to the label that in the body of same code block.

func myFunc() {
	i := 0
Here:   // label ends with ":"
	fmt.Println(i)
	i++
	goto Here   // jump to label "Here"
}

Label name is case sensitive.

for

for is the most powerful control logic in Go, it can read data in loops and iterative operations, just like while.

for expression1; expression2; expression3 {
	//...
}

expression1, expression2 and expression3 are all expressions obviously, where expression1 and expression3 are variable defination or return values from functions, and expression2 is a conditional statement. expression1 will be executed before every loop, and expression3 will be after.

An example is more useful than hundreds of words.

package main
import "fmt"

func main(){
	sum := 0;
	for index:=0; index < 10 ; index++ {
    	sum += index
	}
	fmt.Println("sum is equal to ", sum)
}
// Print:sum is equal to 45

Sometimes we need multiple assignments, but Go doesn't have operator ,, so we use parallel assignment like i, j = i + 1, j - 1.

We can omit expression1 and expression3 if they are not necessary.

sum := 1
for ; sum < 1000;  {
	sum += sum
}

Omit ; as well. Feel familiar? Yes, it's while.

sum := 1
for sum < 1000 {
	sum += sum
}

There are two important operations in loops which are break and continue. break jumps out the loop, and continue skips current loop and starts next one. If you have nested loops, use break with labels together.

for index := 10; index>0; index-- {
	if index == 5{
    	break // or continue
	}
	fmt.Println(index)
}
// break prints 10、9、8、7、6
// continue prints 10、9、8、7、6、4、3、2、1

for could read data from slice and map when it is used with range.

for k,v:=range map {
	fmt.Println("map's key:",k)
	fmt.Println("map's val:",v)
}

Because Go supports multi-value return and gives compile errors when you don't use values that was defined, so you may want to use _ to discard some return values.

for _, v := range map{
	fmt.Println("map's val:", v)
}

switch

Sometimes you may think you use too much if-else to implement some logic, also it's not looking nice and herd to maintain in the future. Now it's time to use switch to solve this problem.

switch sExpr {
case expr1:
	some instructions
case expr2:
	some other instructions
case expr3:
	some other instructions
default:
	other code
}

The type of sExpr, expr1, expr2, and expr3 must be the same. switch is very flexible, conditions don't have to be constants, it executes from top to down until it matches conditions. If there is no statement after keyword switch, then it matches true.

i := 10
switch i {
case 1:
	fmt.Println("i is equal to 1")
case 2, 3, 4:
	fmt.Println("i is equal to 2, 3 or 4")
case 10:
	fmt.Println("i is equal to 10")
default:
	fmt.Println("All I know is that i is an integer")
}

In fifth line, we put many values in one case, and we don't need break in the end of case body. It will jump out of switch body once it matched any case. If you want to continue to match more cases, you need to use statement fallthrough.

integer := 6
switch integer {
	case 4:
		fmt.Println("integer <= 4")
		fallthrough
	case 5:
		fmt.Println("integer <= 5")
		fallthrough
	case 6:
		fmt.Println("integer <= 6")
		fallthrough
	case 7:
		fmt.Println("integer <= 7")
		fallthrough
	case 8:
		fmt.Println("integer <= 8")
		fallthrough
	default:
		fmt.Println("default case")
}

This program prints following information.

integer <= 6
integer <= 7
integer <= 8
default case

Functions

Use the keyword func to define a function.

func funcName(input1 type1, input2 type2) (output1 type1, output2 type2) {
	// function body
	// multi-value return
	return value1, value2
}

We can get following information from above example.

  • Use keyword func to define a function funcName.
  • Functions have zero or one or more than one arguments, argument type after the argument name and broke up by ,.
  • Functions can return multiple values.
  • There are two return values named output1 and output2, you can omit name and use type only.
  • If there is only one return value and you omitted the name, you don't need brackets for return values anymore.
  • If the function doesn't have return values, you can omit return part.
  • If the function has return values, you have to use return statement in some places in the body of function.

Let's see one practical example. (calculate maximum value)

package main
import "fmt"

// return greater value between a and b
func max(a, b int) int {
	if a > b {
    	return a
	}
	return b
}

func main() {
	x := 3
	y := 4
	z := 5

	max_xy := max(x, y) // call function max(x, y)
	max_xz := max(x, z) // call function max(x, z)

	fmt.Printf("max(%d, %d) = %d\n", x, y, max_xy)
	fmt.Printf("max(%d, %d) = %d\n", x, z, max_xz)
	fmt.Printf("max(%d, %d) = %d\n", y, z, max(y,z)) // call function here
}

In the above example, there are two arguments in function max, their type are both int, so the first type can be omitted, like a, b int instead of a int, b int. Same rules for more arguments. Notice here the max only have one return value, so we only write type of return value, this is a short form.

Multi-value return

One thing that Go is better than C is that it supports multi-value return.

We use above example here.

package main
import "fmt"

// return results of A + B and A * B
func SumAndProduct(A, B int) (int, int) {
return A+B, A*B
}

func main() {
	x := 3
	y := 4

	xPLUSy, xTIMESy := SumAndProduct(x, y)

	fmt.Printf("%d + %d = %d\n", x, y, xPLUSy)
	fmt.Printf("%d * %d = %d\n", x, y, xTIMESy)
}

Above example return two values without name, and you can name them also. If we named return values, we just use return to return values is fine because they initializes in the function automatically. Notice that if your functions are going to be used outside the package, which means your functions name start with capital letter, you'd better write complete statement for return; it makes your code more readable.

func SumAndProduct(A, B int) (add int, Multiplied int) {
	add = A+B
	Multiplied = A*B
	return
}

Variable arguments

Go supports variable arguments, which means you can give uncertain number of argument to functions.

func myfunc(arg ...int) {}

arg …int tells Go this is the function that has variable arguments. Notice that these arguments are type int. In the body of function, the arg becomes a slice of int.

for _, n := range arg {
	fmt.Printf("And the number is: %d\n", n)
}

Pass by value and pointers

When we pass an argument to the function that was called, that function actually gets the copy of our variables, any change will not affect to the original variable.

Let's see one example to prove my words.

package main
import "fmt"

// simple function to add 1 to a
func add1(a int) int {
	a = a+1 // we change value of a 
	return a // return new value of a
}

func main() {
	x := 3

	fmt.Println("x = ", x)  // should print "x = 3"

	x1 := add1(x)  // call add1(x)

	fmt.Println("x+1 = ", x1) // should print "x+1 = 4"
	fmt.Println("x = ", x)    // should print "x = 3"
}

Did you see that? Even though we called add1, and add1 adds one to a, the value of x doesn't change.

The reason is very simple: when we called add1, we gave a copy of x to it, not the x itself.

Now you may ask how I can pass the real x to the function.

We need use pointers here. We know variables store in the memory, and they all have memory address, we change value of variable is to change the value in that variable's memory address. Therefore the function add1 have to know the memory address of x in order to change its value. Here we pass &x to the function, and change argument's type to pointer type *int. Be aware that we pass a copy of pointer, not copy of value.

package main
import "fmt"

// simple function to add 1 to a
func add1(a *int) int {
	*a = *a+1 // we changed value of a
	return *a // return new value of a
}

func main() {
	x := 3

	fmt.Println("x = ", x)  // should print "x = 3"

	x1 := add1(&x)  // call add1(&x) pass memory address of x

	fmt.Println("x+1 = ", x1) // should print "x+1 = 4"
	fmt.Println("x = ", x)    // should print "x = 4"
}

Now we can change value of x in the functions. Why we use pointers? What are the advantages?

  • Use more functions to operate one variable.
  • Low cost by passing memory addresses (8 bytes), copy is not an efficient way in both time and space to pass variables.
  • string, slice, map are reference types, so they use pointers when pass to functions as default. (Attention: If you need to change length of slice, you have to pass pointers explicitly)

defer

Go has a good design called defer, you can have many defer statements in one function; they will execute by reverse order when the program executes to the end of functions. Especially when the program open some resource files, these files have to be closed before the function return with errors. Let's see some examples.

func ReadWrite() bool {
	file.Open("file")
// Do some work
	if failureX {
    	file.Close()
    	return false
	}

	if failureY {
    	file.Close()
    	return false
    }

	file.Close()
	return true
}

We saw some code repeat several times, defer solves this problem very well. It doesn't only help you make clean code, and also make code more readable.

func ReadWrite() bool {
	file.Open("file")
	defer file.Close()
	if failureX {
    	return false
	}
	if failureY {
    	return false
	}
	return true
}

If there are more than one defer, they will execute by reverse order. The following example will print 4 3 2 1 0.

for i := 0; i < 5; i++ {
	defer fmt.Printf("%d ", i)
}

Functions as values and types

Functions are also variables in Go, we can use type to define them. Functions that have same signature can be seen as same type.

type typeName func(input1 inputType1 , input2 inputType2 [, ...]) (result1 resultType1 [, ...])

What's the advantage of this feature? So that we can pass functions as values.

package main
import "fmt"

type testInt func(int) bool // define a function type of variable

func isOdd(integer int) bool {
	if integer%2 == 0 {
    	return false
	}
	return true
}

func isEven(integer int) bool {
	if integer%2 == 0 {
    	return true
	}
	return false
}

// pass the function `f` as an argument to another function

func filter(slice []int, f func(int) bool) []int {
	var result []int
	for _, value := range slice {
    	if f(value) {
        	result = append(result, value)
    	}
	}
	return result
}

func main(){
	slice := []int {1, 2, 3, 4, 5, 7}
	fmt.Println("slice = ", slice)
	odd := filter(slice, isOdd)    // use function as values
	fmt.Println("Odd elements of slice are: ", odd)
	even := filter(slice, isEven) 
	fmt.Println("Even elements of slice are: ", even)
}

It's very useful when we use interfaces. As you can see testInt is a variable that has function type, and return values and arguments of filter are the same as testInt. Therefore, we have more complex logic in our programs, and make code more flexible.

Panic and Recover

Go doesn't have try-catch structure like Java does. Instead of throwing exceptions, Go uses panic and recover to deal with errors. However, you shouldn't use panic very much, although it's powerful.

Panic is a built-in function to break normal flow of programs and get into panic status. When function F called panic, function F will not continue executing, but its defer functions are always executing. Then F goes back to its break point where causes panic status. The program will not end until all the functions return with panic to the first level of that goroutine. panic can be produced by calling panic in the program, and some errors also cause panic like array access out of bounds.

Recover is a built-in function to recover goroutine from panic status, only call recover in defer functions is useful because normal functions will not be executed when the program is in the panic status. It catches panic value if the program is in the panic status, it gets nil if the program is not in panic status.

The following example shows how to use panic.

var user = os.Getenv("USER")

func init() {
	if user == "" {
    	panic("no value for $USER")
	}
}

The following example shows how to check panic.

func throwsPanic(f func()) (b bool) {
	defer func() {
    	if x := recover(); x != nil {
        	b = true
    	}
	}()
	f() // if f causes panic, it will recover
	return
}

main function and init function

Go has two retention which are called main and init, where init can be used in all packages and main can only be used in the main package. these two function are not able to have arguments or return values. Even though we can write many init function in one package, I strongly recommend to write only one init function for each package.

Go programs will call init() and main() automatically, so you don't need to call them by yourself. For every package, function init is optional, but package main has one and only one main function.

Programs initializes and executes from main package, if main package imports other packages, they will be imported in the compile time. If one package is imported many times, it will be only compiled once. After imported packages, programs will initialize constants and variables in imported packages, then execute init function if it exists, and so on. After all the other packages were initialized, programs start initialize constants and variables in main package, then execute init function in package if it exists. The following figure shows the process.

Figure 2.6 Flow of programs initialization in Go

import

We use import very often in Go programs as follows.

import(
	"fmt"
)

Then we use functions in that package as follows.

fmt.Println("hello world")

fmt is from Go standard library, it locates in $GOROOT/pkg. Go uses two ways to support third-party packages.

  1. Relative path import "./model" // load package in the same directory, I don't recommend this way.
  2. Absolute path import "shorturl/model" // load package in path "$GOPATH/pkg/shorturl/model"

There are some special operators when we import packages, and beginners are always confused by these operators.

  1. Dot operator. Sometime we see people use following way to import packages.

     import(
     	. "fmt"
     )

    The dot operator means you can omit package name when you call functions in that package. Now fmt.Printf("Hello world") becomes to Printf("Hello world").

  2. Alias operation. It changes the name of package that we imported when we call functions in that package.

     import(
     	f "fmt"
     )

    Now fmt.Printf("Hello world") becomes to f.Printf("Hello world").

  3. _ operator. This is the operator that hard to understand whitout someone explanning to you.

     import (
     	"database/sql"
     	_ "github.com/ziutek/mymysql/godrv"
     )

    The _ operator actually means we just import that package, and use init function in that package, and we are not sure if want to use functions in that package.

Links