使用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执行。