Go学习笔记

字数 4143阅读 420

使用go1.10版本,在liteIde里开发。

1,变量声明后必须使用,不然编译不过(全局变量可以不用)。

2,变量可以不用var关键字(简短形式),如c := 66,但是c必须是没有声明过的,而且c必须在函数中

3, go1.9之后,数字类型可以不加类型关键字,系统自动推断。

4, var(a,b = 9, 99)// 因式分解的写法,一般用于声明全局变量,同一行可以同时声明多个变量,叫并行或同时赋值

5,像int, float, bool,string这些是值类型,这些类型的变量直接指向内存中的值(栈中)。当使用=赋值的时候,实际是在内存中将等号右边的值进行了拷贝。两边地址不一样。更复杂的数据类型,需要使用多个字,这些数据一般使用引用类型保存。如指针类型p1,它的值是一个地址a,指向真正的值v,p2=p1后,拷贝的只是a,指向的仍是同一个真正的值v

6,由于go函数可以多返回,有时候又不需要所有的值,(局部变量声明不用要报错)可以用_表示抛弃值 _,b = 5,7,5就被抛弃

7,常量const关键字除了用于变量不可修改,还可用于枚举,如 const(Unknown =0, Femal = 1, Male = 2)

8,iota,特殊常量,可以认为是可以被编译器修改的常量。每当const关键字出现时,被重置为0,在下一个const出现前,每出现一次iota,所代表的数字自动加1,const(a = iota;b = iota;c = iota ),a,b,c的值依次为0,1,2,后面两个iota可以省略

9,<< ,>>,左移,右移双目运算符,相当于乘以,除以2的n次方(n为移动位数)。

10,select,条件判断语句,跟switch类似,只是select会随机运行一个case,如果没有case,就阻塞,知道有一个case为止,select后不加判断条件。

11,函数定义格式 fund funcName(x type, y type)(type, type){},只返回一个值的话,返回类型括号可以不要。第一个参数的type可以不要,如果两个参数类型一致。

匿名函数命名格式区别是仅仅没有函数名。调用不太一样,如下调用:func(index int){xxx}(paras),paras是参数列表,没有参数就只写小括号。小括号表示匿名函数被调用。如:x, y := func(i, j int) (v1, v2 int) {

        return i + 1, j + 1

    }(9, 99) // 表示直接传入9,99两个参数调用这个匿名函数,x,y的结果分别是10,100

12,函数参数的值传递和引用传递:值传递=调用函数时将实际参数复制一份传递到函数中,这样在函数中对参数的修改不会影响实际参数;引用传递=调用函数时将实际参数的地址传递到函数中,这样在函数中对参数的修改会阴影实际参数。

13,函数作为值:

func maxValue(x, y int) int {

    if x > y {

        return x

    }

    return y

}

fmt.Print(maxValue(11,22)) // 输出22

14, 函数闭包:go支持匿名函数,可作为闭包。匿名函数是一个内联语句或表达式。匿名函数的优越性在于可以直接使用函数内的变量,不必申明。

func getSequence() func() int{

i += 0

return func() int{

i += 1

return i

}

}

nextNum := getSequence() // nextNum 是返回的一个函数

14,方法:方法就是包含接受者的函数。接受者可以是命名类型或者结构体类型的一个值或指针。所有给定类型的方法属于该类型的方法集(感觉就是一个对象的方法)

type Circle struct{

radius float64

}

fun (c Circle) getArea() float64{

return 3.14 * c.radius * c.radius

}

fun main(){

var c1 Circle

c1.radius = 10.00

var area = c1.getArea()

}

15,数组:定义格式 var arrayName[n] type

var a = [5] float32{1,2,3,4,5}

var a1 = […] float32{1,2} // 不设置数组大小,会根据元素个数自动设置。

16,指针:变量是一个占位符,用于引用计算机内存的地址。

 指针变量定义:var varName *type ,var fp *float。空指针用nil表示

指针数组=数组中每个元素指向一个值(每个元素都是一个指针), var ptr [n] *int

17,结构体定义 var structName struct{}

18,切片(slice,就是动态数组),切片不需要说明数组长度。当需要容量大且内容频繁修改的时候,用list更好

定义格式:var s []type 或者用make()创建,var s []type = make([]type, len)

直接初始化: s := [] int {1,2}

其它数组的引用: s := arr[:]

根据其它数组的元素创建新的切片: s := arr[startIndex:endIndex], 一个不写表示前面或后面所有的

通过一个切片创建一个新的切片: s  := s1[startIndex:endIndex],新片中包含startIndex表示的值,不包含endIndex包含的值。s1也可以是一个数组,slice本来也就是基于数组的一个抽象数据类型,是一个数组片段的描述。一个slice结构包括三部分,一个指向底层数组的指针ptr,一个切片的长度len,一个底层数组的长度cap。切片是可以索引的。

make创建: s := make([]int, len, cap) // len表示元素数据个数,默认0,表示最大容量

19,追加切片append():s = append(s, 1, 2), 这里追加两个元素,len+2,cap+4(如果新的切片大小是旧的2倍以上,cap=len,否则,len<1024,cap翻倍,大于1024,cap翻1.25倍)。如果切片作为函数参数,如s是函数外参数,s1是函数参数名,这个s1其实是s的一个复制。但是在函数内,s1[1] = 66这种操作是可以修改s的内容的。因为切片里是根据一个数组指针表示数组的,复制切片后指针指向的地址没变。但是如果用append,增加的只是s1的容量,长度,s不变,所以如果打印,s看起来没变。如果要在函数内根据append修改函数外的值,需要这样写:

func test(s *[]int){*s = append(*s, 22)}

20,切片拷贝copy(): copy(s,s1), 表示把s1中的数据拷贝到s中,s1中的数据顺序不变,从起始位置替换s中的元素。

21,范围(range):range可以枚举数组,切片,unicode支付查等中的索引值和元素值

for i,c := range “go”

22,map:无序键值对的集合,用hash表实现,可以迭代

定义格式:var mapName map[keyType] valueType, 可以用make创建,m := make(map[keyType] valueType)

如 m := make(map[string]string) m[“name”] = “xx”

range可以枚举它 for key := range m;

还可以查看是否有一个key对应的值 value,ok := m[‘name’],  ok是bool值,表示值是否存在

23,接口:一种数据类型,它把所有具有共性的方法定义在一起,任何其它类型只要实现了这些方法就是实现了这个接口,一个应用例子如下:

type Phone interface {

    call()

}

type NokiaPhone struct {

}

func (nokiaPhone NokiaPhone) call() {

    fmt.Println("I am Nokia, I can call you!")

}

type IPhone struct {

}

func (iPhone IPhone) call() {

    fmt.Println("I am iPhone, I can call you!")

}

func main() {

    var phone Phone

    phone = new(NokiaPhone)

    phone.call(

    phone = new(IPhone)

  phone.call()

}

24,defer 语句会延迟函数的执行直到上层函数返回。延迟调用的参数会立刻生成,但是在上层函数返回前函数都不会被调用。延迟的函数调用被压入一个栈中。当函数返回时, 会按照后进先出的顺序调用被延迟的函数调用。

25,go语言的并发支持

go语言相比java等一个巨大的优势就是可以方便的编写并发程序。go语言内置了goroutine机制,可以快速的开发并发程序,更好的利用多核处理器资源。

go语言让事件在新线程中执行变得简单,调用函数的时候前面加上 go 关键字就可以了。但是说到多线程,就不得不说线程之间的通信,我们很多时候要知道子线程何时结束,执行的结果以实现线程的同步。go里用channel(信道)的概念。

和map一样,信道是引用类型,用make分配内存,创建的时候还可以加一个可选的整型参数,以限制缓存区大小。默认为0.

c := make(chan int) // 无缓存整型信道 ;c := make(chan *os,File, 100) // 缓冲的文件指针信道

可以往信道中插入数据 c <- 1; 也可以从信道中读数据 <- c,

例子:

    c := make(chan int)

    go func() {

        for i := 0; i < 10; i++ {

            fmt.Print("go goroutine test......", i, "\n")

        }

        c <- 1

    }()

    fmt.Print("waiting...\n")

    <-c

    fmt.Print("go routine over")

打印结果是:

     waiting… 

     go gorouting test…….0

……

go grouting test….9

go routine over// 如果没有<-c,会一直阻塞。如果信道是非缓存的,则发信者在收信者接收到数据前也一直阻塞。如果信道有缓冲区,发信者只有在数据被填入缓冲区前才被阻塞,如果缓冲区是满的,意味着发信者要等到某个收信者取走一个值

限制channel的吞吐量可以用两种方式,

1,创建的时候添加参数设置。 var see = make(chan int, maxNum)

2,  通过循环,把go func xxx放到循环内

信道可以像其它类型的数值一样被分配内存并传递,此特性常用于实现安全且并行的去复用(demultiplexing)。

3,runtime.GOMAXPROCS(n)n小于cpu核数,显示的设置是否使用多核来执行并发任务

防止主goutine过早的被运行结束的有效手段之一—同步(sync.WaitGroup)

var waitGroup sync.WaitGroup // 用于等待一组操作执行完毕的同步工具

waitGroup.Add(3) // 改组操作的数量3.

chan1 := make(chan int64, 3) // 数字通道1

chan2 := make(chan int64, 3) // 数字通道2

chan3 := make(chan int64, 3) // 数字通道3

sync.WaitGroup代表一个类型,该类型声明存在于代码包sync中,类型名为WaitGroup,add(3)表示我们后面要启用3个

groutine。

下面以一个groutine处理完成再交给第二个grouting为例:

第一个处理:

go fun(){

// 从chan1获取处理对象

process

// 把结果传给第二个chan2

close(chan2)// 关闭chan2

waitGroup.done()// 表示此操作完成,相当于从group中的3减去1

}

第二,三个处理一次接受上一个chan,传给下一个。

调用的时候, 向chan1中传递一个一个数据,完成后关闭chan1.

为了能让这个流程执行完成,末尾还需要加上:

waitGroup.Wait()// 等待前面那组操作(一共3个)的完成。

25,go语言并发机制

线程和进程

现在操作系统中,线程是处理调度和分配的基本单位,进程是资源分配的基本单位。每个进程由私有的虚拟空间,代码,数据和其它各种系统资源组成。线程是进程内的一个执行单元。每个进程至少有一个主线程,系统自动创建。用户根据需要创建其它线程。多个线程并发地运行于同一个进程中。

并行与并发

并发,一个时间段内有很多的线程或进程执行,但在任何时间点上都只有一个在执行,多个线程或进程争抢时间片轮流执行。并行,一个时间段和时间点上都有多个线程或进程在执行。

并行需要硬件支持,单核只能并发。

并发是并行的必要条件,如果一个程序本身就不并发,也就是只有一个逻辑执行顺序,那么不可能让其并行处理。

并发不是并行的充分条件,一个并发程序,还需要多核的支持才能并行。

线程模型的3个分类

线程有用户线程和内核级线程两类。

第一类,多对一模型:多个用户线程映射到一个内核线程,线程管理在用户空间完成,此模式下,用户线程对os透明,这种模型,好处是:线程上下文切换都发生在用户空间,避免模态切换,对于性能有积极影响。缺点是:所有的线程基于一个内核调度实体(即内核线程),这样只有一个处理器被利用,效率低下,并且一个线程阻塞,其它都要等待。

第二类,一对一模型:摸个用户线程映射一个内存线程,每个线程有内核调度器独立调度。这种模型,好处是:多核下,支持并行,效率高,一个线程阻塞,允许其它继续执行。缺点是:每创建一个用户线程都需要创建一个系统线程对应,线程创建的开销大,影响程序性能。

第三类,多对多模型:用户线程和内核线程都有多个(部分用户线程映射一个内核线程)。结合前两种的优点。这种模型需要内核线程调度器和用户线程调度器相互操作,本质上是多个线程被绑定到了多个内核线程上,使得大部分的线程上下文切换发生在用户空间,而多个内核线程又可以充分利用处理器资源。

groutine就采用了第三类模型。grouting机制是协程的一种实现,golang内置调度器,可以让多核cpu中每个cpu执行一个协程。

调度器工作方式

整个调度,包括四个重要部分,M,G, P, sched:

sched是调度器,它维护存储M,G的队列和调度器的一些状态信息

M是系统线程,有os管理,goroutine就是在M上运行。

P是处理器,主要用途就是来执行grouting的,它维护一个grouting队列,即run queue。

G是goroutine实现的核心结构,包含栈,指令指针等,代表一个goroutine 

单核情况下,一个M,一个P,若干个G排队。一个G运行完自己的时间片后,让出上下文,回到runqueue中。多核中,有多个M,每个m有一个p。

线程阻塞情况下,会在创建一个M1,当前的M放弃它的P,P转到M1中去运行。

当一个p的runqueue为空,没有G的时候,P会从另一个上下文偷取一半的G执行。

推荐阅读更多精彩内容

  • 1.1 声明 Go语言中有四个主要的声明:变量(var)、常量(const)、类型(type)、函数(func)。...
  • 类型 引用类型特指slice、map、channel这三种预定义类型。 内置函数new按指定类型长度分配零值内存,...
  • 1.安装 https://studygolang.com/dl 2.使用vscode编辑器安装go插件 3.go语...
  • 出处---Go编程语言 欢迎来到 Go 编程语言指南。本指南涵盖了该语言的大部分重要特性 Go 语言的交互式简介,...
  • fmt格式化字符串 格式:%[旗标][宽度][.精度][arg索引]动词旗标有以下几种:+: 对于数值类型总是输出...