go语言浅析

context

 Context是上下文传递的核心,它包括了请求处理,响应处理,表单解析等重要工作。
 
1.7 标准库引入 context,中文译作“上下文”,准确说它是 goroutine 的上下文,包含 goroutine 的运行状态、环境、现场等信息。
context 主要用来在 goroutine 之间传递上下文信息,包括:取消信号、超时时间、截止时间、k-v 等。context 几乎成为了并发控制和超时控制的标准做法。
context.Context 类型的值可以协调多个 groutine 中的代码执行“取消”操作,并且可以存储键值对。最重要的是它是并发安全的。
与它协作的 API 都可以由外部控制执行“取消”操作,例如:取消一个 HTTP 请求的执行。
context 用来解决 goroutine 之间退出通知、元数据传递的功能。

遍历通道与关闭通道

 Go 通过 range 关键字来实现遍历读取到的数据,类似于与数组或切片。格式如下:
 v, ok := <-ch
 如果通道接收不到数据后 ok 就为 false,这时通道就可以使用 close() 函数来关闭。

交替打印奇数、偶数

// g1  奇数   g2  偶数  100
func main() {
	c := make(chan int, 100)
	odd, even := make(chan bool, 1), make(chan bool, 1)
	var wg sync.WaitGroup
	wg.Add(2)
	go func() {
		defer wg.Done()
		for i := 1; i < 100; i += 2 {
			<-odd
			c <- i
			even <- true
		}
	}()

	go func() {
		defer wg.Done()
		for i := 0; i < 100; i += 2 {
			<- even
			c <- i
			odd <- true
		}
	}()
	even <- true
	wg.Wait()
	close(c)

	for {
		if v, b := <- c; !b {
			break
		} else {
			fmt.Println(v)
		}
	}
	return
}

string和int互转

s = strconv.Itoa(i) //i为int类型
i, err = strconv.Atoi(s) //s为stirng类型

…和defer

...:
1.为函数定义多个可选参数(同一类型)
FunName(args ...int){}
FunName(arg1)
FunName(arg1, arg2)
FunName(arg1, arg2, arg3)
......
2.将切片拆散
slices := make([]int 6)
FunName(slices ...)

defer : 
   Go语言中有种不错的设计,即延迟(defer)语句,你可以在函数中添加多个defer语句。当函数执行到最后时,这些defer语句会按照逆序执行,最后该函数返回。特别是当你在进行一些打开资源的操作时,遇到错误需要提前返回,在返回前你需要关闭相应的资源,不然很容易造成资源泄露等问题。
defer采用后进先出模式,与栈类似。

聚合根

对领域概念做静态建模的时候,有一类概念被视为聚合根(root)。它有自己的生灭过程,数据和行为聚合于其上,有一个唯一的ID可以表征自己并且可以索引到自己的数据。聚合根表征的是一个实体,但并非所有的实体都应该抽象成聚合根(有些可能应该抽象为注入的数据或者一条待处理的消息)。
外部对象需要访问聚合内的实体时,只能通过聚合根进行访问,而不能直接访问。从技术角度考虑,聚合确定了实体生命周期的关注范围,即当某个实体被创建时,同时需要创建以其为根的整个聚合,而当持久化某个实体时,同样也需要持久化整个聚合。

sync.WaitGroup的用法

(1)无法知道for循环需要睡眠时间的具体时间,因此比time.sleep()更好。
(2)管道也能很完美的完成,但是用于此处大材小用。假设有一万十万更多数量的for循环,也要申请相同数量的管道,这对内存也是不小的开销。相对于使用管道来说,WaitGroup 轻巧了许多。
(3)WaitGroup 对象内部有一个计数器,最初从0开始,它有三个方法:Add(), Done(), Wait() 用来控制计数器的数量。Add(n) 把计数器设置为n ,Done() 每次把计数器-1 ,wait() 会阻塞代码的运行,直到计数器地值减为0。计数器不能给负值。
(4)WaitGroup对象不是一个引用类型,在通过函数传值的时候需要使用地址。(wg *sync.WaitGroup)一定要通过指针传值,不然进程会进入死锁状态。

变量\常量定义(变量定义后需调用,否则报错)

1)变量:大写字母开头其他包可直接用,小写字母开头需要传值才可调用
定义方式: 变量名 := 具体值/var 变量名(类型)(=值)/var(...)
2)常量:常量名全大写
定义方式:const 常量名 (类型)= 值/const(...)

ps:如果枚举时常量未赋初值,则与之前最近类型一致常量值相等

iota使用

从零开始且自增,继承之前规则,打断后需要重新赋iota

For-each range 循环

这种格式的循环可以对字符串、数组、切片等进行迭代输出元素。
for i,v := range area{               //i:索引  v:值  area:被遍历
	fmt.Printf("%d,%d\n",i,s)        //可对应赋值    
	fmt.Println(i,s)
}

goto语句

goto 语句可以无条件地转移到过程中指定的行。goto 语句通常与条件语句配合使用。可用来实现条件转移,构成循环,跳出循环体等功能。但是,在结构化程序设计中一般不主张使用 goto 语句,以免造成程序流程的混乱,使理解和调试程序都产生困难。
a := 1
LOOP: for a<10 {
	a++
	goto LOOP  
}
fmt.Println(a)

go不存在三元运算符

语言设计者已经预见到三元运算符经常被用来构建一些极其复杂的表达式。虽然使用if进行替代会让代码显得更长,但这毫无疑问可读性更强。一个语言只需要有一种条件判断结构就足够了。

将索引为1和3的元素初始化

blance := float32[1:2.0,3:7.0]

接口定义、实现

接口代表一种调用契约,是多个方法声明的集合。

在动态语言中,接口(interface)也被称为协议(protocol)。准备交互的双方,共同遵守事先约定的规则,使得在无须知道对方身份的情况下进行写作。接口要实现的是做什么,而不关心怎么做,谁来做。

指针

一个指针变量指向了一个值的内存地址。在使用前要先声明指针。
声明格式: var var-name *var-type
在指针名称前面加上*号(前缀)来获取指针所指向的内容。
当一个指针被定义后没有分配到任何变量时,他的值为nil。nil指针也称为空指针。指针变量通常缩写为ptr。

结构体

结构体是由一系列具有相同类型或不同类型的数据构成的数据集合。结构体表示一项记录。结构体定义需要使用 type 和 struct 语句。struct 语句定义一个新的数据类型,结构体中有一个或多个成员。type 语句设定了结构体的名称。结构体的格式如下:
type struct_variable_type struct {
	member definition
	member definition
	 ...
	member definition
}
创建新结构体:
type Books struct {
	title string
	author string
	subject string
	book_id int
}
func main() {
(1)Books{"Go 语言", "www.runoob.com", "Go 语言教程", 6495407}
(2)Books{title: "Go 语言", author: "www.runoob.com", subject: "Go 语言教程", book_id: 6495407} 		  //key=>value格式
(3)Books{title: "Go 语言", author: "www.runoob.com"}
}
访问结构体成员,需要用句号.操作符,格式:结构体.成员名
可将结构体类型作为参数传递给函数,可访问结构体变量。
type Books struct {
  ...
}
func main(){
	var Books book1
	printBook(&Book1)
}
func printBook(book *Books){
	fmt.Printf( "Book title : %s\n", book.title)
}

切片

未指定大小的数组。
切片不需要说明长度或使用make()函数来创建切片。
切片可索引,由len()方法获取长度。切片提供了计算容量的方法cap()可以测量切片最长可以达到多少。
切片未初始化之前默认为nil,长度为0。
可通过设置下限及上限来设置截取切片。
增加切片容量,必须创建新的更大的切片把原分片的内容拷贝过来。拷贝切片的copy()方法和像切片追加新元素的append()方法。

新切片与原切片共享底层数组,如新切片内容改变或新切片使用append函数添加元素(长度未超出容量),原切片相应位置的内容会发生改变。
当新切片的长度和容量相等后,继续添加元素新切片会新建底层数组(不超过1000,扩容为原来的二倍;超过1000,扩容因子为0.25,扩容为原来的1.25倍),此后原切片内容不再受新切片影响。

在这里插入图片描述

range 关键字

用于 for 循环中迭代数组(array)、切片(slice)、通道(channel)或集合(map)的元素。在数组和切片中它返回元素的索引和索引对应的值,在集合中返回 key-value 对。
不需要使用元素的序号时,用空白符"_"省略。

Map集合

Map(映射)是一种无序的键值对的集合。Map 最重要的一点是通过 key 来快速检索数据,key 类似于索引,指向数据的值。Map 是无序的,我们无法决定它的返回顺序,这是因为 Map 是使用 hash 表来实现的(使用hash算法将键值放入不同的桶中)。
定义:/* 声明变量,默认 map 是 nil .如未在使用前初始化map,则状态异常(exit status 2)*/
var map_variable map[key_data_type]value_data_type

/* 使用 make 函数 */
map_variable := make(map[key_data_type]value_data_type)
如果不初始化 map,那么就会创建一个 nil map。nil map 不能用来存放键值对。
delete() 函数用于删除集合的元素, 参数为 map 和其对应的 key。

ps:字符串实际上是类型为byte的只读切片。或者说一个字符串就是一堆字节。没有专门的字符类型,需要输出实际为单个字符的变量本身时需要强转成字符串类型。
map不是线程安全的,在并发读写的情况下会panic。Go 1.9 引入一种并发安全的 sync.map。
一般情况下解决并发读写 map 的思路是加一把大锁,或者把一个 map 分成若干个小 map,对 key 进行哈希,只操作相应的小 map。前者锁的粒度比较大,影响效率;后者实现起来比较复杂,容易出错。
使用 sync.map ,对 map 的读写不需要加锁。并且它通过空间换时间的方式,使用 read 和 dirty 两个 map 来进行读写分离,降低锁时间来提高效率。sync.map 适用于读多写少的场景。对于写多的场景,会导致 read map 缓存失效,需要加锁,导致冲突变多;而且由于未命中 read map 次数过多,导致 dirty map 提升为 read map,这是一个 O(N) 的操作,会进一步降低性能。

通道

通道(channel)是用来传递数据的一个数据结构。
通道可用于两个 goroutine 之间通过传递一个指定类型的值来同步运行和通讯。操作符 <- 用于指定通道的方向,发送或接收。如果未指定方向,则为双向通道。
ch <- v    // 把 v 发送到通道 ch
v := <-ch  // 从 ch 接收数据
       // 并把值赋给 v
声明一个通道很简单,我们使用chan关键字即可,通道在使用前必须先创建:
ch := make(chan int)
Ps:默认情况下,通道是不带缓冲区的。发送端发送数据,同时必须有接收端相应的接收数据。

通道缓冲区

通道可以设置缓冲区,通过 make 的第二个参数指定缓冲区大小:
ch := make(chan int, 100)
带缓冲区的通道允许发送端的数据发送和接收端的数据获取处于异步状态,就是说发送端发送的数据可以放在缓冲区里面,可以等待接收端去获取数据,而不是立刻需要接收端去获取数据。
不过由于缓冲区的大小是有限的,所以还是必须有接收端来接收数据的,否则缓冲区一满,数据发送端就无法再发送数据了。

Ps:如果通道不带缓冲,发送方会阻塞直到接收方从通道中接收了值。如果通道带缓冲,发送方则会阻塞直到发送的值被拷贝到缓冲区内;如果缓冲区已满,则意味着需要等待直到某个接收方获取到一个值。接收方在有值可以接收之前会一直阻塞。

空白、公开或未公开标识符

下划线字符(_)在Go 语言里称为空白标识符,有很多用法。这个标识符用来抛弃不想继续使用的值,如给导入的包赋予一个空名字,或者忽略函数返回的你不感兴趣的值。
type user struct{
	Name srting
	Email string 
}
type Admin strcut{
	user 
	Right int
}

初始化:
a := Admin{
  	Right : 1,
}
a.Name = "name"
a.Email = "email"

goland开发工具:ROOTPATH和GOPATH

ROOTPATH:Go安装路径,相当于JAVA的JDK。标准包。
GOPATH:存放sdk以外的第三方类库、自己收藏的可复用的代码。引用包和自定义包。
(1)src存放源代码(比如:.go .c .h .s等) 按照golang默认约定,go run,go install等命令的当前工作路径(即在此路径下执行上述命令)。
(2)pkg:golang编译包时编译时生成的中间文件(比如:.a)  
(3)bin:编译后生成的可执行文件(为了方便,可以把此目录加入到 PATH 变量中,如果有多个gopath,那么使用{GOPATH/bin:}/bin添加所有的bin目录)

Global GOPATH是所有项目都可以使用的,Project GOPATH是只有这个一个项目可以使用的。

goroutine

Go语言最大的特色就是从语言层面支持并发(goroutine),goroutine是Go中最基本的执行单元。事实上每一个Go程序至少有一个goroutine:主goroutine。当程序启动时,它会自动创建。我们在使用Go语言进行开发时,一般会使用goroutine来处理并发任务。
goroutine机制有点像线程池:
go 内部有三个对象: P(processor) 代表上下文(M所需要的上下文环境,也就是处理用户级代码逻辑的处理器),M(work thread)代表内核线程,G(goroutine)协程。
正常情况下一个cpu核运行一个内核线程,一个内核线程运行一个goroutine协程。当一个goroutine阻塞时,会启动一个新的内核线程来运行其他goroutine,以充分利用cpu资源。所以线程往往会比cpu核数更多。
优点:
1、创建与销毁的开销小
线程创建时需要向操作系统申请资源,并且在销毁时将资源归还,因此它的创建和销毁的开销比较大。相比之下,goroutine的创建和销毁是由go语言在运行时自己管理的,因此开销更低。所以一个Golang的程序中可以支持10w级别的Goroutine。每个 goroutine (协程) 默认占用内存远比 Java 、C 的线程少(*goroutine:*2KB ,线程:8MB)
2、切换开销小
这是goroutine于线程的主要区别,也是golang能够实现高并发的主要原因。
线程的调度方式是抢占式的,如果一个线程的执行时间超过了分配给它的时间片,就会被其它可执行的线程抢占。在线程切换的过程中需要保存/恢复所有的寄存器信息,比如16个通用寄存器,PC(Program Counter),SP(Stack Pointer),段寄存器等等。
而goroutine的调度是协同式的,没有时间片的概念,由Golang完成,它不会直接地与操作系统内核打交道。当goroutine进行切换的时候,之后很少量的寄存器需要保存和恢复(PC和SP)。因此gouroutine的切换效率更高。
总的来说,操作系统的一个线程下可以并发执行上千个goroutine,每个goroutine所占用的资源和切换开销都很小,因此,goroutine是golang适合高并发场景的重要原因。

协程和通道

协程:goroutine
通道: channel
goland的runtime在goland中的地位类似于Java的虚拟机,不过go runtime不是虚拟机。
golang 程序生成可执行文件在指定平台上即可运行,效率很高, 它和 c/c++ 一样编译出来的是二进制可执行文件. 我们知道运行 golang 的程序并不需要主机安装有类似 Java 虚拟机之类的东西,那是因为在编译时,golang 会将 runtime 部分代码链接进去.
(1)协程(goroutine)调度(并发调度模型)
(2)垃圾回收(GC)
(3)内存分配
(4)使得 golang 可以支持如 pprof、trace、race 的检测
(5)支持 golang 的内置类型 channel、map、slice、string等的实现
(6)等等
golang 语言相比其它语言有一个特殊之处,它实现了自己的调度模块,并不完全是由计算机操作系统进行调度的(进程、线程). golang 原生支持协程 goroutine,区别于线程、进程. goroutine 的调度由 go runtime 进行,这也是 golang 并发效率高的原因之一.
//使用go关键字声明一个匿名函数,并创建一个goroutine
go func(){
	defer ... //函数退出时调用...
}
//当前goroutine从线程退出,幷放回到队列
//强制调度器切换两个goroutine

runtime.Gosched()

可以使用通道来空值程序的生命周期。
带defaul分支的select语句可以用来尝试向通道发送或者接收数据,而不会阻塞。
有缓冲的通道可以用来管理一组可复用的资源。
语言运行时会处理好通道的协作和同步。

并发和并行

只需用go关键字开启goruntime即可,goroutine 是轻量级线程,goroutine 的调度是由 Golang 运行时进行管理的。
goroutine 语法格式:go 函数名( 参数列表 )。用 go 语句开启一个新的运行期线程, 即 goroutine,以一个不同的、新创建的 goroutine 来执行一个函数。 同一个程序中的所有 goroutine 共享同一个地址空间。    
并发(concurrency)和并行(parallelism)。并行是让不同的代码片段同时在不同的物理处理器上执行。并行的关键是同时做很多事情,而并发是指同时管理很多事情,这些事情可能只做了一半就被暂停去做别的事情了。在很多情况下,并发的效果比并行好,因为操作系统和硬件的总资源一般很少,但能支持系统同时做很多事情。这种“使用较少的资源做更多的事情”的哲学,也是指导Go 语言设计的哲学。
如果希望让goroutine 并行,必须使用多于一个逻辑处理器。当有多个逻辑处理器时,调度器会将goroutine 平等分配到每个逻辑处理器上。这会让goroutine 在不同的线程上运行。不过要想真的实现并行的效果,用户需要让自己的程序运行在有多个物理处理器的机器上。否则,哪怕Go 语言运行时使用多个线程,goroutine 依然会在同一个物理处理器上并发运行,达不到并行的效果。

并发是指goroutine运行的时候是相互独立的,goroutine在逻辑处理器上执行,而逻辑处理器具有独立的线程和运行队列。

线程、进程、协程

进程
  进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动,进程是系统进行资源分配和调度的一个独立单位。每个进程都有自己的独立内存空间,不同进程通过进程间通信来通信。由于进程比较重量,占据独立的内存,所以上下文进程间的切换开销(栈、寄存器、虚拟内存、文件句柄等)比较大,但相对比较稳定安全。

线程
  线程是进程的一个实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位.线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的线程共享进程所拥有的全部资源。线程间通信主要通过共享内存,上下文切换很快,资源开销较少,但相比进程不够稳定容易丢失数据。

协程
  协程是一种用户态的轻量级线程,协程的调度完全由用户控制。协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切回来的时候,恢复先前保存的寄存器上下文和栈,直接操作栈则基本没有内核切换的开销,可以不加锁的访问全局变量,所以上下文的切换非常快。
  单从线程调度讲,Go语言相比起其他语言的优势在于OS线程是由OS内核来调度的,goroutine则是由Go运行时(runtime)自己的调度器调度的,这个调度器使用一个称为m:n调度的技术(复用/调度m个goroutine到n个OS线程)。 其一大特点是goroutine的调度是在用户态下完成的, 不涉及内核态与用户态之间的频繁切换,包括内存的分配与释放,都是在用户态维护着一块大的内存池, 不直接调用系统的malloc函数(除非内存池需要改变),成本比调度OS线程低很多。 另一方面充分利用了多核的硬件资源,近似的把若干goroutine均分在物理线程上, 再加上本身goroutine的超轻量,以上种种保证了go调度方面的性能。

goroutine和线程的区别:
 一个线程可以有多个协程
 线程、进程都是同步机制,而协程是异步
 协程可以保留上一次调用时的状态,但过程重入时,相当于进入了上一次的调用状态
 协程是需要线程拉承载运行的,所以协程并不能取代线程(线程是被分隔的CPU资源,协程是组织好的代码流程)

协程竞争和消除竞争

竞争状态:两个或多个goroutine没有互相同步的情况下,访问某个共享的资源,并试图同时读和写这个资源,就处于相互竞争的状态。
对一个共享资源的读和写必须是原子的。

1.原子函数atomic包:

	例如:
	atomic.AddInt64()
	atomic.LoadInt64()
	atomic.StoreInt64()

2.互斥锁 (mutex)

在代码中创建一个临界区,保证同一时间只有一个goroutine可以执行这个临界区代码。
mutex sync,Mutex
mutex.Lock()
{
	...
}
mutex.Unlock()

ps:原子操作和互斥锁的区别

互斥锁是一种数据结构,使你可以执行一系列互斥操作。
原子操作是互斥的单个操作,这意味着没有其他线程可以打断它。
区别:
(1)互斥锁是一种数据结构,用来让一个线程执行程序的关键部分,完成互斥的多个操作。
(2)原子操作是针对某个值的单个互斥操作。
(3)可以把互斥锁理解为悲观锁,共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程。

使用目的:互斥锁是用来保护一段逻辑,原子操作用于对一个变量的更新保护。
底层实现:Mutex由操作系统的调度器实现,而atomic包中的原子操作则由底层硬件指令直接提供支持,这些指令在执行的过程中是不允许中断的,因此原子操作可以在lock-free的情况下保证并发安全,并且它的性能也能做到随CPU个数的增多而线性扩展。

3.通道:通过发送和接收需要共享的资源,在goroutine之间做同步。

当一个资源需要zaigoroutine之间做共享时,通道在goroutine之间架起了一个管道,并提供了确保同步交换数据的机制。声明通道式,需要制定将要被共享的数据的类型。可通过通道共享内置类型、命名类型、结构类型和引用类型的值或指针。
//无缓冲的整型通道
unbuffered := make(chan int)
//有缓冲的字符串通道
buffered  := make(chan string, 10)

无缓冲的通道是指在接收前没有能力保存任何值的通道。这种类型的通道要求发送goroutine和接收goroutine同时准备好。才能完成发送和接收操作。如果两个goroutine没有同时准备好,通道会导致先执行发送或接收操作的goroutine阻塞等待。这种对通道进行发送和接收的交互行为本身就是同步的。其中任意一个操作都无法离开另一个操作单独存在。发送和接收数据的goroutine进入通道后会被锁住,直到交换完成。
有缓冲的通道是一种在被接收前能存储一个或多个值的铜带。这种类型的通道并不强制要求goroutine之间必须完成发送和接收。只有在通道没有可用缓冲区容纳被发送的值时,发送动作才会被阻塞。这导致有缓冲的通道和无缓冲的通道之间的一个很大的不同:无缓冲的通道保证进行发送和接收的goroutine会在同一时间进项数据交换;有缓冲的通道没有这种保证。

v1, ok := chan
v1表示从通道中取出的值, ok表示通道是否还有值
通道赋值与取值使用 <- 

并发模式

1.runner

runner包用来展示如何使用通道来监视程序的执行时间,若果程序运行时间太长,也可以用runner包来终止程序。当开发需要调度后台处理定时任务的程序的时候,这种模式会很有用。这个程序可能会作为cron作业执行,或者在机遇定时任务的云环境(如iron.io)里执行。

2.pool

pool包使用有缓冲的通道实现资源池,来管理可以在任意数量的goroutine之间共享及独立使用的资源。这种模式在需要共享一组静态资源的情况(如共享数据库连接或者内存缓冲区下非常有用)。如果goroutine需要从池里得到这些资源中的一个,它可以从池里申请,使用完后归还到资源池里。

3.work

work包使用无缓冲的通道来创建一个goroutine池,这些goroutine执行并控制一组工作,让其并发执行。在这种情况下,使用无缓冲的通道比随意指定一个缓冲区大小的有缓冲的通道好,因为这个情况既不需要一个工作队列,也不需要一组goroutine配合执行。无缓冲的通道保证两个goroutine之间的数据交换。这种使用无缓冲的通道的方法允许使用者知道神魔时候goroutine池正在执行工作,而且如果池里的所有goroutine都忙,无法接受新的工作的时候,也能及时通过通道来通知调用者。使用无缓冲的通道不会有工作在队列里丢失或者卡住,所有工作都会被处理。 

new和make

make
make 的作用是初始化内置的数据结构,也就是slice、map和 channel,此时编译器不知道你需要使用多少内存,因为这些数据结构占用的内存是运行时才能知晓的。slice、map、channel 这些类型它是复合类型数据结构,通常是一个结构体+堆内存,因此 make 的额外作用就是初始化这些数据和指针,从这一点看,make 的作用是申请内存,并且初始化数据。
new
new 的作用是根据传入的类型分配一片内存空间并返回指向这片内存空间的指针,此时编译器知道你需要使用多少内存,用于结构体和类型,其实只要是任何指针都可以new,它只负责申请内存,但是不会初始化。我们平时的用法就是new一个结构体,返回一个对象的引用。这个指针指向的内容的值为零,记住不是指针为零。当我们声明一个指针类型时,一定要new一个,就是为了给这个指针分配内存,不然会报内存地址为空。

make 只能用于 map 、slice 、channel ,new 可以是任意类型数据。new只是分配内存,不初始化内存; 而make即分配又初始化内存。所谓的初始化就是给类型赋初值,比如字符为空,整型为0, 逻辑值为false等。

nil切片和空切片

nil 切片 : var a []int
空切片: b := make([]int, 0)
nil切片和空切片最大的区别在于指向的数组引用地址是不一样的:
(1)nil空切片引用数组指针地址为0(无指向任何实际地址)
(2)空切片的引用数组指针地址是有的,且固定为一个值

内存分配TCMalloc

在多线程编程下,追求更高内存管理效率:更快的分配是主要目的。
引入虚拟内存后,让内存的并发访问问题的粒度从多进程级别,降低到多线程级别。这是更快分配内存的第一个层次。
为每个线程预分配一块缓存,线程申请小内存时,可以从缓存分配内存,这样有2个好处:
(1)为线程预分配缓存需要进行1次系统调用,后续线程申请小内存时,从缓存分配,都是在用户态执行,没有系统调用,缩短了内存总体的分配和释放时间,这是快速分配内存的第二个层次。
(2)多个线程同时申请小内存时,从各自的缓存分配,访问的是不同的地址空间,无需加锁,把内存并发访问的粒度进一步降低了,这是快速分配内存的第三个层次。

TCMalloc的几个重要概念:

(1)Page:操作系统对内存管理以页为单位,TCMalloc也是这样,只不过TCMalloc里的Page大小与操作系统里的大小并不一定相等,而是倍数关系。《TCMalloc解密》里称x64下Page大小是8KB。
(2)Span:一组连续的Page被称为Span,比如可以有2个页大小的Span,也可以有16页大小的Span,Span比Page高一个层级,是为了方便管理一定大小的内存区域,Span是TCMalloc中内存管理的基本单位。
(3)ThreadCache:每个线程各自的Cache,一个Cache包含多个空闲内存块链表,每个链表连接的都是内存块,同一个链表上内存块的大小是相同的,也可以说按内存块大小,给内存块分了个类,这样可以根据申请的内存大小,快速从合适的链表选择空闲内存块。由于每个线程有自己的ThreadCache,所以ThreadCache访问是无锁的。
(4)CentralCache:是所有线程共享的缓存,也是保存的空闲内存块链表,链表的数量与ThreadCache中链表数量相同,当ThreadCache内存块不足时,可以从CentralCache取,当ThreadCache内存块多时,可以放回CentralCache。由于CentralCache是共享的,所以它的访问是要加锁的。
(5)PageHeap:PageHeap是堆内存的抽象,PageHeap存的也是若干链表,链表保存的是Span,当CentralCache没有内存的时,会从PageHeap取,把1个Span拆成若干内存块,添加到对应大小的链表中,当CentralCache内存多的时候,会放回PageHeap。如下图,分别是1页Page的Span链表,2页Page的Span链表等,最后是large span set,这个是用来保存中大对象的。毫无疑问,PageHeap也是要加锁的。
	小对象大小:0~256KB
	中对象大小:257~1MB
	大对象大小:>1MB
小对象的分配流程:ThreadCache -> CentralCache -> PagHeape,大部分时候,ThreadCache缓存都是足够的,不需要去访问CentralCache和HeapPage,无锁分配加无系统调用,分配效率是非常高的。
中对象分配流程:直接在PageHeap中选择适当的大小即可,128 Page的Span所保存的最大内存就是1MB。
大对象分配流程:从large span set选择合适数量的页面组成span,用来存储数据。
Go中的内存分类并不像TCMalloc那样分成小、中、大对象,但是它的小对象里又细分了一个Tiny对象,Tiny对象指大小在1Byte到16Byte之间并且不包含指针的对象。小对象和大对象只用大小划定,无其他区分。微对象 :(0, 16B)   小对象:[16B, 32KB]    大对象:(32KB, +∞)

小对象是在mcache中分配的,而大对象是直接从mheap分配的,从小对象的内存分配看起。
Go在程序启动时,会向操作系统申请一大块内存,之后自行管理。
Go内存管理的基本单元是mspan,它由若干个页组成,每种mspan可以分配特定大小的object。
mcache, mcentral, mheap是Go内存管理的三大组件,层层递进。mcache管理线程在本地缓存的mspan;mcentral管理全局的mspan供所有线程使用;mheap管理Go的所有动态分配内存。
极小对象会分配在一个object中,以节省资源,使用tiny分配器分配内存;一般小对象通过mspan分配内存;大对象则直接由mheap分配内存。

垃圾回收GC

root 包括全局指针和 goroutine 栈上的指针。

一、对象丢失问题
在三色标记法的过程中对象丢失,需要同时满足下面两个条件:
	条件一:白色对象被黑色对象引用
	条件二:灰色对象与白色对象之间的可达关系遭到破坏
看来只要把上面两个条件破坏掉一个,就可以保证对象不丢失,所以我们的golang团队就提出了两种破坏条件的方式:强三色不变式和弱三色不变式。

GoV1.3- 普通标记清除法,整体过程需要启动STW,效率极低。
GoV1.5- 三色标记法, 堆空间启动写屏障,栈空间不启动,全部扫描之后,需要重新扫描一次栈(需要STW),效率普通
GoV1.8-三色标记法,混合写屏障机制, 栈空间不启动,堆空间启动。整个过程几乎不需要STW,效率较高。

1.三色不变式

两种不变式:
强三色不变式
规则:不允许黑色对象引用白色对象
破坏了条件一: 白色对象被黑色对象引用
解释:如果一个黑色对象不直接引用白色对象,那么就不会出现白色对象扫描不到,从而被当做垃圾回收掉的尴尬。

弱三色不变式
规则:黑色对象可以引用白色对象,但是白色对象的上游必须存在灰色对象
破坏了条件二:灰色对象与白色对象之间的可达关系遭到破坏
解释: 如果一个白色对象的上游有灰色对象,则这个白色对象一定可以扫描到,从而不被回收

2、屏障机制

根据两种不变式提到的原则,分别提出了两种实现机制:插入写屏障和删除写屏障。

(1)插入写屏障:

规则:当一个对象引用另外一个对象时,将另外一个对象标记为灰色。
满足:强三色不变式。不会存在黑色对象引用白色对象
这里需要注意一点,插入屏障仅会在堆内存中生效,不对栈内存空间生效,这是因为go在并发运行时,大部分的操作都			发生在栈上,函数调用会非常频繁。数十万goroutine的栈都进行屏障保护自然会有性能问题。对于插入写屏障来讲,需记住,插入写屏障最大的弊端就是,在一次正常的三色标记流程结束后,需要对栈上重新进行一次stw,然后再rescan一次。

(2)删除写屏障

规则:在删除引用时,如果被删除引用的对象自身为灰色或者白色,那么被标记为灰色。满足弱三色不变式,灰色对象到白色对象的路径不会断。
解释:白色对象始终会被灰色对象保护
在GC开始时,会扫描记录整个栈做快照,从而在删除操作时,可以拦截操作,将白色对象置为灰色对象。

(3)混合写屏障:

GC刚开始的时候,会将栈上的可达对象全部标记为黑色。
GC期间,任何在栈上新创建的对象,均为黑色。
上面两点只有一个目的,将栈上的可达对象全部标黑,最后无需对栈进行STW,就可以保证栈上的对象不会丢失。有人说,一直是黑色的对象,那么不就永远清除不掉了么,这里强调一下,标记为黑色的是可达对象,不可达的对象一直会是白色,直到最后被回收。
堆上被删除的对象标记为灰色
堆上新添加的对象标记为灰色
因为一个对象之所以可以引用另外一个对象,它的前提是需要另外一个对象可达,不可达的不会出现这种情况。

内存分配

在go中,会通过逃逸分析(变量的作用域有没有跑出函数范围),把那些一次性的对象分配到栈区,如果后续还有变量指向,那么就放到堆区。如果一个局部变量非常大,那么它也应该被分配到堆上而不是栈上。
首先可以肯定的是,如果函数里面的变量返回了一个地址,那么这个变量肯定会发生逃逸。go编译器会	判断变量的生命周期,如果编译器认为函数结束后,这个变量不再被外部的引用了,会分配到栈,否则分配到堆。
如果一个函数结束之后外部没有引用,那么优先分配到栈中(如果申请的内存过大,栈区存不下,会分配到堆)。
如果一个函数结束之后外部还有引用,那么必定分配到堆中

堆上动态内存分配的开销比栈要大很多,所以有时我们传递值比传递指针更有效率。因为复制是栈上完成的操作,开销要比变量逃逸到堆上再分配内存要少的多

数据类型转换

golang没有类似于java中的隐式类型转换
golang中的类型转换分为强制类型转换、类型断言、以及“向上造型”
向上造型这个词是取的Java中的定义,没有复杂的含义,表示将子类转为父类。在golang中达到同样的目的只需要.父结构体即可。

类型断言。注意由指针和非指针实现的方法,断言时的写法不同
TODO .(T) 用来类型断言,返回参数1为断言之后的类型值,如果失败则是nil,参数2为是否断言成功
	如果类型本身就是断言的类型,则断言成功,会转换成这个类型并返回
可以断言的情况:
 1.由接口断言到结构体
 2.由父接口断言到子接口

Gin框架总结

1.上下文对象的创建,需要用sync.Pool来复用内存·
2.Gin底层还是使用net/http包,gin的本质是一个路由处理器
3.每个请求方法都有一个radix树
4.添加中间键的过程就是切片追加元素的过程,也决定了中间件会按照添加时间的先后顺序来执行
5.可以为不同的路由组添加不同的中间件
6.路由组本质上只是一个模板,维护了路径前缀、中间件等信息,让用户省去重复配置相同前缀和中间件的操作
7.新路由器组继承父路由器组的所有处理器
8.如果上下文需要被并发使用,需要使用上下文副本

线上代码本地调试

具体步骤可参考: https://blog.csdn.net/MCKZX/article/details/127627424
1.在deployment进行文件映射,利用debug组件进行调试
2.通过ssh连接调试

GRPC

当一个工作负载下的pod有负载均衡,由于gRPC 是长链接, 启动的时候一个实例启动完成之后, 链接都连在那个pod 上面,后启动的那个实例链接数少、负载低。
解决链接数不均的方法是可将工作负载设置定期断开链接重连机制。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值