Go其实是一个不严格是面向什么的语言,和传统的面向对象变成是有区别的。
go没有类的定义,但是结构体在使用上其实和类差不多,可以通过struct实现OOP特性。
去除了传统OOP的方法重载、构造函数和析构函数,隐藏了this指针
但是仍然具有面向对象的继承、封装和多态的特性,只不过实现传统面向对象的不一样,如继承不用extends关键字而是使用匿名字段实现。
Go语言严格区分大小写,入口执行函数是main(),不需要手动在语句末尾添加分号,编译器会自动添加。
是按照一行进行编译的,所以一行只有一个语句。如果有没有用的变量和包,是会报错的。
声明变量可以不用赋值
赋值
var a int=1
//等价于
a:=1
//数组定义
var x [2]int
var array=[5]int{1,2,3,4,5}
array:=[5]int{1,2,3,4,5}
//数组使用[...]表示不确定长度初始化
array:=[...]float32{10.0,2.0,3.4,50.0}
//数组可以调用append的方式添加其中
animals:=[][]string{}
row:=[]string{"bird"}
animals=append(animals,row1)
//等价于
x:=1
//静态常亮,会自动判断对象类型
const s string = "123"
const s = "123"
//可以表示枚举类型
const{
one = 1
two = 2
}
有一个iota,这个是可以让编译器修改这个值,iota在const出现的时候被重置为0,每执行一行const就加1
const{
a=iota
b
c
}
//输出是:0,1,2。因为默认值为0,执行一行iota会加1
const (
a = iota //0
b //1
c //2
d = "ha" //独立值,iota += 1
e //"ha" iota += 1
f = 100 //iota +=1
g //100 iota +=1
h = iota //7,恢复计数
i //8
)
//输出是:0 1 2 ha ha 100 100 7 8
const (
i=1<<iota
j=3<<iota
k
l
)
func main() {
fmt.Println("i=",i)
fmt.Println("j=",j)
fmt.Println("k=",k)
fmt.Println("l=",l)
}
//输出是:1,6,12,24.就是如果这一行不写,就会执行上一行的命令,然后iota再+1,就是3<<2和3<<3
输出
只有fmt.Printf是格式化输出,使用%v输出变量的值value,如果是%+v则会以k-v形式打印出变量,比如对于结构体
//引入包
import "fmt"
fmt.Print("xxx")、fmt.Print("xxx")
fmt.Printf("xxx")
prin和pting("xxx") println("xxx")
if不用说了,差不多,条件不带括号,代码块带括号
switch语句
这个还不太一样,C语言的switch有break,不然会走所有case。这个不需要break也不会走所有case,只会走当前case
有特殊用法,可以判断interface接口中实际存储的变量类型
var x interface{}
switch i := x.(type) {
case nil:
println("是nil")
case *int:
println("*int", i)
case int:
println("这是int")
}
虽然不需要用break控制他不继续往下走case,但是可以使用fallthrought控制他继续往下走case
循环语句
for循环
//假设animals是一个二维数组
for i:=range animals{
fmt.Print(snimals[i])
}
然后,虽然不需要使用break,但是在for循环之类的还是可以用break跳出循环之类的结构,而且可以通过标记指示break需要跳出哪个代码块。
如下,就会跳出re包含的代码块
re:
for i:=0;i<10;i++{
break re
}
对于continue也可以通过标记指定需要从哪个代码块开始重新执行。
go里面是有goto这个关键词的,可以实现条件转移、跳转循环之类的,但是也不推荐使用,和C语言一样
LOOP:
xxx
goto LOOP
yyy
函数的定义如下
func {name}([parameters]) [return type]{
...
}
//示例
func test(a int) int {
return a + 1
}
返回值可以返回多个返回值
func swap(x, y string) (string, string) {
return y, x
}
有几个内置函数
参数
参数类型包括值类型和引用类型
值类型包括:int、float、bool和string,其变量直接指向内存中的具体值,当复制的时候其实就是拷贝的过程。可以使用&获取到变量地址,和C语言其实一样。
值传递的参数在函数中使用是通过拷贝的方式,形参不会修改实参的值
引用的方式就会改变实参的值,就是指针的方式
func swap(a *int, b *int) {
var temp int
temp = *a
*a = *b
*b = temp
}
也可以在函数内部创建一个函数
func main(){
/* 声明函数变量 */
getSquareRoot := func(x float64) float64 {
return math.Sqrt(x)
}
/* 使用函数 */
fmt.Println(getSquareRoot(9))
}
go也有闭包,闭包的优势就在于可以直接使用函数的内部变量不需要进行声明
就是外部函数的返回值是一个内部函数,直接在return写内部函数体
两种声明方式
make和声明空切片
//声明一个没有指定大小的数组就可以定义切片,容量长度为0,可以通过append添加元素
var x []int
x:=[]int{}
//使用make()创建切片,指定长度为10
var slice []int=make([int],10)
//声明初始化,简写
slice := make([]int, 3)
slice = []int{1, 2, 3}
//添加元素,返回值是原切片,第一个参数是原切片,第二个是插入的
slice=append(slice,2)
使用的话比如截取之类的,就类似python的list使用方法,中括号和冒号
扩容
切片的默认的capacity是0,然后如上给他初始化{1,2,3}以后,长度和容量变为3。
之后如果删除元素,容量不变,长度减少
添加元素,长度增加,如果大于容量值了,会进行扩容
然后不需要扩容的时候append返回的是原底层数组的原切片,扩容以后的话返回的是新底层数组的新切片,地址会变。
扩容会调用growslice()函数,这个函数最后两行有一个是capmem和newcap,后者会对内存进行一个对齐,具体和内存分配策略有关,所以结果不一定是1.25的整数倍。
go是按照size class大小分配的,有一个数组
,在其中找到一块最小的满足要求的内存分配runtime/sizeclasses.class_to_size
var class_to_size = [_NumSizeClasses]uint16{0, 8, 16, 32, 48, 64, 80, 96, 112, 128, 144, 160, 176, 192, 208, 224, 240, 256, 288, 320, 352, 384, 416, 448, 480, 512, 576, 640, 704, 768, 896, 1024, 1152, 1280, 1408, 1536, 1792, 2048, 2304, 2688, 3072, 3200, 3456, 4096, 4864, 5376, 6144, 6528, 6784, 6912, 8192, 9472, 9728, 10240, 10880, 12288, 13568, 14336, 16384, 18432, 19072, 20480, 21760, 24576, 27264, 28672, 32768}
比如一个切片里面有五个int,只需要40字节,但是会找到一块48字节的内存给他用。
截取
可以用一个切片中截取部分数据赋值给新的切片,用法就类似于python的listx:=slize[1:3]
截取就是new一个新的slice,底层数组服用原先的。长度是截取数据的个数,容量是截取开始下标到原切片的结尾。
slice:=[int]{1,2,3,4,5,6}
s1:=s[1:3]
//s1的内容为2和3,长度为2,容量为2,因为是从开始下标算起,从1开始到末尾,
使用copy(sc,s)
可以实现将s切片拷贝给sc切片。拷贝数据的个数是min(len(s),len(sc))
,因此需要两个数组大小一致才能完全拷贝
一个切片结构由三部分组成:指针、长度和容量。指针指向底层数组,长度就是当前长度,容量是底层数组的长度。
切片只是一个只读对象,相当于是对数组指针的一个封装。
Go的值传递,用切片传递数组参数既可以节省内存也可以更好处理共享内存问题。
但是并不是什么时候都适合用切片代替数组,因为切片底层数组可能会在堆分配内存,而且小数组在栈上拷贝的消耗未必比make消耗大。
声明一个map,可以直接map也可以使用make创建
var map_map map[ke_type]value_type
var map_map=make(map[ke_type]value_type)
然后赋值的时候就是普通的字典使用方式test_map[“111”]=“222”
然后有一点好的是,在根据键获取值的时候,可以有两个返回值,第一个是值,第二个是一个布尔型,如果存在,就返回值和true。否则返回空字符串和和false
delete()
函数可以删除集合中的元素,根据键
GO中map的底层实现
没有总结完
底层是一个hash表,通过键值进行映射。
buckets中存放了哈希中的最小粒度单元bucket桶,数据通过hash函数均匀分布在每个bucket中,其存放的是bucket的地址
bucket
每个bucket最多存放八组key-value,多余的会放在下一个bmap里面,使用overflow指向,这些数据都不是显式保存的,通过偏移量计算得到的。
查找
用高八位和第八位哈希进行定位查询,高八位查询bucket是否有对应值(在bucket中的位置),低八位确定数据在哪个bucket。
如果找不到返回对应类型的0值
插入元素
负载因子
键值对的数据与桶的数目的比值叫负载因子。
触发扩容的两种方式
渐进式扩容
存储较多的键值的是哦胡,一次迁移需要成本太大了,因此先分配够多的桶,然后使用一个旧的字段记录旧同位置,一个字段记录迁移进去,相当于偏移量。如果当前是扩容阶段,完成一部份键值对任务的迁移。
将键值对的迁移分成多次进行,避免一次的扩容带来抖动。
等量扩容
创建和旧的一样多的bucket,把原来的复制进去。
为什么呢负载因子没满bucket满了呢,因为有很多键值被删了的情况。导致中间可能会出现空位,导师会溢出很多,这个实际上是一种整理。元素在bucket内会重排,不会换桶
增量扩容
当前bucket不够的时候,扩容,元素会重排,可能会发生桶偏移。
负载因子过大的时候就考虑一个新bucket,新的长度是原有的两倍,旧数据弄到新bucket求。如果一次完整搬迁可能会很耗时间,因此用逐步搬迁,每次访问map都会触发一次,每次搬两个键值对。
Go语言基础 - 接口
接口是一种数据类型,可以把具有共性的方法定义在一起,其他类型只需要实现这个接口就可以实现其中的方法。
大概的用法感觉主要就是体现在函数传参之类的,如果有多个结构体,如果有一个函数需要将这些结构体全部作为参数,那么需要好多个函数,因为参数类型不一样。但是如果用了接口,就只需要一个函数,参数类型是接口类型,然后所有实现接口的所有方法的结构体都可以作为参数进来。
大致步骤:
声明一个接口
定义一个结构体
给结构体绑定所有的接口方法
使用,在函数参数中将类型写成接口即可
代码
type Phone interface {
buy()
}
type Apple struct {
Price int
id string
}
//函数
func (Apple Apple) buy() {
fmt.Println("买了一个手i及")
}
func use(MyPhone Phone) {
MyPhone.buy()
}
参数的传递可以是值传递也是引用传递。引用传递就是传一个指针进去,可以直接修改对象信息
java里面的接口是显式声明,go里面的叫隐式声明,只要一个类型实现了接口中规定的方法,那么就实现了这个接口。
然后只要多个类实现了一个接口,其中一个类型的变量就可以直接赋值为另外几个类型的变量。
然后其实可以实现多个接口,只需要给结构体分别绑定所有方法即可。
如果是空接口interface{},作为函数参数的话,表明可以接受任意类型的参数;如果作为map的value类型,那么value可以储存任意类型
test_map_2 := make(map[string]interface{})
test_map_2["1"] = "1"
test_map_2["2"] = 2
test_map_2["3"] = false
fmt.Println("测试空接口作为map的value类型")
for k, v := range test_map_2 {
fmt.Println(k, v)
}
接口值
因为接口的值可以是任意一个实现该接口的类型变量值,因此接口除了记录值以外还需要记录这个值的类型,因此由两部分组成,type和value
如下两种,第一种value是Dog的值Name,type则是*Dog类型;第二种其动态值则为nil,而类型仍然为*Dog
type Mover interface{
move()
}
//Dog是实现接口的结构体
var m Mover
m=&Dog{Name:"123"}
m=new(Dog)
接口变量(如m)之间是可以比较的
类型断言
接口值可能为任意类型
可以使用fmt包直接打印出类型
fmt是使用反射机制在程序运行时获得动态类型的名称的
fmt.Printf("%T\n",m)
可以使用x.(type)
获得对应实际值的类型。
返回两个参数,第一个是x转化为T后的变量,第二个是布尔值,断言是否成功
x.(T)
var n Mover = &Dog{Name: "旺财"}
v, ok := n.(*Dog)
if ok {
fmt.Println("类型断言成功")
v.Name = "富贵" // 变量v是*Dog类型
} else {
fmt.Println("类型断言失败")
}
如果有多个可以使用switch判断。
switch x.(type){
case int:xxx
case bool:yyy
default:zzz
}
是一种特殊类型,存在两种接口,一种是带有方法的接口,一种是不带方法的接口。所有变量都可以赋值给空的接口变量,实现接口中定义的方法的变量就可以赋值给带方法的接口变量,并且可以通过接口直接调用对应方法,实现多态。
内部定义
没有方法的接口在内部定义为eface
结构体;有方法的接口内部定义为iface
结构体。
两种类型都是定义为两个字段的结构体,大小都是16字节。
Go语言中有一个_type
类型结构体,记录某些数据类型的一些基本特征,比如占用的内存大小,类型名称等。每个类型都有一个与之对应的结构体,如果比较特殊的类型,可以对其扩展,如接口的类型结构体就是interfacetype
,包含有额外字段。
看不懂底层实现
包含有goroutine的运行状态、环境、现场等信息
主要用于在goutine之间传递上下文信息,包含有:取消信号、超时时间、截至时间、k-v等
context.Context类型的值可以协调多个goroutine中的代码进行取消操作,并且可以存储键值对。是并发安全的,比如进行取消HTTP请求的操作。
起因
Go经常用于编写高并发的后台服务,搭建http server,通常一个请求里可以请求若干个goroutine,有的进行数据库操作,有的调用接口。他们之间需要共享同一个请求的一些基本信息,比如登录的token,处理请求的最大超时时间等,如果到达超时,需要关闭处理的goroutine,资源会说。
go的server模型其实是协程模型,一个协程处理一个请求。
一个场景
在业务高峰期的时候,有的服务响应变慢,没有超时机制,等待服务的协程越来越多,资源消耗逐渐增大,然后可能就会出意外。这给情况只要设置一个超时时间就行了,如果超时还没有得到数据,就返回默认值或者报错。
Go中不能直接杀死协程,一般通过channel+select进行控制。但是有时候一个请求有多个协程处理,需要共享一些全局变量等等,而且需要被同时关闭,就可以用context上下文控制。
context用于解决goroutine之间的退出通知和原数据传递的问题
看链接吧
然后
Context接口主要由四个方法
type Context interface {
// 当 context 被取消或者到了 deadline,返回一个被关闭的 channel
Done() <-chan struct{}
// 在 channel Done 关闭后,返回 context 取消原因
Err() error
// 返回 context 是否会被取消以及自动取消时间(即 deadline)
Deadline() (deadline time.Time, ok bool)
// 获取 key 对应的 value
Value(key interface{}) interface{}
}
cancel()函数
功能就是关闭channel:c.down(),递归取消所有子节点,从父结点删除自己。
如下是个简单用法,给参数传入一个k-v
func main() {
ctx := context.Background()
process(ctx)
ctx = context.WithValue(ctx, "traceId", "qcrao-2019")
process(ctx)
}
func process(ctx context.Context) {
traceId, ok := ctx.Value("traceId").(string)
if ok {
fmt.Printf("process over. trace_id=%s\n", traceId)
} else {
fmt.Printf("process over. no trace_id\n")
}
}
取消goroutine
场景:启动一个功能定期向后端轮询,后台启动一个协程,然后客户端关闭功能,需要退出goroutine
普通的方法可能会在goroutine的循环里加一个flag,通过true和false判断是否继续。
如果goroutine多了,循环嵌套多了就不好了。
使用context处理,在go的函数里使用select进行判断一下。context本身是没有取消函数的,是保证只能从外部函数调用取消函数,避免子节点调用。
func Perform(ctx context.Context) {
for {
calculatePos()
sendResult()
select {
case <-ctx.Done():
// 被取消,直接返回
return
case <-time.After(time.Second):
// block 1 秒钟
}
}
}
ctx, cancel := context.WithTimeout(context.Background(), time.Hour)
go Perform(ctx)
// ……
// app 端返回页面,调用cancel 函数
cancel()
Go最大的特点就是从语言层面支持并发,不用关心底层逻辑、内存管理,只要编写好自己的业务逻辑。也自带一个比较好的垃圾回收机制,不用关心线程的创建和销毁。
通过go关键词可以开启goroutine,这个是一种轻量级线程,有自己的栈和程序计数器等,由GoLang负责调度。
同一个程序中的所有go routine共享一个地址空间
并发是基于CSP(通信顺序进程)的,这个模型是描述两个独立并发实体通过共享管道(channel)进行通信的模型。
go关键字其实就是协程,实现了goroutine的内存共享,比线程更加好用高效
协程和线程的区别
最重要的区别就是线程切换需要进入内核态,进行上下文的切换,而协程在用户态就可以进行切换,开销小。
用户可以自行决定何时切换协程
线程固有的栈大小是2M,用于保存局部变量之类的信息;而goroutine里固定大小的栈可能会资源浪费,使用动态扩张收缩策略,初始化2KB,最大可以到1GB
go RuntineTask("go")
RuntineTask("普通")
time.Sleep(time.Second * 1)
}
func RuntineTask(Message string) {
for i := 0; i < 10; i++ {
fmt.Println(Message)
}
}
这里一定要给一个延时感觉,不然就go来不及执行协程他就结束main了
不对,也可以通过一个计数器实现sync.WaitGroup
协程终止会调用defer函数,这个函数里执行defer 函数,函数里调用wg.down
表明计数器减1
当计数器0-的时候程序结束了,不然还没执行程序就结束了。
var wg sync.WaitGroup
wg.Add(2)
go func() {
defer wg.Done()
for i:=1; i<100;i++ {
fmt.Println("A:",i)
}
}()
go func() {
defer wg.Done()
for i:=1; i<100;i++ {
fmt.Println("B:",i)
}
}()
wg.Wait()
channel
channel是连接协程之间的一个机制,可以让一个协程发送特定值到另一个协程。
go语言中channel是一个特殊的类型,相当于一个传送带或者队列,遵循先入先出的原则,保证收发顺序,声明一个channel的时候需要指定数据类型。
使用-<
操作符用于指定通道的方向,如发送还是接收
//声明一个通道
ch := make(chan int)
v:=3
//传递数据
ch-<v
v:=-<ch
默认情况下channel是没有缓冲区的,是阻塞式,两端必须同时有发送和接收才行。
也可以在初始化的时候指定一个大小作为缓冲区,这样就可以异步了,当存储的数据超过缓冲区时,就会停止插入数据。发送方会阻塞到值被拷贝到缓冲区,然后缓冲区满了以后,就会阻塞到直到缓冲区有空余
一个小例子
func testchannel(Num int, ch chan int) {
ch <- (Num * Num)
}
func main(){
ch := make(chan int)
go testchannel(4, ch)
go testchannel(5, ch)
aa := <-ch
bb := <-ch
fmt.Println(aa, bb)
}
也可以使用range遍历读取的数据。
通过close(cn)
可以关闭通道。
CPU感知不到goroutine的,只知道内核线程,M就是对于内核线程的一种封装,Go调度本质就是将G分配给M执行。
设计思想
Go的runtine实现了一个小型任务调度器,可以将CPU资源分给每个任务,主要三个函数
Gosched()
使当前Go协程放弃处理器,让其他协程运行,不会挂起协程,未来会恢复运行。Go的协程是抢占式的,长时间执行或者系统调用的时候会把CPU释放,让其他runtine运行。
出现这几种情况就会发生调度
Goexit():
终止调用的Go协程,其他协程不影响,在终止之前会执行所有defer函数。
go协程需要等主函数执行完再退出,如果主线程停了好像就不运行了,需要在main加个Sleep或者用WorkGroup
func RuntineTask1() {
defer fmt.Println("这是task 1的defer")
fmt.Println("这是task2 的普通函数")
}
func RuntineTask2() {
defer fmt.Println("这是task 2的defer")
fmt.Println("这是 的普通task2")
go RuntineTask1()
go RuntineTask2()
GOMAXPROCS
设置程序运行时使用的CPU数,默认使用最大CPU计算。设置的同时可以返回可执行的最大CPU数。
通过runtime.GOMAXPROCS(1)
可以设置使用的核心数。
GMP是Go运行时调度层面的实现,包括G、M、P和Sched
G
代表Goroutine协程,存储执行栈信息、状态和任务函数等,G的数量是无限制的,受内存限制。一个G的栈大小2-4K,一般简单的机器也可以启动数十万个goroutine。G退出的时候还可以把G清理的内存放在P或者全局闲置列表gFree复用。
M
代表Machine,Go对操作系统线程的一个封装,相当于操作系统内核线程,现在CPU执行代码必须有线程,通过系统调用clone创建线程。M在绑定一个有效的P以后进入一个调度循环。
循环的机制大概就是从P的本地队列以及全局队列中获得G,切换到G的执行栈上并运行G的函数,调用goexit清理工作回到M。M不会保留G的状态,这是G可以跨M调度的基础。
M的数量有限,默认一千,可以设置最大数。
P
虚拟处理器Processor,M执行G所需要的资源和上下文,只有将M和P绑定,才能让P的runq中的G真正运行起来,P的数量决定了系统最大可并行的G的数量,P的数量受到本机CPU的限制,也可以通过变量修改,默认CPU核心数。
Sched
调度器结构,维护有存储的M和G的全局队列,以及调度器的一些状态信息。
但是这个模型的缺点是不支持抢占式调度。如果一个G有死循环逻辑,G将永久占用分配的M和P,这个M和P的其他G会被饿死。
因此后来引进了基于写作的抢占式调度和基于信号的抢占式调度。
# go——内存分配机制
Go可以自主管理内存,包括如内存池、预分配等机制,不会每次分配内存都进行系统调用。
设计思想
内存管理主要组件有mspan、mcache .mcentral和mheap
分配对象
分配的流程
每个函数都拥有自己的内存空间存放入参、局部变量和返回地址等,会由编译器在栈上分配,每个函数都有一个栈帧,函数运行结束后销毁。但是有时候需要在函数结束后仍然使用这个栈,就需要将其在堆上分配,从栈上逃逸到堆就叫内存逃逸。
在栈分配的内存由系统申请和释放,不会带来额外的开销。堆上分配到内存则需要进行GC,会带来一定的性能开销。
编译器会自行决定变量是否被外部引用从而是否逃逸
如果外部函数没有引用,就优先在栈里;
如果外部函数引用了,必定在堆上分配
如果栈放不下,就在堆里。
编译器可以自动回收释放栈分配的内存,堆是程序共享的内存,需要使用GC进行回收。
分为两个半独立的组件:赋值器(就是用户态代码)和回收器(负责垃圾回收的代码)
三色标记算法
所有对象初始化都是白色对象
从根对象开始扫描所有根对象,标记为灰色。(根对象是当前所有全局变量和goroutine中的栈中对象)
从灰色对象中取出一个对象,看他有没有引用其他对象,没有的话标记为黑色,放入黑色队列。如果引用了,被引用的对象也被标记为灰色,同时该引用对象标记为黑色,放入黑色对象。也就是所有灰色都会被放在黑色里面。
如果灰色队列不空的话,继续上面遍历扫描,最终内存中只有黑色和白色对象,将所有的白色对象清理了。
Go
Go的GC主要分为几个步骤
# Go的反射
反射指的是程序运行期间对程序本身进行访问和修改的能力,能够在运行时动态获取变量各种信息。
go反射通过接口的类型信息实现:当向接口变量赋予一个实体类型的时候,接口会储存实体的类型信息和值信息,在运行时可以利用反射:
Type是reflect的一个interface、Value是reflect的一个struct,Value实现了Type接口
提供有两个主要方法
通过反射获取接口变量的类型信息
如果是结构体只能通过获取结构体类型变量的基础类型Type获取其字段信息,如果不是指针,直接reflect.TypeOf()
获取concreate type;如果是指针,需要Type.Elem()
获取基础类型
这里有两种分类
reflect.Type.Name()
例如type Person struct
,Person就是Type,struct是kind
通过反射获取接口变量的值信息Value
就在写代码的时候使用println和fmt.Println
,然后发现输出的顺序不一致,我还以为是没有输出,然后才发现好像是函数的问题
这两个函数的本质其实都不一样,前者会重定向输出到stderr
标准错误,字体颜色都不一样,vscode中是红色。这个一般用于程序启动和调试,Go内部使用。不接受数组和结构体参数,对于不同类型的组合参数比如用逗号连接好多变量,输出的是地址
后者会重定向输出到stdout
标准输出,是正常颜色字体,vscode中是蓝色。是有返回值的,返回写入的字节数和遇到的任何写入错误
如果一个实参由String() string和Error() string
方法,fmt和log库打印的时候会调用这两个方法,内置的print和println会忽略这些参数。
www.topgoer.com
new和make区别
new是初始化一个指向类型的指针,new是内建函数,参数是一个类型不是一个值,返回值是指向这个类型0值的新分配的指针
make作用是初始化切片slice、集合map或者通道chan并返回其引用,也是内建函数。第一个参数是类型,第二个是长度,返回值是一个类型。
make只用来创建slice、map和chan并返回类型是T的初始化实例。
Printf()、Sprintf()和Fprintf()
都是格式化输出字符串,比如使用占位符%d之类的。
数组和切片的区别
数组是具有固定长度且有零个或多个相同类型元素的序列,数组长度是数组类型的一部分,[3]int和[4]int是两个类型。
数组需要指定大小,否则会根据初始化自动确定,不可变,是值传递,作为参数直接传入的时候,是拷贝一份作为形参,而不是直接引用地址。
切片是储存相同类型元素的一个可变长度的序列,是一个轻量级数据结构,包含有指针、长度和容量。切片可以使用数组或者make初始化,可以不指定长度
go终端常用命令
go的协程
协程和线程都可以实现程序的并发。
go中可以直接使用go关键词创建一个协程go runtine,在协程之间可以使用channel进行通信。
go的引用类型包含有哪些
数组切片、字典、通道和接口interface{}
go的指针运算*
和c语言差不多,&和*
,前者对变量取地址,后者是取得指针所指向地址的数据。
go语言的main函数
不能有参数
没有返回值
main函数所在的包必须是main包
main函数可以使用flag包获取命令行传入的参数
go语言同步锁
一个goroutine获得Mutex之后,其他的goroutine只能等待,除非释放锁
RWMutex在读锁占用的时候,会禁止写操作,可以读
RWMutex在写锁占用的时候,会禁止任何goroutine读或者写操作,相当于全部独占
go里面channel的特性
触发异常的场景
go语言的select机制
用来处理异步io的,最大的一条限制就是每个case里面必须是一个io操作,在语言级别支持select关键字
进程线程和协程的区别
进程是资源的分配和调度的一个独立单元,线程是CPU调度的基本单元。
一个进程里面可以有多个线程,进程结束后所有线程都会结束,线程的结束不会影响进程以及其他线程。
线程共享进程的资源,包括寄存器、堆栈和上下文,一个进程至少有一个线程。
进程的切换资源消耗很大,效率低,县城切线的资源消耗一般效率一般,协程是小号最小的效率最高。协程的本质是当前进程在不同函数代码中切换执行,是一个用户层面的,可以是单线程多协程也可以是多线程多协程。
map实现有序排序
map本身是无序的,想有序的话只能通过对key处理。比如使用slice对key进行有序存放,然后通过slice的索引在map里面取值。
channel应用场景和原理
# Go channel的使用场景,用法总结
应用场景:
分为三种状态:nil、active和closed
其底层原理就是在内存中实例化了一个hchan结构体,返回一个chan指针。
通道使用互斥锁mutex,让goroutine以FIFO的方式进入到结构体中,需要收发消息的时候,锁住结构体,缓存中的数据按照链表顺序存放,按照链表顺序读取
如果通道满了,继续发送消息会阻塞goroutine,原理是通过Go运行时的scheduler完成调度。
go如何实现面向对象
面向对象的三个特点:封装、继承和多态
继承
type Person struct{
name string
}
func (p *Person) setName(name string){
p.name=name
}
func (p *Person) getName(){
fmt.Println(p.name)
}
func main(){
p:=Person("name")
p.setName("ddd")
p.getName()
}
//封装
//多态
type Dove struct{}
type Eagle struct{}
func (d *Dove) fly(){}
func (e *Eagle) fly(){}
func main(){
var b Birds//抽象接口
b=&Dove
b.fly()
b=&Eagle
b.fly()
}