go学习笔记

特点

  • 简单,只有25个关键字,并发编程,内存管理处理方便。

  • 高效,编译的静态语言,支持用指针直接访问内存。

  • 生产力,有简洁清晰的依赖管理,独特的接口类型设计,以及对编程方式的约束。

  • 复合大于继承,go不支持继承

背景

  • 面临软件开发的新挑战:
  1. 多核硬件架构
  2. 超大规范分布式计算集群
  3. Web模式导致的前所有为的开发模式和更新速度

工作区

  • GOROOT:Go 语言安装根目录的路径,也就是 GO 语言的安装路径。
  • GOPATH:若干工作区目录的路径。是我们自己定义的工作空间。
  • GOBIN:GO 程序生成的可执行文件(executable file)的路径。

GOPATH

  • Go 语言的工作目录,可以是多个目录路径,每个目录都代表 Go 语言的一个工作区(workspace)。
  • 工作区里面放置 Go 语言的源码文件(source file),以及安装(install)后的归档文件(archive file,也就是以“.a”为扩展名的文件)和可执行文件(executable file)。

源码文件

  • 分为三种,即:命令源码文件、库源码文件和测试源码文件,它们都有着不同的用途和编写规则。

Go 语言源码的组织方式

  • Go 语言的源码也是以代码包为基本组织单位的。代码包的名称一般会与源码文件所在的目录同名。如果不同名,那么在构建、安装的过程中会以代码包名称为准。

源码安装结果

  • 某个工作区的 src 子目录下的源码文件在安装后,如果产生了归档文件(以“.a”为扩展名的文件),就会放进该工作区的 pkg 子目录;如果产生了可执行文件,就可能会放进该工作区的 bin 子目录。

源码安装和构建

  • 安装操作会先执行构建,然后还会进行链接操作,并且把结果文件搬运到指定目录。
构建 安装
命令 build install
操作 编译+打包 编译+打包
库源码文件 存放到临时目录 (检查和验证) 存放到工作区的 pkg 目录下
命令源码文件 源码文件所在的目录 工作区的 bin 目录或环境变量GOBIN目录
  • 命令go get会自动从一些主流公用代码仓库(比如 GitHub)下载目标代码包,并把它们安装到环境变量GOPATH包含的第 1 工作区的相应目录中。如果存在环境变量GOBIN,那么仅包含命令源码文件的代码包会被安装到GOBIN指向的那个目录。

语法

应用程序入口

  • 必须是main包,package main
  • 必须是main方法,func mian(){}
  • 文件名不一定是main.go

差异

  • main函数不支持返回值,通过os.Exit()来返回状态,(而且只能返回数字或者是单个字符,单个字符会被直接转成ASCII码)。
  • main函数不支持传入参数,可以通过os.Args获取参数

测试

  • 测试用例,文件名要以“_test.go”结尾,
  • 每个测试方法名都以大写的Test开始,
  • 参数为(t *testing.T)
func TestFriList(t *testing.T)  {

	a, b := 1, 1
	t.Log(a)
	for i:=0; i<5;i++  {
		t.Log(b)
		a, b = b, a+b
	}
}

变量赋值

  • 可以使用 := 来让程序自动推断类型
  • 在一个赋值语句可以赋值多个变量
  • string类型默认值是空字符串而不是nil
const (
	Monday = 1 + iota
	Tuesday
	Wedensday
)

类型转化

  • go语言不支持隐式类型转换;32位转64位也不行。
  • 别名和原有类型也不支持隐式类型转换;

指针类型

  • 不支持指针运算

算数运算符

  • 不支持前置的++,–,后置++,–是支持的,其他都 支持
  • == 在比较两个数组时,只有相同维度相同元素个数时才可以。

条件和循环

  • go只有for没有while,可以直接用for代替while。
  • if 语句condition表达式必须是布尔值,condition支持前面赋值
if var declaration; condition{
}
switch {
case i % 2 == 0 :
	t.Log("Even")
case i % 2 == 1:
	t.Log("Odd")
default:
	t.Log("not zero")
}

switch i {
case 0,2:
	t.Log("Even")
case 1,3:
	t.Log("Odd")
default:
	t.Log("not zero")
}

数组

  • 定义:数组需要指定类型和元素个数,内存空间是固定。
var arr [3]int
arr := [...]int{1,2,3}

//数组切片,前闭后开
arr[开始索引, 结束索引]

数组遍历

for i:=0; i<len(a);i++  {
t.Log(i, a[i])
}
//类似foreach写法
for key, value := range a{
t.Log(key, value)
}
//不想要下标,用_代替
for _, value := range a{
t.Log( value)
}

切片

  • 切片是没有固定长度的,每一次长度变化都会将原来的数据进行拷贝,并分配新的内存空间,
  • 不能用 == 比较
var s0 []int
s0 = append(s0, 1)
s := []int{}
s1 := []int{1,2,3,4}

s2 := make([]int, 3, 5) //3是指初始化的元素个数,(用len查看),初始化默认值为0,5是最大长度(用cap查看),违背初始化的元素无法访问,每一次cap增长都是前一次的2倍。

Map

  • Map是没有固定长度的,以键值对的形式存在;
  • 访问不存在的key时,会返回value类型值的默认值,string是空字符串,int是0,不能通过nil来判断是否存在。
  • value可以是一个函数,来构建工厂模式。
m := map[string]int{"s":1,"d":1,"p":1}
m := make(map[string]int, 9)

//Map工厂模式
m := map[int]func(op int)int{}
m[1] = func(op int) int {
return op
}
m[2] = func(op int) int {
return op*op
}
m[3] = func(op int) int {
return op*op*op
}

t.Log(m[1](2), m[2](2), m[3](2))

字符串

  • 与其他编程语言的差异
  1. string是数据格式,不是引用或指针类型
  2. string是只读的byte slice,len函数可以计算它包含的byte数,而不是字符数
  3. string的byte数组可以存放任何数据

函数

  • 类似python的闭包
func timeSpent(inner func(op int) int) func(op int) int {

	return func(n int) int {
		start := time.Now()
		ret := inner(n)
		fmt.Println("time spent: ", time.Since(start).Seconds())
		return ret
	}
}

func slowFunc(op int) int {
	time.Sleep(time.Second *2)
	return op
}

defer函数

  • 在函数内容定义defer函数,会在函数执行成功之后再运行,一般可用来释放锁。
  • 用panic抛出错误,defer函数仍会执行,但是panic后面的程序不再执行
  • 多个defer的话,先入后出。

面对对象

  • 不支持继承,可以进行复合,但是复合也可以获得相关的方法,但是不能继承那样可以对父类方法进行重写来实现重载。
  • 加上*表示指针引用,指针引用和值引用结果是一样的,但是指针引用不会占用新的内存。
type Empleyee struct {
	Id int
	Name string
	Age int
}

func TestMyObject(t *testing.T)  {
	e := Employee{0, "wc", 24}
	e1 := Employee{Id:2, Name:"wen"}
	e2 := new(Employee)
	e2.Id = 3
	e2.Name = "cai"
	t.Log(e, e1, e2)
}

接口

  • 接口是非入侵性的,实现是不依赖于接口定义的
  • 接口的定义是可以包含在接口使用者包中
type Program interface {
	Write() string
}

type GoProgram struct {
	Id string
	Name string
}

func (g *GoProgram) Write() string {
	return "hello world"
}

func TestInterface(t *testing.T)  {
	var p Program
	p = new(GoProgram)
	t.Log(p, p.Write())
}

init 函数

  • 包在被调用的时候,会先执行init函数
  • 一个包中可以存着多个init函数
  • 包中init函数的执行顺序与包被调用的依赖关系有关

  • 包必须放在GOPATH或者GOROOT的src或vender文件夹下面,才可以被调用
  • 远程包可以通过go get获取到本地
go get -u https://github.com/easierway/concurrent_map
  • go不能同时使用同一个包的不同版本
  • go的包管理工具:glide

协程Goroutine

  • 协程与线程thread区别:
    jdk中stack默认大小是1M,而go中stack是2k
  • KSE(kernel space entity)对应关系:
    Java Thread 是1对1,而go的Groutine是多对多

Goroutine的使用

  • 在函数的调用者那里使用,而不是写在函数内部。
  • 一定要能够在Goroutine外部能够控制Goroutine的生命周期。
  • 要做好超时控制。

共享内存并发机制

  • go中的协程不是线程安全的,需要用锁来进行保护,使用锁的时候同时会用defer来最后释放锁,防止程序被整体挂起。
  • WaitGroup线程同步方法,当wait中的程序执行完成之后才能继续往下执行。
func TestGroutine(t *testing.T)  {

	var mut sync.Mutex
	var wg sync.WaitGroup
	sum := 0
	for i:=1; i<=10000; i++ {
		wg.Add(1)
		go func (i int) {

			defer func() {
				mut.Unlock()
			}()
			mut.Lock()
			sum ++
			wg.Done()
		}(i)
	}
	wg.Wait()
	//time.Sleep(time.Microsecond * 1000 )
	t.Logf("counter is %d", sum)
}

Channel

  • 对于同一个Channel,发送操作和接受操作都是互斥的。
  • 整个发送操作和整个接受操作都是原子性的不可分割。发送操作分为:复制元素值,和放置副本到队列中两个操作。接受操作分为:复制Channel内元素值,放置副本到接受方,删除Channel内元素值三个操作。
  • 发送操作和接受操作在完成之前都是会被阻塞的。
  • 往Channel中发送nil会阻塞,Channel是引用类型,所以它的零值就是nil。当我们只声明该类型的变量但没有用make函数对它进行初始化时,该变量的值就会是nil。
  • 通道一旦关闭,再对它进行发送操作,就会引发 panic。
  • 关闭一个已经关闭了的通道,也会引发 panic。

非阻塞的Channel,长度为0,同步执行

  • 发送方和接收方必须同时在线,发送方发送的过程中,接收方先一直等待。
  • 数据是直接从发送方复制到接收方的,中间并不会用非缓冲通道做中转。

阻塞的Channel,长度大于0,异步执行

  • 设置一个Channel容量,在容量没有满时,发送方可以一直发新的消息,如果容量满了发送消息会进行阻塞,需要等待接收方处理才能发送。
  • 通道会优先通知最早因此而等待的、那个发送操作所在的 goroutine,后者会再次执行发送操作。

channel的关闭

  • 向关闭的channel发送数据,会导致panic
  • channel可以返回两个值,第一个是数据第二是channel的状态,状态为true时表示正常接收,状态为false时表示通道关闭。
  • 所有channel的接受者都会在channel关闭时,立即从阻塞的等待中返回。

func dataProducer(ch chan int, wg *sync.WaitGroup)  {
	go func() {
		for i:=0; i<10; i++ {
			ch <- i
		}
		close(ch)
		wg.Done()
	}()
}

func dataReceiver(ch chan int, wg *sync.WaitGroup)  {
	go func() {
		for {
			if data, ok := <-ch; ok {
				fmt.Println(data)
			} else {
				break
			}
		}
		//for i:=0; i<10; i++ {
		//	data := <-ch
		//	fmt.Println(data)
		//}
		wg.Done()
	}()
}

func TestCloseChannel(t *testing.T)  {
	var wg sync.WaitGroup
	ch := make(chan int)
	wg.Add(1)
	dataProducer(ch, &wg)
	wg.Add(1)
	dataReceiver(ch, &wg)
	wg.Add(1)
	dataReceiver(ch, &wg)
	wg.Add(1)

}

CSP

  • CSP模型是上个世纪七十年代提出的,用于描述两个独立的并发实体通过共享的通讯 channel(管道)进行通信的并发模型。 CSP中channel是第一类对象,它不关注发送消息的实体,而关注与发送消息时使用的channel。

多路选择和超时

  • select实现多渠道的选择,不同渠道之间执行跟代码的顺序无关,一般用来处理超时问题,防止主线程一直阻塞。
func service() string {
	time.Sleep(time.Millisecond * 50)

	return "Down"
}

func otherTask()  {
	fmt.Println("working on something else")
	time.Sleep(time.Millisecond * 100)
	fmt.Println("work down")
}

func AsyncService() chan string  {
	retCh := make(chan string, 1)
	go func() {
		ret := service()
		fmt.Println("return result")
		retCh <- ret
		fmt.Println("service exited")
	}()

	return retCh
}

func TestAsyncService(t *testing.T)  {

	retCh := AsyncService()
	otherTask()
	fmt.Println(<-retCh)
}

func TestService(t *testing.T)  {

	select {
	case ret := <-AsyncService():
		t.Log(ret)
	case <-time.After(time.Millisecond*100):
		t.Error("time  out!")
	}
}

从 panic 被引发到程序终止运行的大致过程是什么

  • 我们先说一个大致的过程:某个函数中的某行代码有意或无意地引发了一个 panic。这时,初始的 panic 详情会被建立起来,并且该程序的控制权会立即从此行代码转移至调用其所属函数的那行代码上,也就是调用栈中的上一级。

  • 这也意味着,此行代码所属函数的执行随即终止。紧接着,控制权并不会在此有片刻的停留,它又会立即转移至再上一级的调用代码处。控制权如此一级一级地沿着调用栈的反方向传播至顶端,也就是我们编写的最外层函数那里。

  • 这里的最外层函数指的是go函数,对于主 goroutine 来说就是main函数。但是控制权也不会停留在那里,而是被 Go 语言运行时系统收回。

  • 随后,程序崩溃并终止运行,承载程序这次运行的进程也会随之死亡并消失。与此同时,在这个控制权传播的过程中,panic 详情会被逐渐地积累和完善,并会在程序终止之前被打印出来。

评论