第一个程序 HelloWorld
按照国际惯例,我们来实现 helloworld
package main import "fmt" func main() { fmt.Println("Hello,World") }
package main 标注程序位置
import "fmt" 导入 fmt 包,类似 python,包名需要使用 "" 圈起,如果引入的包太多,考虑使用 import ()
import ( "fmt" "time" "os" )
func main 定义函数名为 main 的函数,接着在函数里面使用 fmt.Println 函数输出 “hello,world”
变量和常量
在 golang 中,定义一个变量可以使用 var 语法
var name type = value
如果是在函数体内,你还可以使用更加简洁的命名方式
varName := value
变量的命名采用骆驼法,首字母小写,往后每个新单词的首字母大写。如 myString。如果想要定义的内容被其他外部包使用,则首字母也要大写
package main import "fmt" func main() { const c = 45.2 //常量 var myInt int = 23 myString := "hello" var myBool1,myBool2 bool myBool1 = true myBool2 = false fmt.Println(myInt,c) fmt.Printf("my string: %s, type: %T",myString,myString) fmt.Printf("my bool: %v,type %T",myBool1,myBool2) myBool1 = false //赋值语句,myBool1之前已经使用:=声明过了,所以直接使用= fmt.Println(myBool1) }
if,switch 和 for
if 语句可以如下定义:
if condition { }
if-else:
if condition { } else { }
if-else if-else
if condition { } else if condition2 { } else { }
在使用 if-else 时,必须保证 else 与 if 的} 在同一行,否则 else 无效
package main import "fmt" func main() { var first int = 10 var cond int if first <= 0 { fmt.Printf("first is less than or equal to 0\n") } else if first > 0 && first < 5 { fmt.Printf("first is between 0 and 5\n") } else { fmt.Printf("first is 5 or greater\n") } if cond = 5; cond > 10 { //此处cond = 5为赋值语句,等同于cond=5 if cond > 10 {} fmt.Printf("cond is greater than 10\n") } else { fmt.Printf("cond is not greater than 10\n") } }
switch 语句定义如下
switch var { case condition1: ..... case condition2: ..... case condition3: fallthrough default: ...... }
上述的 var,default,fallthrough,都是非必须选项,default 代表 switch 最后默认执行,fallthrough 代表在执行 case 后继续执行后续的 case
package main import "fmt" func main() { num := 10 switch num { case 1: fmt.Println("num is 1") case 10: fmt.Println("num is 10") default: fmt.Println("default") } }
试着修改上面的第二 case 为 12,看看输出结果是否不同?
当然我们在比较数值时,会把 num 放到 switch 里面
package main import "fmt" func main() { var num int = 100 switch { case num == 1: fmt.Println("Num is 1") case num > 1 && num < 60: fmt.Println("1 < Num < 60") case num >= 60 && num < 100: fmt.Println("60 <= num < 100") case num == 100: fmt.Println("Num is 100") fallthrough default: fmt.Println("default") } }
上述代码加入了 fallthrough,使得原本不表达的 default 也被执行了
记得 if 的赋值语句,你也可以把上面的代码这样写
package main import "fmt" func main() { switch num := 100; { case num == 1: fmt.Println("Num is 1") case num > 1 && num < 60: fmt.Println("1 < Num < 60") case num >= 60 && num < 100: fmt.Println("60 <= num < 100") case num == 100: fmt.Println("Num is 100") fallthrough default: fmt.Println("default") } }
for 语句的语法如下:
for 初始化语句;条件语句;修饰语句{}; package main import "fmt" func main() { for i := 0; i < 10; i++ { fmt.Printf("i is %d\n", i) } }
上面是一个简单的 for 语句,有趣的是,for 语句中的初始化,条件,修饰都不是必须选项,你可以这样修改
package main import "fmt" func main() { i := 0 for i < 10 { fmt.Printf("i is %d\n", i) i++ } }
只保留条件语句,甚至你可以连条件语句也去掉:
package main import "fmt" func main() { i := 0 for { fmt.Printf("i is %d\n", i) if i == 10 { break } i++ } }
for {} 的功能其实等同于 while,go 语言没有 while 的语法
接下来我们来看另一个 for 语句
package main import "fmt" func main() { str := "Hello,World" for i := 0; i < len(str); i++ { fmt.Printf("Character on position %d is : %c\n", i, str[i]) } }
获取字符串或数组的下标和对应值是比较常见的,go 提供了 for-range 结构可以优雅的实现,go-reange 语法:
for pos, char := range str {}
所以上面的代码可以 for-range 优雅的表述为
package main import "fmt" func main() { str := "Hello,World" for pos, cha := range str { fmt.Printf("Character on position %d is : %c\n", pos, cha) } }
数组,切片,map
我们可以这样定义一个数组:
var name [len]type //e.g. var myArr [10]int
类似于 c 语言,还可以有如下定义:
var name = new([len]int) //e.g. var myArr = new([10]int)
上述两种定义方式产生的效果不同:
第一种定义方式产生的是 [len] int e.g. [10] int
第二种定义方式产生的是 [len] int e.g. [10] int
我们可以通过下面两个程序来体会两者的不同
package main import "fmt" func main() { var arr1 [5]int test(arr1) fmt.Println("The orignal array is:", arr1) } func test(a [5]int) { for i, v := range a { fmt.Printf("Old value is:%d ----> ", v) a[i] = i fmt.Printf("New value is:%d\n", a[i]) } fmt.Println("The array is: ", a) }
上面的程序可以看到,当传递到 test 函数的值为 [len] int 时,函数并未作用到原来的 arr1,只是拷贝了一个新数组,然后在 test 函数内对新数组进行赋值
package main import "fmt" func main() { //var arr1 [5]int var arr1 = new([5]int) test(arr1) fmt.Println("The orignal array is:", arr1) } func test(a *[5]int) { for i, v := range a { fmt.Printf("Old value is:%d ----> ", v) a[i] = i fmt.Printf("New value is:%d\n", a[i]) } fmt.Println("The array is: ", a) }
但是如果传入的值为 new [5] int 时,test 函数就可以直接作用到 main 中的 arr 数组
类似于一维数组,多维数组的定义如下
var name [len1][len2]int // e.g. var myArray [2][3]int
多维数组的赋值,读取操作:
package main import ( "fmt" "strconv" //使用strconv.Itoa将int转为str ) func main() { var myArr [3][4]string for pos, val := range myArr { fmt.Printf("pos is %d,type of val is %T\n", pos, val) for posC, valC := range val { valC = "row:" + strconv.Itoa(pos) + ",col:" + strconv.Itoa(posC) fmt.Printf("the value is %s\n", valC) } } }
如果你想定义一个常量数组,你可以这样
var myArr = [5]int{1,2,3,4,5} // or var myArr = [...]int{1,2,3,4,5}
切片可以理解为长度可以变化的数组,是数组的一小段,我们可以如下定义:
var slicename []type = arr[start:end] //arr为同类型的数组,start为数组的起始标,end为数组的终止下标
或者也可以这样定义:
slicename := arr[start:end] package main import ( "fmt" ) func main() { var myArr [7]int var mySlice []int = myArr[2:4] //定义切片 for i := 0; i < len(myArr); i++ { myArr[i] = i } for i := 0; i < len(mySlice); i++ { fmt.Printf("Slice at %d is %d\n", i, mySlice[i]) } fmt.Printf("The length of myArr is %d\n", len(myArr)) fmt.Printf("The length of mySlice is %d\n", len(mySlice)) fmt.Printf("The cap of mySlice is %d\n", cap(mySlice)) //cap方法 fmt.Println("------------------切片扩容--------------------->") //切片扩容 mySlice = mySlice[:3] for i := 0; i < len(mySlice); i++ { fmt.Printf("mySlice at %d is %d\n", i, mySlice[i]) } fmt.Printf("The length of mySlice is %d\n", len(mySlice)) fmt.Printf("The cap of mySlice is %d\n", cap(mySlice)) //切片,数组共享数据 mySlice[0] = 100 for pos, val := range myArr { fmt.Printf("The myArr at %d is %d\n", pos, val) } for pos, val := range mySlice { fmt.Printf("The mySlice at %d is %d\n", pos, val) } }
- 切片 myArr [2:4] 获取数组 myArr 的的 myArr [2],myArr [3] 但不包含 myArr [4],即 [2,3]
- go 提供了 len 来获取切片的长度为 2
- cap (mySlice) 获取切片的容量,容量即切片的最大长度,mySlice 的 cap 为 3, 即从 mySlice [2] 开始算起,整个 myArr 的长度
- 切片和数组共享数组,修改切片中的数值,数组的相应值也会发变化
将切片传递给函数可参考数组传递
package main import ( "fmt" ) func sum(a []int) int { s := 0 for _, val := range a { s += val } return s } func main() { var myArr = [5]int{0, 1, 2, 3, 4} fmt.Println("The sum of myArr is ", sum(myArr[:])) }
上面切片的定义总是依赖数组,但是如果想单独定义一个切片的话可以这样:
var mySlice = make([]type,len,cap) //cap为可选参数,e.g. var mySlice = make([]int,10,20) //or var mySlice = new([len]type)[start:end] //e.g. var mySlice = new([20]int)[0:10]
go 中的字符串是无法修改的,类似于数组,我们可以字符串进行切片,len 等常规操作
package main import ( "fmt" ) func main() { var myStr string = "Hello,World" //len操作 for i := 0; i < len(myStr); i++ { fmt.Printf("Pos is %d,Val is %c\n", i, myStr[i]) } //切片操作 mySlice := myStr[:5] fmt.Printf("mySlice is %s", mySlice) }
如果我们想对 string 类型进行更多的操作,如更换其中某个位置的值的话,可以考虑引入一个字节数组 [] byte
package main import "fmt" func main() { var myStr string = "Hello,World" //引入一个byte数组,修改str中的某个位置的值 myByteArr := []byte(myStr) myByteArr[0] = 'P' myStr = string(myByteArr) fmt.Printf("myStr is %s\n", myStr) //copy操作 myByteArr1 := make([]byte, len(myByteArr)) copy(myByteArr1, myByteArr) fmt.Printf("myByteArr1 is %s\n", myByteArr1) //appand操作 myByteArr = append(myByteArr, "..."...) fmt.Printf("myByteArr is %s\n", myByteArr) }
在使用 append 时,当我们插入的是同类型的数组时,需要在数组后面加入...,如果插入的是当个值,如 append ([] int,int) 时,插入的 int 不需要添加...
map 类似于 python 中的 dict 结构,我们可以这样定义 map
var mapname map[keytype]valuetype // e.g. var myMap map[string]int //or var mapname = make(map[keytype]valuetype,cap) // cap可选,e.g. var myMap = make(map[string]int)
map 相当于强化版的数组,普通数组的下标 pos 只能是 int,而 map 的下标 key 可以是 string,int,float 等类型。
我们在介绍数组时提到数组可以使用 new 定义,但是 map 中一定别用 new 定义,因为这样你只会得到一个空应用的指针
package main import "fmt" func main() { var myMap = map[string]int{"one": 1, "two": 2} var myMap1 = make(map[int]string) //声明时不加入cap参数,map的容量动态增长 myMap1[1] = "one" //下标不必从零开始 myMap1[2] = "two" for key, val := range myMap { fmt.Printf("myMap key is %s,val is %d\n", key, val) } //使用if val,isPresent := myMap[keyname]判断keyname是否存在, //如果存在,则ifPresentweitrue,反之则反 if val,isPresent := myMap["two"];isPresent{ fmt.Println(`The valueof "two" in myMap is `,val) } else { fmt.Println(`myMap does not contain "two"`) } //使用delete(mapname,keyname)删除 delete(myMap,"two") fmt.Println(myMap) }
map 的 val 可以为任意类型,例如函数,空接口类型等
package main import "fmt" func main() { var myMap = map[string]func(int) int{ "func": func(a int) int { return a }, } //定义一个string:func的map,func接收一个int,并将其返回 fmt.Println(myMap["func"](23)) }
结构体
go 中的结构体 struct 相当于其他语言中类的概念,我们可以这样定义一个结构体 struct
type namestruct struct { fieldname1 type fieldname2 type ... } var ms = new(namestruct) //使用new返回一个指向结构体的指针*structname
我们可以使用。符来获取 struct 中的 field, e.g. structname.fieldname
使用 fmt.Printf ("% v",structname) 可以很好的输出结构体的内容
package main import "fmt" type person struct { name string age int } func main() { var joey = new(person) joey.name = "Joey" joey.age = 23 //或者也可以这样赋值 // joey := person{name:"Joey",age:23} fmt.Printf("The name is: %s\n", joey.name) fmt.Printf("The age is: %d\n", joey.age) fmt.Printf("The struct is %v\n", joey) fmt.Println(joey) }
结构体的继承的使用内嵌结构体实现
type structname1 struct { fielName structname // }
结构体的方法使用结构器实现:
func (recv receiverType) methodName(parameterList) (returnValueList){ ... } //e.g. func (p *structname) add() int { return p.x + p.y } package main import "fmt" //定义一个包含x,y的结构体 type myStruct struct { x int y int } //定义myStruct1继承myStruct,再添加一个field type myStruct1 struct { myStruct z int } //定义myStruct的一个加法方法 func (p *myStruct) Add() int { return p.x + p.y } //定义myStruct1的一个加法方法 func (p *myStruct1) Add() int { return p.myStruct.Add() + p.z } func main() { s1 := myStruct{x: 1, y: 2} fmt.Printf("The add method in myStruct is %d\n", s1.Add()) var s2 = myStruct1{s1, 3} fmt.Printf("The add method in myStruct1 is %d\n", s2.Add()) }
接口
接口是方法 method 的集合,我们可以这样定义一个接口
type Namer interface { Method1(paramList) returnType Method2(parmList) returnType } //接口里面只能包含方法
接口的命名采用 er 结尾。一般来说,一个接口通常包括 0 到 3 个方法
package main import ( "fmt" "math" ) //定义一个圆的结构体 type circle struct { radius float32 } //定义一个矩形结构体 type rectangle struct { length, width float32 } //圆的面积计算方法 func (c circle) Area() float32 { return math.Pi * c.radius * c.radius } //矩形的面积计算方法 func (r *rectangle) Area() float32 { return r.length * r.width } //定义一个接口,里面包含Area方法 type shaper interface { Area() float32 } func main() { c := circle{3} r := &rectangle{2, 3} //定义一个接口类型的数组 s := []shaper{c, r} for pos, _ := range s { fmt.Printf("shape details: %T\n", s[pos]) fmt.Println("Area of this shape is: ", s[pos].Area()) } }
使用接口可以使得程序更加可读和灵活,接下来我们用接口来实现整数数组的排序
package main import "fmt" //定义一个整形数组 type intArray []int //定义整形数组的Len方法,该方法返回数组的长度 func (p intArray) Len() int { return len(p) } //定义数组的比较方法 func (p intArray) Less(i, j int) bool { return p[i] < p[j] } //定义数组的交换方法,该方法交换数组里面的两个数 func (p intArray) Swap(i, j int) { p[i], p[j] = p[j], p[i] } //定义接口,该接口包含intArray的所有方法 type Sorter interface { Len() int Less(i, j int) bool Swap(i, j int) } //定义排序函数,该函数调用接口的方法实现 func Sort(data Sorter) { for pass := 1; pass < data.Len(); pass++ { for i := 0; i < data.Len()-pass; i++ { if data.Less(i+1, i) { data.Swap(i, i+1) } } } } func main() { data := []int{2, 4, 1, 8, 5, 3, 7, 6} a := intArray(data) var t Sorter = a Sort(t) fmt.Println(data) }
空接口是不包含任何方法,但是可以被赋予任意类型的值,在 go 语言中应用广泛。我们可以这样定义一个空接口
type Any interface {} package main import "fmt" type person struct { name string age int } //定义空接口 type Any interface{} func main() { var val Any //赋予int val = 5 fmt.Printf("The type of val is %T,val of Any is %v\n", val, val) //赋予string val = "Apple" fmt.Printf("The type of val is %T,val of Any is %v\n", val, val) //赋予person val = person{"Joey", 23} fmt.Printf("The type of val is %T,val of Any is %v\n", val, val) //val.(type)获取val类型 switch t := val.(type) { case int: fmt.Printf("Type int %T\n", t) case string: fmt.Printf("Type string %T\n", t) case bool: fmt.Printf("Type boolean %T\n", t) case person: fmt.Printf("Type person %T\n", t) default: fmt.Printf("Unexpected type %T\n", t) } }
上面的例子中,我们使用 val.(type) 方法获取 field 的类型。除此之外我们还可以使用 reflect 反射达到同样的效果,reflect 反射是 go 的一个内置包,使用时需要先 import
package main import ( "fmt" "reflect" ) type rectangle struct { length int width int } func (p rectangle) Area() int { return p.length * p.width } var secret interface{} = rectangle{2, 3} func main() { //使用ValueOf方法获取空接口的值 val := reflect.ValueOf(secret) //使用TypeOf方法获取空接口的类型 typ := reflect.TypeOf(secret) //对获取的值使用Kind方法进一步获取其基本类型 k := val.Kind() fmt.Println("the val is ", val) fmt.Println("the type is ", typ) fmt.Println("The kind is ", k) //NumField获取val的field数量,Field(int)可获取对应下标的val的值 for i := 0; i < val.NumField(); i++ { fmt.Printf("Field %d: %v\n", i, val.Field(i)) } }
协程与通道
在 go 语言中,为了能够并行的执行程序,我们可以使用 go 关键字,e.g.
go func(){}() //开启一个协程执行匿名函数 //OR go funcName(paramList) // 开启一个协程执行函数
观看以下程序
package main import ( "fmt" "time" ) func printInt() { for i := 1; i < 6; i++ { fmt.Println("printInt ---> ", i) } time.Sleep(1e9) } func printStr() { s := []byte{'a', 'b', 'c', 'd', 'e'} for _, i := range s { fmt.Println("printStr ---> ", string(i)) } time.Sleep(1e9) } func main() { t1 := time.Now() go printInt() go printStr() time.Sleep(1e9) fmt.Println(time.Since(t1)) }
上面的例子中,如果 main 函数中的 printInt,printStr 函数的调用去掉 go,那么整个程序的运行时间为 3s。但是由于使用了协程,main,printInt,printStr 同时执行,所以程序的运行时间为 1s。(实际测试为 1s 稍多一点)
上面的例子其实存在一个缺陷,如果你尝试把 main 函数中的 time.Sleep 去掉,那么整个程序将无法按照预期运行。
因为 main 函数一旦退出,系统并不会等待 printInt 和 printStr 执行结束,整个程序马上结束了。
为了防止这种情况的发生,我们在 main 中让程序等待 1s,但是这种方式看起来很笨拙,接下来,我们来介绍一种优雅的方式 (通道) 协调协程和主函数的运行
通道的声明
var channelName chan type // chan为通道的关键字,type为通道可接收的类型,可以是int,string,struct等
当然我们还可以使用 make 语句定义通道
var channelName = make(chan type,int) //make里的第一个参数是chan的类型,第二个参数是该通道的容量,e.g. var ch = make(chan string,10) //如果make的第二个参数int省略的话,默认为1
现在我们知道定义通道方法了,但是要做到协调,我们还需要知道如何进行通道的通信
var ch = make(chan int) ch <- 3 //往通道内发送3 s := <-ch //将通道内的3发送给s
通道的通信使用 <- 符表示,箭头的方向代表通道的发送和接收,需要注意的是,一个通道最大能接收的数据量为通道的容量,而且只有当通道内有数据时才能被读取
package main import ( "fmt" "time" ) func printInt(ch chan bool) { for i := 1; i < 6; i++ { fmt.Println("printInt ---> ", i) } time.Sleep(1e9) ch <- true } func printStr(ch chan bool) { s := []byte{'a', 'b', 'c', 'd', 'e'} for _, i := range s { fmt.Println("printStr ---> ", string(i)) } time.Sleep(1e9) ch <- true } func main() { t1 := time.Now() chInt := make(chan bool) chStr := make(chan bool) go printInt(chInt) go printStr(chStr) <-chInt //如果printInt函数中chInt没有数发送,则会一直阻塞 <-chStr fmt.Println(time.Since(t1)) }
使用通道协调 main 和协程的本质在于通道的阻塞机制。上面的程序 main 函数中的 <-chInt 依赖于协程 printInt 的 ch<-true,否则会一直阻塞。这就相当于 main 函数需要等待 printInt 执行结束。
当我们在写程序中需要使用较多数量的通道时,select 语句就变得很有用。select 语句类似于 switch 语句
select { case u := <- ch1: ... case v := <- ch2: ... ... default: ... }
我们试着将上一个程序使用 select 语句编写
package main import ( "fmt" "time" ) func printInt(ch, cs chan bool) { for i := 1; i < 6; i++ { fmt.Println("printInt ---> ", i) } time.Sleep(1e9) ch <- true cs <- true } func printStr(ch, cs chan bool) { s := []byte{'a', 'b', 'c', 'd', 'e'} for _, i := range s { fmt.Println("printStr ---> ", string(i)) } time.Sleep(1e9) ch <- true cs <- true } func main() { t1 := time.Now() chInt := make(chan bool) chStr := make(chan bool) chStop := make(chan bool, 2) go printInt(chInt, chStop) go printStr(chStr, chStop) for { select { case <-chInt: fmt.Println("func printInt done!") case <-chStr: fmt.Println("func printStr done!") default: if len(chStop) == 2 { fmt.Println(time.Since(t1)) return } } } }
错误处理和测试
go 语言没有 try/catch 的错误处理机制,对于一些普通的错误,在设计程序时就将程序的正确执行与否作为返回值,如果返回的值为 nil,则证明函数正常运行
通常我们可以使用 if 语句来判断
if value,err := func(paramList);nil != nil { processError() } package main import ( "errors" "fmt" ) func divison(dividend float64, divisor float64) (float64, error) { if divisor == 0 { return 0, errors.New("The divisor is zero...") } return dividend / divisor, nil } func main() { if val, err := divison(3, 0); err != nil { fmt.Printf("error:%v\n", err) } else { fmt.Println("The value is ", val) } }
上面的例子中我们用 errors.New 方法定义了一个 error,用以鉴别除数为零的错误情况
虽然出现了错误,但是上面的程序还是能够完整的运行,只不过是返回的结果不同。如果我们在运行程序中,碰到了一些严重的错误,需要让整个程序停止下来时,可以使用 panic
package main import ( "fmt" ) func divison(dividend float64, divisor float64) (float64, error) { if divisor == 0 { //return 0, errors.New("The divisor is zero...") panic("The divisor is zero...") } return dividend / divisor, nil } func main() { if val, err := divison(3, 0); err != nil { fmt.Printf("error:%v\n", err) } else { fmt.Println("The value is ", val) } }
程序一旦触发 panic,会先打印 panic 方法内的字符串,然后标明触发的位置和信息
如果需要从 panic 中恢复,使得程序继续运行,可以使用 recover 语句。
package main import ( "fmt" ) func divison(dividend float64, divisor float64) (float64, error) { if divisor == 0 { //return 0, errors.New("The divisor is zero...") panic("The divisor is zero...") } return dividend / divisor, nil } func test() { defer func() { if e := recover(); e != nil { fmt.Printf("Panic %s\r\n", e) } }() divison(3, 0) fmt.Println("Test function...") } func main() { fmt.Println("Start main...") test() fmt.Println("End main...") }
recover 语句只能和 defer 搭配使用,defer 语句通常和匿名函数配合使用,可以说 recover,defer, 匿名函数是一套组合
类似于 python,在 go 语言中,我们也可以使用 go test 工具对程序进行测试。使用 go test 需要遵循以下几个规则:
- 引入 testing 包
- 测试的程序名需要以_test 结尾
_test.go 中的测试函数名需要以 Test 开头
接下来我们按照上述规则来写一个实例,新建一个文件夹 even,文件夹中新建一个文件 even.gopackage even
//判断一个数为奇偶
func Even(i int) bool {
return i%2 == 0
}
func Odd(i int) bool {
return i%2 != 0
}
接下来在 even 中再创建一个测试文件 even_test.go
package even import "testing" func TestEven(t *testing.T) { if Even(10) { t.Log("10 must be even!") } if Even(7) { t.Log("7 is not even") t.Fail() } } func TestOdd(t *testing.T) { if !Odd(11) { t.Log("11 must be odd!") } if Odd(10) { t.Log("10 is not odd!") t.Fail() } }
even_test.go 中的函数分别对 Even 和 Odd 函数进行测试,Log 方法打印错误信息,Fail 标记函数失败,然后继续接下来的测试
在文件夹所在路径下,终端运行 go test 就可以看到测试结果了
© 著作权归作者所有
相关热门文章
发表评论