Files
build-web-application-with-…/tr/02.3.md
2016-08-29 22:10:15 +03:00

18 KiB
Raw Blame History

2.3 Kontrol ifadeleri ve fonksiyonlar

Bu bölümde, Go da kontrol ifadeleri ve fonksiyon işlemleri hakkında konuşacağız.

Kontrol ifadesi

Akış kontrolü programcılıkta en büyük icattır. Onun sayesinde, basit kontrol ifadeleri kullanarak daha karmaşık lojik ifadeler temsil edilebilir. Akış kontrolünü üç kategoriye ayırabiliriz: şartlı, döngü kontrollü ve şartsız çıkış.

if

if kelimesi büyük ihtimalle programlarında en sık geçen kelimedir. Eğer şartlar sağlanırsa birşeyler yapar, sağlanmazsa başka şeyler yapar.

if şartları için Go da paranteze gerek yoktur.

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

Go da çok işe yarar bir if kullanımı, şart ifadesinden önce değişken tanımlama yapmaktır. Bu değişkenin kapsamı sadece if blok alanı içerisinde geçerlidir.

// x'i tanımla, ve 10 dan büyük olup olmadığını kontrol et 
if x := computedValue(); x > 10 {
	fmt.Println("x is greater than 10")
} else {
	fmt.Println("x is less than 10")
}

// Bu satır derlenemez
fmt.Println(x)

Birden fazla şart için if-else kullanın.

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 da goto terimi mevcuttur fakat kullanırken dikkatli olun. goto programın kontrol akışını önceden belirlenmiş aynı gövde içindeki bir label a yönlendirir.

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

Label'ın adı büyük-küçük harfe duyarlıdır.

for

for Go da bulunan en güçlü kontrol lojiğidir. Datayı döngüsel olarak ve tekrarlı işlemlerle okuyabilir, while döngüsü gibi.

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

expression1, expression2 ve expression3 birer ifade olmak üzere, expression1 ve expression3 değişken tanımlama veya bir fonksiyondan dönen return olabilirken, expression2 ise kontrol ifadesidir. expression1 ifadesi döngüye girmeden önce bir kere işlenecektir. expression3 ise her döngü sonunda işlenir.

Örnekler düz yazıdan daha yararlı olacaktır.

package main
import "fmt"

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

Bazen birden fazla atama yapmak gerekir fakat Go bunun için kullanılacak bir , operatörü yoktur. Biz de i, j = i + 1, j - 1 gibi paralel atamalar yaparız.

İhtiyacımız yoksa expression1 ve expression1 ifadelerini çıkarabiliriz.

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

Hatta ; bile çıkarılabilir. Tanıdık geldi mi? Evet, tamamen while gibi oldu.

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

Döngülerde break ve continue adında iki önemli işlem vardır. break döngüden çıkartır ve continue o anki tekrarı atlar ve sonraki tekrara geçer. Eğer birden fazla iç içe döngüleriniz varsa break ile labelları kullabilirsiniz.

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 döngüsü range kullanıldığında slice ve map de bulunan datayı okuyabilir.

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

Go da birden fazla değer return yapılabildiği ve kullanılmayan bir değişken olduğunda derleyici hata verdiği için, kullanmak istemediğiniz değişkenler için _ kullanabilirsiniz.

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

switch

Bazı durumlarda çok fazla if-else kullandığınızı farkedebilirsiniz. Bu programı okumayı zorlaştırır ve gelecekte bakım yapmayı zorlaştırabilir. Bu durumda problemi çözmek için switch kullanmak mükemmeldir.

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

sExpr, expr1, expr2, ve expr3 ifadelerinin türleri aynı olmalıdır. switch çok esnektir. Şartlar sabit olmak zorunda değildir ve şart sağlanana kadar yukarıdan aşağıya doğru çalışır. Eğer switch in ardından bir ifade gelmiyorsa true olarak görülür.

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")
}

Beşinci satırdaki gibi case içinde birden fazla değer olabilir. case sonlarına break eklemeye gerek yoktur, şart sağlanıp işlem yapıldıktan sonra çıkacaktır. Eğer çıkmasını istemiyorsanız fallthrough ifadesini kullanarak bir sonraki şarta devam edebilirsiniz.

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 the following information.

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

Functions

func terimini kullanarak bir fonksiyon tanımlayın.

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

Yukarıdaki örnekten tahmin edebileceğiniz üzere aşağıda açıklamaları bulunur.

  • funcName adlı foonksiyonu tanımlamak için func terimini kullanın.
  • Fonksiyonlar sıfır veya daha fazla parametreye sahip olabilir. Parametrenin türü adından sonra gelir ve birden fazla parametre varsa , ile ayrılır.
  • Fonksiyonlar birden fazla değer döndürebilirler.
  • Bu örnekte output1 ve output2 adında iki değer döndürülmüş. Bunlara ad vermek zorunda değilsiniz, türünü yazmanız yeterli.
  • Eğer sadece bir değer döndürecekseniz parantez olmadan yazmalısınız.
  • Eğer en az bir değer döndürüyorsanız, fonksiyonun içinde istediğiniz yerde return terimini kullanmalısınız.

Şimdi pratik bir örnek görelim. (Maksimum değerini hesaplama)

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
}

Yukarıdaki örnek fonksiyon max da iki aynı tür parametre int olduğu için bir tane yazmak yeterli olur. Yani a int, b int yerine a, b int kullanılır. Birden fazla parametre için de aynı kural geçerlidir. Farkettiyseniz max fonksiyonu sadece bir değer döndürür ve zorunda olmadığımız için o değere bir isim vermedik, bu kısa halini kullandık.

Multi-value return

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

We'll use the following 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)
}

The above example returns two values without names -you have the option of naming them also. If we named the return values, we would just need to use return to return the values since they are initialized in the function automatically. Notice that if your functions are going to be used outside of the package, which means your function names start with a capital letter, you'd better write complete statements for return; it makes your code more readable.

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

Variadic functions

Go supports functions with a variable number of arguments. These functions are called "variadic", which means the function allows an uncertain numbers of arguments.

func myfunc(arg ...int) {}

arg …int tells Go that this is a 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 so any change will not affect to the original variable.

Let's see one example in order to prove what i'm saying.

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"
}

Can you see that? Even though we called add1 with x, the origin 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 are stored in memory and they have some memory addresses. So, if we want to change the value of a variable, we must change its memory address. Therefore the function add1 has to know the memory address of x in order to change its value. Here we pass &x to the function, and change the argument's type to the pointer type *int. Be aware that we pass a copy of the 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 the value of x in the functions. Why do we use pointers? What are the advantages?

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

defer

Go has a well designed keyword called defer. You can have many defer statements in one function; they will execute in reverse order when the program executes to the end of functions. In the case where the program opens some resource files, these files would have to be closed before the function can 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 being repeated several times. defer solves this problem very well. It doesn't only help you to write clean code but also makes your 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 defers, 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 the same signature can be seen as the same type.

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

What's the advantage of this feature? The answer is that it allows us to 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 testInt) []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 a function as type and the returned values and arguments of filter are the same as those of testInt. Therefore, we can have complex logic in our programs, while maintaining flexibility in our code.

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 the normal flow of programs and get into panic status. When a function F calls panic, F will not continue executing but its defer functions will continue to execute. Then F goes back to the break point which caused the panic status. The program will not terminate until all of these 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 errors.

Recover is a built-in function to recover goroutines from panic status. Calling recover in defer functions is useful because normal functions will not be executed when the program is in the panic status. It catches panic values if the program is in the panic status, and 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 retentions 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 functions are not able to have arguments or return values. Even though we can write many init functions in one package, I strongly recommend writing 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, the init function is optional, but package main has one and only one main function.

Programs initialize and begin execution from the main package. If the 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 importing packages, programs will initialize the constants and variables within the imported packages, then execute the init function if it exists, and so on. After all the other packages are initialized, programs will initialize constants and variables in the main package, then execute the init function inside the 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 is located within $GOROOT/pkg. Go supports third-party packages in two ways.

  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 the package name when you call functions inside of that package. Now fmt.Printf("Hello world") becomes to Printf("Hello world").

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

     import(
     	f "fmt"
     )
    

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

  3. _ operator. This is the operator that is difficult to understand without someone explaining it to you.

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

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