前言
因为工作中主要用golang进行开发,所以最近开始学习golang。不得不说,golang是一种灵活简洁的语言,不仅吸取了很多语言的优点,原生支持的goroutine和channel更是极大简化了并发开发。
也是因为这样,初学leaf框架的时候,遇到了很多问题。虽然在看的过程中遇到不懂的再学也可以慢慢理解框架,但是那样终究还是效率太低,而且理解的很片面,因此我决定系统的学习一下golang的特性,作为我第一篇周记。
golang特性
1.指针
golang的指针与C的指针用法大致相同,不过有几个地方需要注意。
- golang取消了->,也就是说对于一个对象的元素x,无论p是指针变量还是元素本身,都可以用p.x取到。
- 也是因为这个原因,对于数组p[],&p的值是这个数组的地址,而不是p[0]的地址。
2.函数参数及返回值
golang的函数支持多返回值,不必采用在调用参数中添加指针来获取返回值的方式,让代码可读性更高。golang的结构体函数不是定义在结构体里面,而是单独定义在外面,和普通的函数一样,不过在函数名字前面加上结构体的声明就可以了,类似
type A struct{
name string
age int
}
func (obj *A) Hello() (string, int) {
fmt.Println("Hello I'm " + obj.name)
return obj.name, obj.age
}
这样就为A这个结构体定义了一个名为Hello的方法,调用的对象作为指针obj传进函数,返回调用对象的两个属性。
3.面向对象
golang在语言层面并没有显示的支持面向对象,但是确实有方法可以实现。
- 封装在golang中是以包为单位的,首字母大写的函数和变量才能被其他包访问。结构体中的属性小写在json解析的时候不能获取,可以使用
json:"keyName"
这种方式生成key值小写的json串。
type A struct{
a string `json:"A"`
B string
c string
D string `json:"d"`
}
func main() {
obj := A{"a", "b", "c", "d"}
m_json,_:= json.Marshal(obj)
fmt.Println(string(m_json))
}
这样输出的就是 {"B":"b","d":"d"}
- 继承可以用一种叫做组合的方式实现。组合即在一个结构体Child中声明另一个结构体Father,这样Child就可以直接使用father中的所有方法和属性。
type father struct {
Name string
FamilyName string
}
type mother struct {
Name string
}
type son struct{
father
mother
Name string
}
func main() {
son := son{father{"name1", "family" }, mother{"name2"}, "name3"}
fmt.Println(son.FamilyName)
}
对于继承的属性,子类可以直接调用,即son.FamilyName直接使用father的属性。但是如果声明的多个结构体中出现同名的属性或方法,就需要在调用的时候指定使用哪个结构体了。可以使用son.father.Name来指定到底调用的是father中定义的Name。
- 重载不能显性的支持,但是可以使用 args ...interface{} 作为函数参数来支持任意多个参数,其中的interface{}可以代表任意的类型。
4.Goroutine
协程可以说是golang最大的特点了。处理高并发的任务,多线程的缺点在于切换的开销太大,而异步的回调机制又比较繁琐,要考虑各种异常情况,容易出问题。协程兼顾二者的有点,提高了程序的开发效率和运行效率。
下面是在网上找的一张图,基本说明了goroutine的结构。
M:代表真正的内核OS线程,真正干活的人
G:代表一个goroutine,它有自己的栈,instruction pointer和其他信息(正在等待的channel等等),用于调度。
P:代表调度的上下文,可以把它看做一个局部的调度器,使go代码在一个线程上跑
协程可以理解为用户创建的线程,不过他们的调度是在应用层完成的。golang维护了一个线程池,runtime.GOMAXPROCS(n)可以设置池子的大小,默认只有一个,每个线程都维护自己的一个goroutine队列。golang封装了一些异步的系统接口,这样一旦一个正在运行的协程调用了这些系统接口,golang就会把这个协程打包丢到队列里面排队,然后从队列里拿到一个新的goroutine运行。
golang的协程实现非常方便,只需要go funcName()就完成了一个协程的创建过程。但是要注意的是,和创建线程不同,创建的协程会随着主线程结束而结束。
5.Channel和sync
既然有了goroutine来做高并发,自然少不了并发的通信。
- WaitGroup
WaitGroup总共有三个方法:
Add:添加或者减少等待goroutine的数量
Done:相当于Add(-1)
Wait:执行阻塞,直到所有的WaitGroup数量变成0
var waitgroup sync.WaitGroup
func Solve(event int) {
fmt.Println(event)
waitgroup.Done()
}
func main() {
for i := 0; i < 10; i++ {
waitgroup.Add(1)
go Solve(i)
}
waitgroup.Wait()
}
这个WaitGroup可以用来确保goroutine都执行完成或者对某个资源进行监控,很像操作系统里的信号量。
- channel
channel是golang中一种内置类型,channel一共有4中操作:
make:创建channel,第二个参数为缓存的大小,缺省值为0
channel<- :向channel中存入数据
<-channel:从channel中取出数据
close:关闭channel
func DoSth(ch chan int) {
fmt.Println("finish")
<-ch
}
func main() {
ch := make(chan int)
go DoSth(ch)
ch <- 1
}
首先创建一个channel,缓存大小为0即无缓存,创建goroutine,之后主协程调用ch<-向channel中存入数据,等待创建的协程执行<-ch读取。
这里说一下缓存的作用,在存入数据的时候,如果有未满的缓存,则只需要阻塞直到数据存入缓存就结束了,但是如果缓存满了,则需要等待数据被读取。
因为channel的这种阻塞特性,有可能产生死锁,我们必须处理超时的情况。
go func(){
DoSth()
c2 <- "Finish"
}
go func() {
time.Sleep(time.Second * 1)
c1 <- "Time Over"
}()
select {
case msg1 := <-c1:
fmt.Println("time out", msg1)
case msg2 := <-c2:
fmt.Println("received", msg2)
}
这里的select会轮询两个条件,直到有一个满足,则执行对应的操作。如果DoSth在一秒内执行完成,c2中会存入数据,select执行输出received的操作,相反过了一秒DoSth没完成,c1中存入数据,select收到c1的数据,就执行输出time out的操作。
select {
case msg1 := <-c1:
fmt.Println("time out", msg1)
case msg2 := <-c2:
fmt.Println("received", msg2)
default:
fmt.Println("default")
}
如果select中定义了default标签,select立即返回结果,如果c1,c2都为空,则执行default操作。
总结
除了这里的这些特性,学习golang自然还少不了众多的第三方的包。不过这个要在以后的日子里一点一点积累。
这次总结知识梳理了一下golang的一些用法,要想真正掌握这些特性,还是需要把这些东西结合到实际的应用中。