函数
定义:
作用域
在Go中,定义在函数外的变量是全局的,定义在函数内部是局部的。如果全局与局部变量同名,局部变量将覆盖全局变量。
传值与传指针
Go中参数传递是值传递,当传一个参数值到被调用函数里的时候,实际上是拷贝了这个参数值。
变参
func myfunc(arg ...int) {}
:变参的类型都相同,在这个例如的函数体中,变量arg是一个int类型的slice。
如果不指定变参的类型,默认是空的接口interface{}
。
传递变参:
func myfunc(arg ...int) {
myfunc2(arg...) // 按原样传递
myfunc2(arg[:2]...) // 传递片段
}
多个返回值
Go函数的返回值或者结果参数可以指定一个名字,并且像原始的变量那样使用,就像输入参数那样。
官方建议:最好命名返回值,因为不命名返回值,虽然使得代码更加简洁了,但是会造成生成的文档可读性差。
如果对其命名,在函数开始时,它们会用其类型的零值初始化;如果函数在不加入参数的情况下执行了return语句,结果参数的当前值会作为返回值返回。
函数作为值
在Go中函数也是一种变量,我们可以通过type
来定义它,它的类型就是所有拥有相同的参数,相同的返回值的一种类型:
type typeName func(input1 inputType1 , input2 inputType2 [, ...]) (result1 resultType1 [, ...])
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)
}
延迟执行
defer
关键字后面指定的函数在函数退出前调用。延迟的函数是按照后进先出(LIFO)的顺序执行。
利用defer甚至可以修改返回值。
defer func(x int) {
// 在这里匿名函数内部,可以访问外围函数的返回值。
} (5) // () 在这里是必须的。
自定义类型 struct
在Go中也可以声明新的类型,作为其他类型的属性或字段的容器。
定义
type Student struct {
name string
age int
}
// 给自定义类型添加方法
func (s *Student)show() {
fmt.Println("i'am " + s.name)
}
注意:
struct 不允许递归定义或定义里包含循环。下面两种情形都是编译不通过的。
// 形式一
type Node struct {
parent, lChild, rChild Node
key int
}
// 形式二
type Node struct {
parent, lChild, rChild BinaryTree
key int
}
type BinaryTreestruct {
node Node
}
实例化
类型的初始化使用方式可以有三种:
var s Student
:先声明变量,变量的属性值为默认值,然后再对属性赋值。s := Student {"coderbee", 28}
:按照声明的顺序提供属性的初始值。s := Student {name:"coderbee", age:28}
:按照field:value
的形式初始化属性值,任意顺序。
匿名字段
如果省略字段的名字,可以创建匿名字段(默认字段名是类型的名字,所以一个struct里面不能定义两个相同类型的匿名字段)。
当匿名字段是一个struct的时候,这个struct所拥有的全部字段都被隐式地引入当前定义的这个struct。外围的struct可以像访问自己的属性那样访问匿名struct的属性。
如果内层struct与外层struct具有同名的字段,那么最外层的优先访问,对于内层字段的访问可以通过内层struct的字段名来访问。
package main
import "fmt"
type Human struct {
name string
age int
weight int
}
type Student struct {
Human
spciality string
weight int
}
func main() {
mark := Student{Human{"mark", 25, 120}, "computer science", 125}
fmt.Printf("mark: %v\n", mark)
fmt.Printf("mark's name: %v\n", mark.name)
mark.weight = 130
fmt.Printf("out weight:%d, inner weight:%d, default weight:%d\n", mark.weight, mark.Human.weight, mark.weight)
}
方法
方法就是有接收者的函数,可以在任意类型上定义方法(除了非本地类型(定义在其他包的),包括内建类型)。
接口定义为一个方法的集合,方法包含实际代码。也就是说,一个接口是定义,而方法是实现。因此,接收者不能定义为接口类型,这样做会引起invalid receiver type
的编译器错误。来自语言说明书:
接收者类型必须是T
或*T
,这里的T
是类型名。T
叫做接收者基础类型或简称基础类型。基础类型一定不能是指针或接口类型,并且定义在与方法相同的包中。
给类型添加方法有几种途径:
- 函数调用:
func doSomething(s * Student, age int) { /* ... */ }
- 方法调用,类型指针上添加:
func (s * Student)doSomething(age int) { /* ... */ }
- 方法调用,类型值上添加:
func (s Student)doSomething(age int) { /* ... */ }
指针类型上添加和类型值上添加的区别在于: 指针作为Receiver会对实例对象的内容发生操作,而普通类型作为Receiver仅仅是以副本作为操作对象,并不对原实例对象发生操作。
如果x
可获取地址,并且&x
的方法中包含了m,x.m()
是(&x).m()
更短的写法。
如果一个method的receiver是*T
,你可以在一个T
类型的实例变量V上面调用这个method,而不需要&V
去调用这个method。
如果一个method的receiver是T
,你可以在一个*T
类型的变量P上面调用这个method,而不需要*P
去调用这个method。
接口 interface
每个类型都有接口,意味着对那个类型定义了方法集合。interface类型定义了一组方法,如果某个对象实现了某个接口的所有方法,则此对象就实现了此接口。
定义
type I interface {
Get() int
Put(int)
}
type S struct { i int }
func (p *S) Get() int { return p.i }
func (p *S) Put(v int) { p.i = v }
对于接口I,S是合法的实现,因为它定义了I所需的两个方法。
每个类型都能匹配到空接口: interface{}
。 空interface在我们需要存储任意类型的数值的时候相当有用,因为它可以存储任意类型的数值。它有点类似于C语言的void*
类型。
无需明确一个类型是否实现了一个接口意味着Go实现了duck typing
的模式。这不是纯粹的duck typing
,因为如果可能的话Go编译器将对类型是否实现了接口进行静态检查。
类型判断
方法一:
func getType(p interface{}) string {
switch p.(type) {
case int: return "int"
case string: return "string"
case Person: return "Person"
default: return "unknow type"
}
}
在switch
之外使用(type)
是非法的。
方法二:
另一种判断一个接口类型是否实现了某个特定接口的方法:
func runtimeType(any interface{}) {
if _, ok := any.(I); ok {
println("type is I .")
} else if _, ok := any.(int); ok {
println("type is int .")
}
}
.(I)
是断言类型,用于转换any到I类型的接口。
继承
Go不支持继承,鼓励使用组合和委派。
type Engine interface {
Start()
Stop()
}
type Car struct {
Engine
}
定义Car的时候,Engine是一个匿名成员,这是一个只能被其类型识别的成员。匿名成员和其他的成员一样,并有着和类型一样的名字。
如果一个类型定义了一个方法,就使用它;如果不是,就使用匿名成员定义的方法。如果两个匿名成员都提供一个方法,编译器会报错,但只在该方法被调用的情况下。
这种组合是通过委派来实现的,而不是继承。一旦匿名成员的方法被调用,控制流就被委托给了该方法。
type Base struct {}
func (Base) Magic() { fmt.Print("base magic") }
func (self Base) MoreMagic() {
self.Magic()
self.Magic()
}
type Foo struct {
Base
}
func (Foo) Magic() { fmt.Print("foo magic") }
// 将得到:
f := new(Foo)
f.Magic() //=> foo magic
f.MoreMagic() //=> base magic base magic
要注意两种继承的不同之处
type NewStudent Student
// 不会继承 Student 的方法
type SubStudent struct { Student }
// 会继承 Student 的方法
并发
并行是关于性能的;并发是关于程序设计的。
goroutine
goroutine是一个普通的函数,只是需要使用保留字go作为开头。
goroutine有简单的模型:它是与其他goroutine并行执行的,有着相同地址空间的函数。是轻量的,仅比分配栈空间多一点点消耗,并且随着需要在堆空间上分配(和释放)。
虽然goroutine是并发执行的,但是它们并不是并行执行的。如果不告诉Go额外的东西,同一时刻只会有一个goroutine执行。利用runtime.GOMAXPROCS(n)
可以设置goroutine并行执行的数量。来自文档:GOMAXPROCS
设置了同时运行的CPU的最大数量,并返回之前的设置。如果n<1
,不会改变当前设置。当调度得到改进后,这将被移除。
runtime与goroutine
runtime包有几个处理goroutine的函数:
Goexit
:退出当前执行的goroutine,但是defer
函数还是会继续调用。Gosched
:让出当前goroutine的执行权限,掉赌气安排其他等待中的任务运行,并在下次某个时候从该位置恢复执行。(相当于Java的Thread.yield()
)NumCPU
:返回CPU核数量。NumGoroutine
:返回正在执行和排队的任务总数。GOMAXPROCS
:用来设置可以运行的CPU核数。
channel
channel可以与Unix shell中的双向管道做类比:可以通过它发送或者接收值,这些值只能是特定的类型:channel类型。定义一个channel时,也需要定义发送到channel的值的类型。
必须使用make
创建channel,如果创建时不指定大小,将创建无缓冲的channel。无缓冲意味着:读取时将被阻塞,直到有数据可接收;发送也将被阻塞,直到数据被读出。无缓冲channel可在多个goroutine之间同步。
创建: ci := make(chan int); ci := make(chan int, 4)
// 4是channel的缓冲大小。
发送到channel: ci <- 1
从channel接收: i := <- ci
range和close
for i := range c
的形式能够不断地从channel c读取数据,直到c被显式关闭。
关闭channel之后就无法再发送任何数据了,在消费方可以通过语法v, ok := <-ch
测试channel是否被关闭。如果ok
返回false
,那么说明channel已经没有任何数据并且已经被关闭。
记住应该在生产者的地方关闭channel,而不是消费的地方去关闭它,这样容易引起panic
另外记住一点的就是channel不像文件之类的,不需要经常去关闭,只有当你确实没有任何发送数据了,或者你想显式的结束range循环之类的
select
select可以监听多个channel上的数据流动。
select默认是阻塞的,只有当监听的channel中有发送或接收可以进行时才会运行,当多个channel都准备好的时候,select是随机的选择一个执行的。
在select里面还有default语法,select其实就是类似switch的功能,default就是当监听的channel都没有准备好的时候,默认执行的(select不再阻塞等待channel)。
超时
可以利用select来设置超时,通过如下的方式实现:
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
}
}
}()
<- o
}
欢迎关注我的微信公众号: coderbee笔记,可以更及时回复你的讨论。