GO.语言基础
Go程序设计的一些规则
Go之所以会那么简洁,是因为它有一些默认的行为:
大写字母开头的变量是可导出的,也就是其它包可以读取的,是公用变量;小写字母开头的就是不可导出的,是私有变量。
大写字母开头的函数也是一样,相当于class中的带public关键词的公有函数;小写字母开头的就是有private关键词的私有函数。
内建函数make、new
make用于内建类型(map、slice 和channel)的内存分配。new用于各种类型的内存分配。
内建函数new本质上说跟其它语言中的同名函数功能一样:new(T)分配了零值填充的T类型的内存空间,并且返回其地址,即一个*T类型的值。用Go的术语说,它返回了一个指
针,指向新分配的类型T的零值。
内建函数make(T, args)与new(T)有着不同的功能,make只能创建slice、map和channel,并且返回一个有初始值(非零)的T类型,而不是*T。本质来讲,导致这三个类型有所
不同的原因是指向数据结构的引用在使用前必须被初始化。
例如,一个slice,是一个包含指向数据(内部array)的指针、长度和容量的三项描述符;在这些项目被初始化之前,slice为nil。对于slice、map和channel来说,make初始
化了内部的数据结构,填充适当的值。make返回初始化后的(非零)值。
函数
函数是Go里面的核心设计,它通过关键字func来声明,格式如: ---支撑多个返回值,变参(不定数量的参数)
func funcName(input1 type1, input2 type2) (output1 type1, output2 type2) { //这里是处理逻辑代码 //返回多个值 return value1, value2 }
Go语言中有种不错的设计,即延迟defer语句,你可以在函数中添加多个defer语句。
当函数执行到最后时,这些defer语句会按照逆序执行,最后该函数返回。
特别是当你在进行一些打开资源的操作时,遇到错误需要提前返回,在返回前你需要关闭相应的资源,不然很容易造成资源泄露等问题。
一般写打开一个资源有这样的操作:
func ReadWrite() bool { file.Open("file") // do something if failureX { file.Close() return false } if failureY { file.Close() return false } file.Close() return true }
我们看到上面有很多重复的代码,Go的defer有效解决了这个问题。使用它后,不但代码量减少了很多,而且程序变得更优雅。在defer后指定的函数会在函数退出前调用。
如果有很多调用defer,那么defer是采用后进先出模式
func ReadWrite() bool { file.Open("file") defer file.Close() if failureX { return false } if failureY { return false } return true }
函数作为值、类型
在Go中函数也是一种变量,我们可以通过type来定义它,它的类型就是所有拥有相同的参数,相同的返回值的一种类型。
函数作为类型到底有什么好处呢?那就是可以把这个类型的函数当做值来传递
package main import "fmt" type testInt func(int) bool // 声明了一个函数类型 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 } // 声明的函数类型在这个地方当做了一个参数 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 elements of slice are: ", odd) even := filter(slice, isEven) // 函数当做值来传递了 fmt.Println("Even elements of slice are: ", even) }
函数当做值和类型在我们写一些通用接口的时候非常有用,觉得可以对比java等面向对象语言中的接口或者策略模式。
Panic和Recover
Go没有像Java那样的异常机制,它不能抛出异常,而是使用了panic和recover机制。一定要记住,你应当把它作为最后的手段来使用,也就是说,你的代码中应当没有,或者很少有panic的东西。
Panic是一个内建函数,可以中断原有的控制流程,进入一个恐慌的流程中。当函数F调用panic,函数F的执行被中断,但是F中的延迟函数会正常执行,然后F返回到调用它的地方。在调用的地方,F的行为就像调用了panic。这一过程继续向上,直到发生panic的goroutine中所有调用的函数返回,此时程序退出。恐慌可以直接调用panic产生。也可以由运行时错误产生,例如访问越界的数组。
Recover是一个内建的函数,可以让进入恐慌的流程中的goroutine恢复过来。recover仅在延迟函数中有效。在正常的执行过程中,调用recover会返回nil,并且没有其它任何效果。如果当前的goroutine陷入恐慌,调用recover可以捕获到panic的输入值,并且恢复正常的执行。
panic使用 :
var user = os.Getenv("USER") 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包还导入了其它的包,那么就会在编译时将它们依次导入。有时一个包会被多个包同时导入,那么它只会被导入一次(例如很多包可能都会用到fmt包,但它只会被导入一次,因为没有必要导入多次)。当一个包被导入时,如果该包还导入了其它的包,那么会先将其它包导入进来,然后再对这些包
中的包级常量和变量进行初始化,接着执行init函数(如果有的话),依次类推。等所有被导入的包都加载完毕了,就会开始对main包中的包级常量和变量进行初始化,然后执行main包中的init函数(如果存在的话),最后执行main函数。
import导入包文件
import(
"fmt"
)
上面这个fmt是Go语言的标准库,其实是去goroot下去加载该模块,当然Go的import还支持如下两种方式来加载自己写的模块:
1. 相对路径
import “./model” //当前文件同一目录的model目录,但是不建议这种方式来import
2. 绝对路径
import “shorturl/model” //加载gopath/src/shorturl/model模块
还有一些特殊的import方式:
1. 点操作
import(
. "fmt"
)
这个点操作的含义就是这个包导入之后在你调用这个包的函数时,你可以省略前缀的包名,也就是前面你调用的fmt.Println("hello world")可以省略的写成Println("hello world")
2. 别名操作
import(
f "fmt"
)
别名操作的话调用包函数时前缀变成了我们的前缀,即 f.Println("hello world")
3. _操作
import (
"database/sql"
_ "github.com/ziutek/mymysql/godrv"
)
_操作其实是引入该包,而不直接使用包里面的函数,而是调用了该包里面的init函数。
struct类型
和其他语言一样,我们可以使用struct声明新的类型
type person struct { name string age int }
Go支持只提供类型,而不写字段名的方式,也就是匿名字段,也称为嵌入字段。
package main import "fmt" type Human struct { name string age int weight int } type Student struct { speciality string Human // 匿名字段,默认Student包含了Human所有字段 } func main() { mark := Student{Human{"Mark", 25, 120}, "Computer Science"} fmt.Println("His speciality is ", mark.speciality) fmt.Println("His name is ", mark.name) mark.speciality = "AI" fmt.Println("His speciality is ", mark.speciality) mark.age = 46 fmt.Println("His age is", mark.age) }
这里有一个问题:如果human里面有一个字段叫做phone,而student也有一个字段叫做phone,那么该怎么办呢?
Go里面很简单的解决了这个问题,最外层的优先访问,也就是当你通过student.phone访问的时候,是访问student里面的字段,而不是human里面的字段。
这样就允许我们去通过匿名字段继承的一些字段,当然如果我们想访问重载后对应匿名类型里面的字段,可以通过匿名字段名来访问。
面向对象
函数的另一种形态,带有接收者的函数,称为method。
package main import "fmt" type Rectangle struct { width, height float64 } func area(r Rectangle) float64 { return r.width * r.height } func main() { r1 := Rectangle{12, 2} r2 := Rectangle{9, 4} fmt.Println("Area of r1 is: ", area(r1)) fmt.Println("Area of r2 is: ", area(r2)) }
使用method的时候重要注意几点
1.虽然method的名字一模一样,但是如果接收者不一样,那么method就不一样
2.method里面可以访问接收者的字段
3.调用method通过.访问,就像struct里面访问字段一样
值得说明的一点是,
Receiver还可以是指针, 两者的差别在于, 指针作为Receiver会对实例对象的内容发生操作,而普通类型作为Receiver仅仅是以副本作为操作对象,并不对原实例对象发生操作。
另外,method可以定义在任何你自定义的类型、内置类型、struct等各种类型上面。
例如,给自定义类型定义method
package main import "fmt" const ( WHITE = iota BLACK BLUE RED YELLOW ) type Color byte type Box struct { width, height, depth float64 color Color } type BoxList []Box func (b Box) Volume() float64 { //调用者不会被修改 return b.width * b.height * b.depth } func (b *Box) SetColor(c Color) { //调用者会被修改 b.color = c } func (bl BoxList) BiggestsColor() Color { v := 0.00 k := Color(WHITE) for _, b := range bl { if b.Volume() > v { v = b.Volume() k = b.color } } return k } func (bl BoxList) PaintItBlack() { for i, _ := range bl { bl[i].SetColor(BLACK) } } func (c Color) String() string { strings := []string{"WHITE", "BLACK", "BLUE", "RED", "YELLOW"} return strings[c] } func main() { boxes := BoxList{ Box{4, 4, 4, RED}, Box{10, 10, 1, YELLOW}, Box{1, 1, 20, BLACK}, Box{10, 10, 1, BLUE}, Box{10, 30, 1, WHITE}, Box{20, 20, 20, YELLOW}, } fmt.Printf("We have %d boxes in our set\n", len(boxes)) fmt.Println("The volume of the first one is", boxes[0].Volume(), "cm³") fmt.Println("The color of the last one is", boxes[len(boxes)-1].color.String()) fmt.Println("The biggest one is", boxes.BiggestsColor().String()) fmt.Println("Let's paint them all black") boxes.PaintItBlack() fmt.Println("The color of the second one is", boxes[1].color.String()) fmt.Println("Obviously, now, the biggest one is", boxes.BiggestsColor().String()) }
Color作为byte的别名,上面SetColor这个method,它的receiver是一个指向Box的指针(定义SetColor的真正目的是想改变这个Box的颜色,如果不传Box的指针,那么
SetColor接受的其实是Box的一个copy,也就是说method内对于颜色值的修改,其实只作用于Box的copy,而不是真正的Box。所以我们需要传入指针。)
如果一个method的receiver是*T,你可以在一个T类型的实例变量V上面调用这个method,而不需要&V去调用这个method,反之如果一个method的receiver是T,你也可以在一个*T类型的变量P上面调用这个method,而不需要 *P去调用这个method
method继承与重写
类似于字段的继承,如果匿名字段实现了一个method,那么包含这个匿名字段的struct也能调用该method。
同样,与匿名字段冲突一样的道理,可以在包含匿名字段的struct上定义一个同样method,重写了匿名字段的方法
package main import "fmt" type Human struct { name string age int phone string } type Student struct { Human //匿名字段 school string } type Employee struct { Human //匿名字段 company string } func (h *Human) SayHi() { fmt.Printf("Hi, I am %s you can call me on %s\n", h.name, h.phone) } //Employee重写Human的method func (e *Employee) SayHi() { fmt.Printf("Hi, I am %s, I work at %s. Call me on %s\n", e.name, e.company, e.phone) //Yes you can split into 2 lines here. } func main() { mark := Student{Human{"Mark", 25, "222-222-YYYY"}, "MIT"} sam := Employee{Human{"Sam", 45, "111-888-XXXX"}, "Golang Inc"} mark.SayHi() sam.SayHi() }
interface
简单的说,interface是一组method的组合,我们通过interface来定义对象的一组行为。如果某个对象实现了某个接口的所有方法,则此对象就实现了此接口。
如果我们定义了一个interface的变量,那么这个变量里面可以存实现这个interface的任意类型的对象。
package main import "fmt" type Human struct { name string age int phone string } type Student struct { school string loan float32 Human //匿名字段 } type Employee struct { company string money float32 Human //匿名字段 } //Human实现Sayhi方法 func (h Human) SayHi() { fmt.Printf("Hi, I am %s you can call me on %s\n", h.name, h.phone) } //Human实现Sing方法 func (h Human) Sing(lyrics string) { fmt.Println("La la la la...", lyrics) } //Employee重载Human的SayHi方法 func (e Employee) SayHi() { fmt.Printf("Hi, I am %s, I work at %s. Call me on %s\n", e.name, e.company, e.phone) //Yes you can split into 2 lines here. } // Interface Men被Human,Student和Employee实现 type Men interface { SayHi() Sing(lyrics string) } func main() { mike := Student{Human{"Mike", 25, "222-222-XXX"}, "MIT", 0.00} paul := Student{Human{"Paul", 26, "111-222-XXX"}, "Harvard", 100} sam := Employee{Human{"Sam", 36, "444-222-XXX"}, "Golang Inc.", 1000} var i Men i = mike i.SayHi() i = sam i.SayHi() x := make([]Men, 3) x[0], x[1], x[2] = paul, sam, mike for _, value := range x { value.SayHi() } }
由上面的代码可以知道,interface可以被任意的对象实现。一个对象也可以实现任意多个interface,
其实,任意的类型都实现了空interface(我们这样定义:interface{}),也就是包含0个method的interface,这点类似于java里的object。
如上面代码,如果我们定义了一个interface的变量,那么这个变量里面可以存实现这个interface的任意类型的对象。
interface函数参数
interface的变量可以持有任意实现该interface类型的对象,同样,也可以通过定义interface参数,让函数接受各种类型的参数。
interface变量存储的类型
如何反向知道interface变量中实际保存了的是哪个类型的对象。
Comma-ok断言
Go语言里面有一个语法: value, ok = element.(T),这里value就是变量的值,ok是一个bool类型,element是interface变量,T是断言的类型。
如果element里面确实存储了T类型的数值,那么ok返回true,否则返回false。
另一种语法 element.(type)只能在switch中使用,如果你要在switch外面判断一个类型就使用comma-ok。
package main import ( "fmt" "strconv" ) type Element interface{} type List []Element type Person struct { name string age int } //定义了String方法,实现了fmt.Stringer func (p Person) String() string { return "(name: " + p.name + " - age: " + strconv.Itoa(p.age) + " years)" } func main() { list := make(List, 3) list[0] = 1 list[1] = "Hello" list[2] = Person{"Dennis", 70} for index, element := range list { if value, ok := element.(int); ok { fmt.Printf("list[%d] is an int and its value is %d\n", index, value) } else if value, ok := element.(string); ok { fmt.Printf("list[%d] is a string and its value is %s\n", index, value) } else if value, ok := element.(Person); ok { fmt.Printf("list[%d] is a Person and its value is %s\n", index, value) } else { fmt.Println("list[%d] is of a different type", index) } } }
嵌入interface
如果一个interface1作为interface2的一个嵌入字段,那么interface2隐式的包含了interface1里面的method。
源码包container/heap里面有这样的一个定义:
type Interface interface { sort.Interface //嵌入字段sort.Interface Push(x interface{}) //a Push method to push elements into the heap Pop() interface{} //a Pop elements that pops elements from the heap }
另一个例子就是io包下面的 io.ReadWriter ,他包含了io包下面的Reader和Writer两个interface。
// io.ReadWriter type ReadWriter interface { Reader Writer }
反射
Go语言实现了反射,所谓反射就是动态运行时的状态。我们一般用到的包是reflect包。使用reflect一般分成三步:
1.要去反射是一个类型的值(都实现了空interface),首先需要把它转化成reflect对象(reflect.Type或者reflect.Value,根据不同的情况调用不同的函数):
t := reflect.TypeOf(i) //得到类型的元数据,通过 t 我们能获取类型定义里面的所有元素
v := reflect.ValueOf(i) //得到实际的值,通过 v 我们获取存储在里面的值,还可以去改变值
2.转化为reflect对象之后我们就可以进行一些操作了,也就是将reflect对象转化成相应的值:
tag := t.Elem().Field(0).Tag //获取定义在struct里面的标签
name := v.Elem().Field(0).String() //获取存储在第一个字段里面的值
获取反射值能返回相应的类型和数值
var x float64 = 3.4
v := reflect.ValueOf(x)
fmt.Println("type:", v.Type())
fmt.Println("kind is float64:", v.Kind() == reflect.Float64)
fmt.Println("value:", v.Float())
最后,反射的话,那么反射的字段必须是可修改的,前面说过传值和传引用的区别,反射的字段必须是可读写的意思是,
如果下面这样写,那么会发生错误
var x float64 = 3.4
v := reflect.ValueOf(x)
v.SetFloat(7.1)
如果要修改相应的值,必须这样写
var x float64 = 3.4
p := reflect.ValueOf(&x)
v := p.Elem()
v.SetFloat(7.1)
GO并发
GO从语言层面就支持了并行
goroutine
goroutine是Go并行设计的核心。goroutine说到底其实就是线程,但是他比线程更小,十几个goroutine可能体现在底层就是五六个线程,Go语言内部帮你实现了这些goroutine
之间的内存共享。执行goroutine只需极少的栈内存(大概是4~5KB),当然会根据相应的数据伸缩。也正因为如此,可同时运行成千上万个并发任务。goroutine比thread更易
用、更高效、更轻便。goroutine是通过Go的runtime管理的一个线程管理器。goroutine通过go关键字实现了,其实就是一个普通的函数:go hello(a, b, c)
channels
goroutine运行在相同的地址空间,因此访问共享内存必须做好同步。
Go提供了一个很好的通信机制channel。channel可以与Unix shell 中的双向管道做类比:可以通过它发送或者接收值。这些值只能是特定的类型:channel类型。定义一个
channel时,也需要定义发送到channel的值的类型。
注意,必须使用make 创建channel:
ci := make(chan int)
cs := make(chan string)
cf := make(chan interface{})
channel通过操作符<-来接收和发送数据
ch <- v // 发送v到channel ch.
v := <-ch // 从ch中接收数据,并赋值给v
package main import ( "fmt" ) func sum(a []int, c chan int) { sum := 0 for _, v := range a { sum += v } c <- sum // send sum to c } func main() { a := []int{7, 2, 8, -9, 4, 0} fmt.Println(len(a)) fmt.Println(a[:3]) c := make(chan int) go sum(a[:len(a)/2], c) go sum(a[len(a)/2:], c) x, y := <-c, <-c // receive from c fmt.Println(x, y, x+y) }
默认情况下,channel接收和发送数据都是阻塞的,除非另一端已经准备好,这样就使得Goroutines同步变的更加的简单,而不需要显式的lock。
Buffered Channels
默认的是非缓存类型的channel,不过Go也允许指定channel的缓冲大小,很简单,就是channel可以存储多少元素。
ch:= make(chan bool, 4),创建了可以存储4个元素的bool 型channel。在这个channel 中,前4个元素可以无阻塞的写入。当写入第5个元素时,代码将会阻塞。
package main import ( "fmt" ) func main() { c := make(chan int, 2) //修改2为1就报错,修改2为3可以正常运行 c <- 1 c <- 2 fmt.Println(<-c) fmt.Println(<-c) }
Range和Close
上面例子中,需要读取两次c,不是很方便,考虑到这一点,可以通过range,像操作slice或者map一样操作缓存类型的channel.
package main import ( "fmt" ) func fibonacci(n int, c chan int) { x, y := 1, 1 for i := 0; i < n; i++ { c <- x x, y = y, x+y } close(c) } func main() { c := make(chan int, 10) go fibonacci(cap(c), c) for i := range c { fmt.Println(i) } }
Select
如果多个channel,Go里面提供了一个关键字select,通过select可以监听channel上的数据流动。
select默认是阻塞的,只有当监听的channel中有发送或接收可以进行时才会运行,当多个channel都准备好的时候,select是随机的选择一个执行的。
package main import "fmt" func fibonacci(c, quit chan int) { x, y := 1, 1 for { select { case c <- x: x, y = y, x+y case <-quit: fmt.Println("quit") return } } } func main() { c := make(chan int) quit := make(chan int) go func() { for i := 0; i < 10; i++ { fmt.Println(<-c) } quit <- 0 }() fibonacci(c, quit) }
c和quit,随机选择一个准备好的执行,两个线程执行,func线程中,先将c打印完在给quit赋值。
select其实就是类似switch的功能,在select里面还有default语法,default就是当监听的channel都没有准备好的时候,默认执行的(select不再阻塞等待channel)。
超时
有时候会出现goroutine阻塞的情况,可以利用select来设置超时,来避免整个的程序进入阻塞。
package main import "time" func main() { c := make(chan int) o := make(chan bool) go func() { for { select { case v := <-c: println(v) case <-time.After(5 * time.Second): println("timeout") o <- true break } } }() println(<-o) }
runtime goroutine
runtime包中有几个处理goroutine的函数:
Goexit:退出当前执行的goroutine,但是defer函数还会继续调用
Gosched:让出当前goroutine的执行权限,调度器安排其他等待的任务运行,并在下次某个时候从该位置恢复执行。
NumCPU:返回 CPU 核数量
NumGoroutine:返回正在执行和排队的任务总数
GOMAXPROCS:用来设置可以运行的CPU核数
#笔记内容来自 《Go Web编程》