golang学习

前言

因为工作中主要用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的结构。

协程.jpg

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的一些用法,要想真正掌握这些特性,还是需要把这些东西结合到实际的应用中。

你可能感兴趣的:(golang学习)