Go流程与函数
流程控制
if
Go里面的if条件判断语句中不需要括号
if x > 10 { fmt.Println("x is greater than 10") } else { fmt.Println("x is less than 10") }
Go的if还有一个强大的地方就是条件判断句里面允许声明一个变量,这个变量的作用域只能在该条件逻辑块内,其他地方就不起作用了,如下代码:
if x := 5; x > 10 { fmt.Println("x is greater than 10") } else { fmt.Println("x is less than 10") } fmt.Println(x) // 会报错,x是在if条件的作用域里
多个条件的时候如下所示:
if integer := 4; 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 gerater than 3") }
goto
Go有goto语句----请明智的使用它.使用goto跳转到必须在当前函数内定义的标签.假如假设这样一个循环:
func myFunc() { i := 0 Here: //这行的第一个词,以冒号结束作为标签 println(i) i++ goto Here //跳转到Here去 }
for
Go里面最强大的一个控制逻辑就是for,它既可以用来循环读取数据,又可以当作while来控制逻辑,还能迭代操作.他的语法如下:
for experssion1; expression2; expression3 { //... }
expression1,expression2,expression3都是表达式,其中expression和expression3是变量或者函数调用返回值之类的,expression2是用来条件判断,expression1在循环开始之前调用,expression3在每轮循环结束之时调用.
一个例子比上面将那么多更有用,那么我们看看下面的例子吧
package main import "fmt" func main() { sum := 0 for index := 0; index < 10; index++ { sum += index } fmt.Println("sum is euqal to ", sum) } // 输出: sum is equal to 45
有些时候需要多个赋值操作,由于Go里面没有,操作符,那么可以使用平行赋值, i, j = i + 1, j -1
有些时候我们忽略expression1 和 expression3
sum := 1 for ; sum < 100; { sum += sum }
其中; 可以省略,那么就变成了如下代码了,是不是似曾相识?对,这就是while的功能
sum := 1 for sum < 100 { sum += sum }
在循环里面两个关键操作break和continue,break操作时跳出当前循环,continue时跳过本次循环.当嵌套过深的时候,break可以配合标签使用,即跳转至标签所指定的位置.
package main import "fmt" func main() { for index := 10; index > 0; index-- { if index == 5 { //break continue } fmt.Println(index) } }
//break 打印出来的是10,9,8,7,6
//continue打印出来的是10,9,8,7,6,4,3,2,1
break和continue还可以跟着标号,用来跳转到多重循环中的外层循环
for配合range何以用于读取slice和map的数据:
func main() { var arr [10]int for num := range arr { fmt.Println(num) } }
for k,v:=range map { fmt.Println("map's key:",k) fmt.Println("map's val:",v) }
由于Go支持"对返回值",而对于声明而未被使用的变量,编译器会报错,在这种情况下,可以使用_来丢弃不需要的返回值
for _, v := range map { fmt.Println(v) }
switch
有些时候你需要写很多if-else来实现一些逻辑,这个时候代码看上去很丑很冗长,而且不容易以后维护
这个时候switch很好的解决这个问题了.
switch sExpr { case expr1: some instructions case expr2: some other instructions case expr3: some other instructions default: other code }
sExpr和expr1,expr2,expr3的类型必须一致.Go的switch非常灵活,表达式不必是常量或整数,执行过程从上至下,知道找到匹配项,而如果switch没有表达式,他会匹配true
package main import "fmt" func main() { i := 10 switch i { case 1: fmt.Println("i is equal to 1") case 2,3,4: fmt.Print("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") } }
在第五行中,我们把很多值聚合了一个case里面,同时,Go里面switch默认相当于每个case最后带有break,匹配成功后不会自动向下执行其他case,而是跳出整个switch,但是可以使用fallthrough强制执行后面的case代码
integer := 6 switch integer { case 4: fmt.Println("The integer was <= 4") fallthrough case 5: fmt.Println("The integer was <= 5") fallthrough case 6: fmt.Println("The integer was <= 6") fallthrough case 7: fmt.Println("The integer was <= 7") default: fmt.Println("default case")
函数
函数里
函数是Go里面的核心设计,它通过关键字func来声明,他的格式如下:
func funcName(input1 type1, input2 type2) (output1 type1, output2 type2) { //这里是处理逻辑代码 //返回多个值 return value1, value2 }
上面代码可以看出:
关键字func用来声明一个函数funcName
函数可以有一个或者多个参数,每个参数后面带有类型,通过, 分割
函数可以有多个返回值
上面返回值声明了两个变量output1和output2,如果你不想声明也可以,直接就两个类型
如果只有一个返回值且不声明返回值变量,那么你可以省略包括返回值的括号
如果没有返回值,那么就直接省略最后的返回值
如果没有返回值,那么必须在函数的外层添加return语句
下面我们来看一个实际用函数的例子
package main import "fmt" func max(a, b int) int { if a > b { return a } return b } // 返回a, b中最大值 func main() { x := 3 y := 4 z := 5 max_xy := max(x, y) max_xz := max(x, z) fmt.Printf("max(%d, %d) = %d\n", x, y, max_xy) fmt.Printf("max(%d, %d) = %d\n", x, y, max_xz) fmt.Printf("max(%d, %d) = %d\n", y, z, max(y, z)) //也可以直接调用 }
上面这个里面我们可以看到max函数由两个参数,他们的类型都是int, 那么第一个变量的类型可以省略(即a,b int,而非a int, b int),默认为离他最近的类型,同理多于2个同类型的变量或者返回值.同时我们注意到它的返回值就是一个类型,这个就是省略写法
多个返回值
Go语言比C更先进的特性,其中一点就是函数能够返回多个值
我们直接上代码
package main import "fmt" 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) }
上面的例子我们可以看到直接返回了两个参数,当然我们也可以命名返回参数的变量,这个例子里面只是用了两个类型,我们也可以改成如下这样定义,然后返回的时候不用带上变量名,因为之家在函数里面初始化了.但如果你的函数是导出的(首字母大写),官方建议:最好命名返回值,因为不命名返回值,虽然是得代码更加简洁了,但是造成的生成文档可读性差.
func SumAndProduct(A, B int)(add int, Multiplied int) { add = A + B Multiplied = A * B return
变参
Go函数支持变参,接受变参的函数是有着不定量的参数的,为了做到这一点,首先需要定义函数是其接受变参
func myfunc(arg...int) {}
arg ...int 告诉Go这个函数接受不定数量的参数,注意:这些参数的类型全部都是int.在函数体中,变量arg是一个int的slice:
for _, n := range arg { fmt.Printf("And the number is: %d\n", n) }
传真与传指针
当我们传一个参数值到被调用函数里面时,实际上时传了这一个值的一份copy,当在被调用函数中修改数值的时候,调用函数中相应参数不会发生任何变化,因为数值变化只作用在copy上.
package main import "fmt" func add1(a int) int { a +=1 return a } func main() { x := 3 fmt.Println("x = ", x) x1 := add1(x) fmt.Println("x+1 = ", x1) fmt.Println("x = ", x) }
看到了吗?虽然我们调用了add1函数,并且在add1中执行a = a + 1操作,但是上面例子中x变量的值没有发生变化.
理由很简单:因为我们调用add1的时候,add1接受的参数其实是x的copy,而不是x本身.
那么你也许会问,如果传着个x本身该怎么办呢?
这就牵扯到了所谓的指针.我们知道,变量在内存中存放于一定的地址上,修改变量实际就是修改变量地址处的内存,只有add1函数知道x变量所在的地址,才能修改x变量的值.所以我们需要将x所在地址&x传入函数,并将函数的参数的类型由int改为*int,即改为指针类型,才能在函数中修改x变量的值.此时参数仍是按copy传递的,只是copy的是一个指针
package main import "fmt" //简单的一个函数,实现了参数+1的操作 func add1(a *int) int { // 请注意 *a = *a + 1 //修改了a的值 return *a //返回新值 } func main() { x := 3 fmt.Println("x = ", x) x1 := add1(&x) fmt.Println("x + 1 = ", x1) fmt.Println("x = ", x) }
这样,我们就达到了修改x的目的,那么到底传指针有什么好处呢?
传指针是得多个函数能操作一个对象
传指针比较轻量级(8byte),只是传内存地址,我们可以用指针传递体积大的结构体,如果用参数值传递的话,每次copy上面就会花费相对较多的系统开销(内存和时间).所以当你要传递大的结构体的时候,用指针是一个明智的选择.
Go语言中,channel,slice, map这三种类型的实现机制类似指针,所以可以直接传递,而不用去地址后传递指针,(注: 若函数需改变slice的长度,则仍需要取地址传递指针)
defer
Go语言中有中不错的设计,即延迟(defer)语句,你可以在函数中添加多个defer语句,当函数执行到最后时,这些defer语句会按照顺序执行,最后该函数返回,特别是当你在进行一些打开资源的操作时,遇到错误要提前返回,在返回前你需要关闭相应的资源,不然很容易造成资源泄露等问题.如下代码所示,我们一般写打开一个资源这样操作的.
package main func ReadWrite() bool { file.Open("file") if failureX { file.Colse() } if failureY { file.Close() return false } file.Close() return ture }
我们看上面有很多重复代码,Go的defer有效解决了这个问题.使用它后,不但代码量减少了分多,而且程序变得更优雅,在defer后指定的函数会在函数退出前调用
func ReadWrite() bool { file.Open("file") defer file.Close() if failureX { return false } if failureY { return false } return true }
如果有很多调用defer,那么defer是采用后进先出模式,所以如下代码会输出4 3 2 1
for i := 0; i < 5; i++ { defer fmt.Printf("%d ", i)
函数作为值,类型
在Go中函数也是一种变量,我们可以通过type来定义它,它的类型就是所有拥有相同的参数,相同的返回值的一种类型
type typeName func(input1 inputType1 , input2 inputType2 [, ...]) (result1 resultType1 [, ...])
函数作为类型到底有什么好处呢?那就是可以把这个类型的函数当作值来传递,请看:

package main import ( "fmt" ) type testInt func(int) bool // 声明一个函数类型 func isOdd(interger int) bool { if interger%2 == 0 { return false } return true } func isEven(interger int) bool { if interger%2 == 0 { return true } return false } //声明的函数类型在这个地方当作一个参数 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) fmt.Println(odd) even := filter(slice, isEven) fmt.Println(even) }
Panic和Recover
Go没有像java那样的异常机制,他不能抛出异常,而是使用了panic和recover机制,一定要记住,你应当把它作为最后的手段来使用,也就是说,你的代码中应当没有,或者很少由panic的额东西,这是个强大的工具,请明智的使用它,那么,我们应该如何使用它呢:
Panic
是一个内建函数,可以中断原有的控制流程,进入一个panic加状态,当函数F调用panic,函数F的执行被中断,用它的地方,再调用的地方,F的行为就像调用了panic,这一过程继续向上,知道发生panic的goroutine中所有调用的函数返回,此时程序退出.panic可以直接调用panic产生.也可以运行时错误产生,例如访问越界的数组.
Recover
是一个内建的函数,可以进入panic状态的goroutine恢复过来.recover尽在延迟函数中有效,在正常的执行过程中,调用recover会返回nil,并且没有其他任何效果,如果当前的gorotutine陷入panic状态,调用panic状态,调用recover可以捕获到panic的输入值,并且恢复正常的执行
下面这个函数演示了如何在过程中使用panic

func init() { if user == "" { panic("no value for %USER") } } // 下面这个函数检查作为其参数的函数在执行时是否或产生panic func throwsPanic(f func()) (b bool) { defer func() { if x := recover(); x != nil { b = true } }() f() // 执行函数f, 如果f中出现了panic,那么就可以恢复过来 return }
main函数和init函数
Go里面有两个保留的函数:init函数(能够应用于所用的package)和main函数(只能应用于package main).这两个函数在定义时不能由任何参数和返回值.虽然一个package里面可以任意多个init函数,但这无论是对于可读性还是以后的可维护性来说,我们强烈建议用于在一个package中每个文件只写一个init函数.
Go程序会自动调用init()和main(),所以你不需要在任何地方调用者两个函数,每个package中的init函数都是可选的,但package main就必须包含一个main函数.
程序的厨师胡和执行都其实于main包,如果main包还导入了其他的包,那么就会在编译时将他们依次导入,有时一个包会被多个包同时导入,那么他只会被导入一次.当一个包被导入时,如果该包还导入了其他的包,那么会先将其他包导入进来,然后在对这些包中的包级常量和变量进行初始化,接着这姓init函数(如果有的话),一次类推.等所有被导入的包都加载完毕了,就会开始对main包中的包级常量和变量进行初始化,然后执行main包中的init函数(如果存在的话),最后执行main函数,下图详细的解释了整个执行过程
****************插入图片*****************
import
我们在写Go代码的时候经常用到import这个命令来导入包文件,而我们经常看到的方式参考如下:
import( "fmt" )
然后我们可以这样调用
fmt.Println("hello world")
上面这个fmt时Go语言的标准库,其实是去GORROT环境变量指定目录下取加载该模块,当饭Go的import还支持如下两个方式加载自己写的模块:
相对路径
import "./model" //当前文件统一目录的model目录,但是不建议这种方式来import
绝对路径
import "shorturl/model" // 加载gopath/src/shorturl/model模块
上面展示了一些import常用的几种方式,但是还有些特殊的import, 让很多新手很费解,下面我们来一一讲解一下怎么回事
点操作
我们又是会看到如下的方式导入包
import ( ."fmt" )
这个点操作的含义就是这个包导入之后在你到用这个包的函数时,你可以省略前缀的包名,也就是前面你调用的fmt.Println("hello world")可以省略的写成Pirntln("hello world")
别名操作
别名操作顾名思义我们可以把包名命名成另一个我们用起来比较方便的名字
import ( f"fmt" )
别名操作的话调用包函数时前缀变成了我们前缀,即f.Println("hello world")
_操作
这个操作经常时让很多人费解的一个操作
import ( "database/spl" _ "github.com/ziutek/mymysq;/godrv" )
_操作其实时引入该包,而不直接使用包里面的函数,而是调用了该包里面的init函数.