【切片】
1.内部实现
数据结构中有三个字段
地址指针 | 指向底层数组 |
---|---|
长度 | 切片能够访问的元素个数 |
容量 | 切片容许增长到的元素个数 |
所以切片在函数间传递成本非常低,指针4个字节,长度和容量是整型各4个字节,一共12个字节,无论切片地址指针指向的底层数组有多大都不影响,但是在函数中对切片底层数组修改外部会有感知
2.初始化
初始化方法 | 说明 |
---|---|
slice := make([]int,5) | 创建一个整型切片,长度容量都是5,底层数组为00000 |
slice := make([]int,3,5) | 创建整型切片,长度为3,容量为5,底层数组为000,后两个元素不能访问 |
slice := []int{1,2,3} | 切片字面量声明切片,长度容量都是3 |
slice := []int{2:1} | 索引声明切片,第3个元素初始为1,长度容量都是3,底层001 |
var slice []int | 创建nil切片,表示一个不存在的切片,地址指针为nil, 函数要求返回一个切片但是发生异常时可以使用 |
slice := make([]int,0) slice := []int{} | 声明空切片,底层数组包含0个元素,没有分配任何存储空间, 可以用来表示空集合 |
3.使用切片
从一个切片中创建的新切片,两者共享同一个底层数组,如果修改共享部分,另一个切片有感知
slice := []int{1,2,3}
newSlice := slice[1:2]
4.切片作为参数传递
1.把切片作为一个参数时,传进去的是复制的切片结构体,所以初始和进入接口的 切片结构体地址不一样,但是结构体里面的三个元素是复制过去的,所以数组地址和容量,长度都是一样的
2.添加元素,导致切片扩展,数组重新划分了新地址存储数值,所以append操作后数组地址和进入接口后数组地址不一样了,但是切片结构体地址不会因为append操作变化,append只会影响数组的地址
3.调用接口不会影响外部的切片结构体的信息,接口内部操作只会改变复制的结构体里面的数组信息,容量和长度
4.接口内部如果改变了数组某个位置的数值,会影响外部切片,具体看操作的时候数组地址是否和外部一致,图中在共享一个地址的时候修改i[0]=9,所以外部也会受影响,如果在append之后操作,在外部数组不会受影响,因为不共享数组了
变量 | 底层数组 | 长度 | 容量 |
---|---|---|---|
slice | 1 2 3 | 3 | 3 |
newSlice | 2 3 | 1 | 2 |
newSlice从slice索引1开始创建,只能共享从这个索引开始的数组部分,只复制了slice的一个元素,所以长度是1,但是slice从索引1开始后面有两个元素,所以容量是2 所以newSlice[0]和slice[1]实际是同一个内存地址上的值,修改一个切片另一个也会被改
newSlice[0]=-1 //slice[1]也变成-1
newSlice=append(newSlice,-2) //在执行之前 newSlice -1,长度1 容量2,
slice 1 -1 3 长度3 容量3
执行之后,newSlice -1 -2 长度容量都是2
把slice 第三个元素改了 1 -1 -2
append函数能够将容量合并到切片长度里,没有在长度里的容量是不能访问的,slice[i],i所在位置不再长度范围内运行会出错
slice := make([]int,1,3) //0 _ _
print(slice[1]) //会出错,长度为1 只能访问slice[0]
append函数可以实现切片的动态增长,当容量长度一致,append再往切片中添加新元素时,会重新分配一块更大的内存给切片底层数组,这个时候底层数组地址是改变的
所以当两个切片共享同一底层数组时,其中一个容量长度相等,再添加新元素,两个切片就会分离
slice := []int{1,2,3}
newSlice := slice[1,2]
slice := append(slice,4)
slice append之后会和newSlice分离,newSlice底层数组不变,slice底层数组迁移到一个新地址,并且长度为4,容量为6
5.数组与切片
package main
import "fmt"
func main() {
var p [100]int
var m interface{} = [...]int{99: 0}
fmt.Println(p == m)
}
返回true
解析:首先 [...]int{99: 0} 是数组还是切片 ?
切片和数据的主要区别是:切片容量可变,而数组长度不可变。所以数组在声明时是需要确定其长度的。切片不需要。在实际开发时我们区分中括号里是否有设置长度就可以区分二者了,如果设置了长度就一定是数组,否则一定是切片。
举例说明:
var a = [0]int{} 长度为0的数组
var c = [6]int{} 长度为6的数组
var b = [...]int{1,3,7} 长度为3的数组 其中的三个点是表示数组长度以声明时实际赋予的元素个数为依据
所以说 [...]int{99: 0} 是数组
然后接口和非接口类型可以比较判断吗?
1.interface类型变量与非interface类型变量判等时,首先要求非interface类型实现了接口,否则编译不通过(本题接口方法集为空,我们认为所有类型都实现了该接口)
2.满足上一条的前提下,interface类型变量的动态类型、值均与非interface类型变量相同时,两个变量判等结果为true,因为[...]int{99: 0} 是数组 而且长度是100,值都为0,所以答案为true
6.数组比较
package main
import "fmt"
func main() {
type pos [2]int
a := pos{4, 5}
b := pos{4, 5}
fmt.Println(a == b)
}
返回 true
数组是值类型,长度相等,每个元素也相等,数组就相等
7.append 参数类型
func main() {
var nums1 []interface{}
nums2 := []int{1, 3, 4}
nums3 := append(nums1, nums2...) //报错 cannot use nums2 (variable of type []int) as type []interface{} in argument to append
//nums3 := append(nums1, nums2[0],nums2[1],nums2[2]) //ok int实现了interface{} nums3长度为3
//nums3 := append(nums1, nums2) // ok nums2作为一个元素 append到nums1上 []int实现了interface{} nums3长度为1
fmt.Println(len(nums3))
}
【map】
1.内部实现
散列表,映射是一个存储键值对的无序集合
2.初始化
初始化 | 说明 |
---|---|
mp := map[int]int{} | 创建空映射,可以直接mp[1]=1 |
var mp map[int]int | 声明映射创建了一个nil映射,不能直接通过mp[1]=2添加元素 |
mp := make(map[int]int) | 创建一个映射 |
mp := map[int]int{1:1,2:2} | 创建映射,用两个键值对初始化映射 |
3.判断键是否存在
value,exist := mp[1]
if exist{
print(value)
}
4.如果获取一个不存在的key的值,不会出错,返回类型默认值
msg := make(map[int64]int64)
msg[1]=1
msg[2]=2
fmt.Println(msg[1],msg[2],msg[3])//return 1 2 0
5.nil map 和 空map的关系
nil map和empty map的关系,就像nil slice和empty slice一样,两者都是空对象,未存储任何数据,但前者不指向底层数据结构,后者指向底层数据结构,只不过指向的底层对象是空对象
直接读取nil map:m[“a”] 并不会报错,会返回默认类型的空值
【defer用法】
1.defer执行顺序先进后出,panic语句在defer语句后面执行
package main
import (
"fmt"
)
func main() {
defer_call()
}
func defer_call() {
defer func() { fmt.Println("打印前") }()
defer func() { fmt.Println("打印中") }()
defer func() { fmt.Println("打印后") }()
panic("触发异常")
}
结果为
打印后
打印中
打印前
panic: 触发异常
解释
defer语句执行顺序是先进后出,类似于入栈,“打印前”先入栈所以最后一个打印,panic语句会导致函数退出执行,但函数退出执行前要先执行defer语句,所以先打印defer 中的内容,再panic
panic发生的时候,协程内是否会把panic信息在栈中层层传递,这个文章讲的很好 blog.csdn.net/pengpengzho…
(1.每一个协程会维护自己的延迟函数链表
(2.panic对应函数底层逻辑,先执行协程内延迟函数链表中延迟函数,然后打印panic信息,然后执行exit结束整个进程,所以panic信息不会传递给父方法,其他协程因为进程终止也结束,并且不会调用延迟函数
(3.defer是在执行到的时候会放入协程的延迟函数链表,如果panic发生了,或者return发生了,后面的defer加不到链表中,不会被执行
(4.recover 的时候panic会被新的panic信息终止,返回最新的panic
注意上面说的是在一个协程里面,不是在一个方法里面,如果是在一个协程里面,多个递归调用方法panic也是可以往上传递的
func a() {
panic("panic")
}
func b() {
a()
}
func c() {
defer func() {
e := recover()
if e != nil {
fmt.Println("c", e)
}
}()
b()
}
func main() {
c()
time.Sleep(10 * time.Second)
}
返回的是
c panic
,因为3个方法是在同一个协程里面
如果func b是这样
func b() {
go a()
}
那就会捕捉不到panic直接崩了,因为b新开了一个协程执行a,不再同一个协程里面了,c也就捕捉不到直接panic了 但是如果function a是这样
func a() {
defer func() {
e := recover()
if e != nil {
fmt.Println("a", e)
}
}()
panic("panic")
}
返回是a panic
,因为a自己在方法里面捕捉了
defer里面如果发生了panic,如果后面还没有执行的defer有recover可以捕捉到这个panic也不会崩,比如下面这样
func a() {
defer func() {
panic("a panic")
}()
}
func b() {
a()
}
func c() {
defer func() {
e := recover()
if e != nil {
fmt.Println("c", e)
}
}()
b()
}
func main() {
c()
time.Sleep(10 * time.Second)
}
在a里面的panic可以被前面入栈的defer recover捕捉到,返回是c a panic
但是如果recover是在后面的子方法a里面,前面的方法c的panic是捕捉不到的
func a() {
defer func() {
e := recover()
if e != nil {
fmt.Println("a", e)
} else {
fmt.Println("a no panic")
}
}()
}
func b() {
a()
}
func c() {
b()
panic("c panic")
}
func main() {
c()
}
result
因为每一个方法在结束前会调用自己的defer方法,a在调用的时候c还没有panic所以会打印
a no panic
,a返回之后c继续往下执行panic没有recover方法所以崩
2.defer语句内部参数为函数调用
package main
func calc(index string, a, b int) int {
ret := a + b
fmt.Println(index, a, b, ret)
return ret
}
func main() {
a := 1
b := 2
defer calc("1", a, calc("10", a, b))
a = 0
defer calc("2", a, calc("20", a, b))
b = 1
}
结果
10 1 2 3
20 0 2 2
2 0 2 2
1 1 3 4
解析
运行到第一个defer语句时,要对其参数“入栈”,遇到参数还需要计算先计算出来再“入栈”(跟变量类型有关?静态,运行时之类的),所以先计算calc("10", a, b),遇到第二个defer语句在计算其中的calc("20", a, b),函数返回时在执行两个defer语句
3.先执行return后面的语句再执行defer语句
package main
func main() {
println(DeferFunc1(1))
println(DeferFunc2(1))
println(DeferFunc3(1))
println(DeferFunc4(1))
}
func plus1(i int )int{
i++
return i
}
func DeferFunc1(i int) (t int) {
t = i
defer func() {
t += 3
}()
return t
}
func DeferFunc2(i int) int {
t := i
defer func() {
t += 3
}()
return t
}
func DeferFunc3(i int) (t int) {
defer func() {
t += i
}()
return 2
}
func DeferFunc4(i int) (t int) {
t=i
defer func() {
t += i
}()
return plus1(t)
}
结果
4
1
3
3
解析
DeferFunc1声明了一个变量t作为返回值,想象现在有一块内存里面就是t的值,目前初始为0,
t = i//第一步t被赋值,t所在的内存上被写上值1
defer func() {
t += 3//第三步t被再次赋值,内存上的值被改写为4,然后返回,返回值是内存上的值即4
}()
return t//第二步return,发现有defer语句,继而去执行defer语句
DeferFunc2函数中没有给返回值声明一个显示变量,当执行return t时,将t的值复制到了返回值的内存中,注意这里t的地址和返回值的地址不一样,只是将t的值复制给了返回值地址,所以defer中对t操作只会改变t所在内存处的值,对返回值没影响
DeferFunc3 中return 2 将2复制给了返回值地址,也就是t所在的地址,此时t=2,defer语句中对t操作,就是对返回值地址里的值操作,所以返回3
DeferFunc4中会先执行plus1函数,t变成2,再执行defer函数
return 底层做了什么很重要,要搞清楚,才能理解上面的题目
golang语言中的return语句不是原子操作,分为(1)返回值赋值和(2)RET指令两步。而defer语句执行在赋值之后,RET之前
返回参数的地址也很重要比如DeferFunc2,返回没有参数名,函数会隐式的分配一块内存给返回值,这个地址跟t不同,所以 return t中 ,第一步返回值赋值,是把t的值复制给了隐式分配的那个地址,defer后续对t的操作都跟他无关,所以最后输出值是1 这个时候t是等于3的;DeferFunc3 返回值显式指明是t,return 2 第一步就是给t赋值为2,执行defer t加1 等于3 ,最后返回 3;DeferFunc4 第一步返回值赋值,plus方法执行之后t=2 赋值给t,defer 里面+1,等于3最后返回
func e1() {
var err error
defer fmt.Println(err)
err = errors.New("defer err1")
}
func e2() {
var err error
defer func() {
fmt.Println(err)
}()
err = errors.New("defer err2")
}
func e3() {
var err error
defer func(err error) {
fmt.Println(err)
}(err)
err = errors.New("defer err3")
}
func main() {
e1()
e2()
e3()
}
返回
<nil>
defer err2
<nil>
解析:如果明确传参的,应该是复制 参数的值到方法中去,把参数放到方法的栈上了,后续这个参数在外面的修改就不会影响到方法内部,如果没有传参,闭包里面会记录变量的地址,地址上的值发生变化,内部有感知
4.只有最后一个panic会被recover捕获
package main
import (
"fmt"
)
func main() {
defer func() {
if err:=recover();err!=nil{
fmt.Println(err)
}else {
fmt.Println("fatal")
}
}()
defer func() {
panic("defer panic")
}()
panic("panic")
}
结果
defer panic
解析
执行第一个panic("panic")后,执行defer语句,里面又是一个panic,然后执行上面的defer,里面的recover语句只会捕获最后一个panic,也就是defer panic,所以打印defer panic,另外程序也不会崩,因为panic被recover捕获了,只会打印出错误信息 也就是如果有多个panic,后面的panic会覆盖前面的,最后只会保留最新的panic信息
【foreach】
1.foreach创建副本
package main
func main() {
pase_student()
}
type student struct {
Name string
Age int
}
func pase_student() {
m := make(map[string]*student)
stus := []student{
{Name: "zhou", Age: 24},
{Name: "li", Age: 23},
{Name: "wang", Age: 22},
}
// 错误写法
for i, stu := range stus {
println(&stu)
println(&stus[i])
m[stu.Name] = &stu
}
for k,v:=range m{
println(k,"=>",v.Name,v.Age)
}
// 正确
for i:=0;i<len(stus);i++ {
m[stus[i].Name] = &stus[i]
}
for k,v:=range m{
println(k,"=>",v.Name,v.Age)
}
}
结果
0xc000004060
0xc000048000
0xc000004060
0xc000048018
0xc000004060
0xc000048030
zhou => wang 22
li => wang 22
wang => wang 22
zhou => zhou 24
li => li 23
wang => wang 22
解析 foreach中创建变量stu(有自己的内存地址,不同于stus地址)会复制stus的元素值到自己所在位置,可以看到结果中stu的地址和对应的stus[i]地址并不相同,最后map中键值是正确的,但是值(是个指针)指向的并不是真正的存储学生信息的地址,然后foreach创建的一个temp变量的地址,也就是stu的地址,这个地址里面存储着foreach最后一次执行复制的stus里面的内容,所以打印出来m所有键的value是同一个值 正确就是直接按下标找到真正地址复制给map
【go闭包,执行随机性】
1.并发函数内部变量
package main
import (
"runtime"
"sync"
"fmt"
)
func main() {
runtime.GOMAXPROCS(1)
wg := sync.WaitGroup{}
wg.Add(20)
for i := 0; i < 10; i++ {
go func() {
fmt.Println("A: ", i)
wg.Done()
}()
}
for i := 0; i < 10; i++ {
go func(i int) {
fmt.Println("B: ", i)
wg.Done()
}(i)
}
wg.Wait()
}
结果
B: 9
A: 10
A: 10
A: 10
A: 10
A: 10
A: 10
A: 10
A: 10
A: 10
A: 10
B: 0
B: 1
B: 2
B: 3
B: 4
B: 5
B: 6
B: 7
B: 8
解析
第一个循环中并发函数内部的变量i是外部循环变量,循环很快就会遍历完,所以等并发函数要执行的时候,i已经到了循环终止时的10,所以打印的都是10
第二个循环中,i是入参变量,对当前的外部循环变量i值拷贝,所以即使并发函数执行的时候外部i已经是10,但是并发函数内部i是当时复制下来的数,并发函数之间执行顺序不确定,所以打印的0-9数顺序也不确定
附加
答案
ans1不一定是 15, ans2 不一定是 15.
ans1因为闭包问题,ans2因为并发加操作不是原子性的
【go组合无继承】
package main
import "fmt"
type People struct{}
func (p *People) ShowA() {
fmt.Println("people showA")
p.ShowB()
}
func (p *People) ShowB() {
fmt.Println("people showB")
}
type Teacher struct {
People
}
func (t *Teacher) ShowB() {
fmt.Println("teacher showB")
}
//func (t *Teacher) ShowA() {
// fmt.Println("teacher showA")
//}
func main() {
t := Teacher{}
t.ShowA()
}
结果
people showA
people showB
解析 go组合模式 被组合的类型People所包含的方法虽然升级成了外部类型Teacher这个组合类型的方法(一定要是匿名字段),但它们的方法(ShowA())调用时接受者并没有发生变化。 但是如果将注释代码去掉注释结果就变成了
teacher showA
因为函数相同时,外部类型的方法会隐藏内部类型的方法
【select语句】
1.select随机性
会触发异常吗?
func main() {
runtime.GOMAXPROCS(1)
int_chan := make(chan int, 1)
string_chan := make(chan string, 1)
int_chan <- 1
string_chan <- "hello"
select {
case value := <-int_chan:
fmt.Println(value)
case value := <-string_chan:
panic(value)
}
}
结果
不确定
select可以在多个chan间等待执行。有三点原则:
select 中只要有一个case能return,则立刻执行
当如果同一时间有多个case均能return则伪随机方式抽取任意一个执行。
如果没有一个case能return则可以执行”default”块
【make用法】
1.make初始化值
func main() {
s := make([]int, 5)
s = append(s, 1, 2, 3)
fmt.Println(s)
}
结果
0 0 0 0 0 1 2 3
解析
make会根据创建元素的类型将元素初始化为零值,这里创建5个int类型的值,所以初始化5个0,后面追加1,2,3,
make 类型是切片时还需要第二个参数,类型个数,甚至还可以有第三个 类型是阻塞通道或者map则不需要 make(chan int) make(map[string]int)
make([]int,10) 一个int数组,里面10个元素值为0
make([]int,0,10) 一个数组,参数2是len目前元素个数,参数3是cap数组容量,所以元素个数现在为0 ,但能装10个
make([]int,5,10) 已经有5个元素值为0,能装10个
【map】
1.map线程不安全
package main
import (
"sync"
)
type UserAges struct {
ages map[string]int
sync.Mutex
}
func (ua *UserAges) Add(name string, age int) {
ua.Lock()
defer ua.Unlock()
ua.ages[name] = age
}
func (ua *UserAges) Get(name string) int {
if age, ok := ua.ages[name]; ok {
return age
}
return -1
}
func main(){
ua := new (UserAges)
ua.ages = make(map[string]int)
go func() {
for{
ua.Add("jack",22)
}
}()
go func() {
for{
ua.Get("jack")
}
}()
c := make(chan int)
c<-1
}
结果
fatal error: concurrent map read and map write
解析 写的时候虽然加了锁,但是在写的时候可能会同时读,map在并发情况下时线程不安全的,要在多线程中使用map,应该用sync.Map,修改程序,对比两个程序,观察sync.Map用法,他不需要初始化,有load store 两个重要用法
package main
import (
"sync"
)
type UserAges struct {
ages sync.Map
}
func (ua *UserAges) Add(name string, age int) {
ua.ages.Store(name,age)
println("add")
}
func (ua *UserAges) Get(name string) int {
println("get")
if age, ok := ua.ages.Load(name); ok {
return age.(int)
}
println("get")
return -1
}
func main(){
ua := new (UserAges)
go func() {
for{
ua.Add("jack",22)
}
}()
go func() {
for{
ua.Get("jack")
}
}()
c := make(chan int)
c<-1
}
【channel】
1.无缓冲channel
package main
import "sync"
type threadSafeSet struct{
sync.RWMutex
s []int
}
func (set *threadSafeSet) Iter() <-chan interface{} {
wg := sync.WaitGroup{}
wg.Add(1)
ch := make(chan interface{})
//ch := make(chan interface{},len(set.s))
go func() {
set.RLock()
for elem := range set.s {
ch <- elem
}
wg.Done()
close(ch)
set.RUnlock()
}()
wg.Wait()
return ch
}
func main(){
set := new (threadSafeSet)
set.s = make([]int,5)
set.Iter()
}
结果
fatal error: all goroutines are asleep - deadlock!
解析
ch是无缓冲的通道,第一个元素放进通道的时候就会阻塞,除非从通道中读数据,否则会一直阻塞,所以会报错,如果用注释的那一行,则不会报错,正好通道装满返回
2.从一个nil的通道读数据会怎样?
阻塞
【go鸭子类型】
1.鸭子类型
package main
import (
"fmt"
)
type People interface {
Speak(string) string
}
type Stduent struct{}
func (stu *Stduent) Speak(think string) (talk string) {
if think == "bitch" {
talk = "You are a good boy"
} else {
talk = "hi"
}
return
}
//func (stu Stduent) Speak(think string) (talk string) {
// if think == "bitch" {
// talk = "You are a good boy"
// } else {
// talk = "hi"
// }
// return
//}
func main() {
//var peo People = &Stduent{}
var peo People = Stduent{}
think := "bitch"
fmt.Println(peo.Speak(think))
}
结果
编译不通过,因为student类没有实现people下的方法,是*student 指针类型实现的,所以换成注释的那一行就可以编译通过了,或者让student类型实现speak函数,如上面注释额函数
解析
golang中没有class的概念,而是通过interface类型转换支持动态类型语言中常见的鸭子类型,来达到运行时多态的效果,注意一定是people 是interface如果换成struct就不是这么回事了
思考:如果speak实现换成注释的函数,main函数中会编译通过吗
解析
不管是student还是&student都会通过,因为如果是&student,函数会在执行的时候加上*解地址符
接收者 | 传值 | 可行 |
---|---|---|
指针 | 值 | 不可行 |
值 | 指针 | 可行 |
【interface】
1.interface不代表nil
package main
import (
"fmt"
)
type People interface {
Show()
}
type Student struct{}
func (stu *Student) Show() {
}
func live() People {
var stu *Student
return stu
}
func main() {
if live() == nil {
fmt.Println("AAAAAAA")
} else {
fmt.Println("BBBBBBB")
}
}
结果
BBBBBBB
解析
live()函数返回的是interface类型数据,go中的接口分为两种,一种是空的接口
var in interface{}
另一种如题目:
type People interface {
Show()
}
他们的底层结构如下
type eface struct { //空接口
_type *_type //类型信息
data unsafe.Pointer //指向数据的指针(go语言中特殊的指针类型unsafe.Pointer类似于c语言中的void*)
}
type iface struct { //带有方法的接口
tab *itab //存储type信息还有结构实现方法的集合
data unsafe.Pointer //指向数据的指针(go语言中特殊的指针类型unsafe.Pointer类似于c语言中的void*)
}
type _type struct {
size uintptr //类型大小
ptrdata uintptr //前缀持有所有指针的内存大小
hash uint32 //数据hash值
tflag tflag
align uint8 //对齐
fieldalign uint8 //嵌入结构体时的对齐
kind uint8 //kind 有些枚举值kind等于0是无效的
alg *typeAlg //函数指针数组,类型实现的所有方法
gcdata *byte
str nameOff
ptrToThis typeOff
}
type itab struct {
inter *interfacetype //接口类型
_type *_type //结构类型
link *itab
bad int32
inhash int32
fun [1]uintptr //可变大小 方法集合
}
可以看出iface比eface 中间多了一层itab结构。 itab 存储_type信息和[]fun方法集,从上面的结构我们就可得出,因为data指向了nil 并不代表interface 是nil
2.type只能在interface类型上使用
package main
func main() {
i := GetValue()
switch i.(type) {
case int:
println("int")
case string:
println("string")
case interface{}:
println("interface")
default:
println("unknown")
}
}
func GetValue() int {
return 1
}
结果 编译不通过,报错
cannot type switch on non-interface value i (type int)
3.interface内部结构
package main
import (
"fmt"
)
func Foo(x interface{}) {
if x == nil {
fmt.Println("empty interface")
return
}
fmt.Println("non-empty interface")
}
func main() {
var x *int = nil
Foo(x)
}
结果
non-empty interface
解析
理解的不是很透彻,还是跟interface内部结构有关,内部data可能为nil但是这个interface是不为空的
【函数】
1.返回值要么都不命名,要么都命名
package main
func main() {
funcMui(1,2)
}
func funcMui(x,y int)(sum int,error){
return x+y,nil
}
结果 编译不通过报错
syntax error: mixed named and unnamed function parameters
解析
在函数有多个返回值时,只要有一个返回值有指定命名,其他的也必须有命名 如果返回值有有多个返回值必须加上括号 如果只有一个返回值并且有命名也需要加上括号
2.闭包延迟求值
package main
func test() []func() {
var funs []func()
for i:=0;i<2 ;i++ {
funs = append(funs, func() {
println(&i,i)
})
}
return funs
}
func main(){
funs:=test()
for _,f:=range funs{
f()
}
}
结果
0xc00000a048 2
0xc00000a048 2
解析 类似于foreach中的复制,这里的i始终在同一块内存上最后打印的时候是同一个地址,且上面的值是最后跳出循环的2
func Increase() func() int {
n := 0
return func() int {
n++
return n
}
}
func main() {
in := Increase()
fmt.Println(in()) // 1
fmt.Println(in()) // 2
}
解析:Increase()
返回值是一个闭包函数,该闭包函数访问了外部变量 n,那变量 n 将会一直存在,直到 in
被销毁。很显然,变量 n 占用的内存不能随着函数 Increase()
的退出而回收,因此将会逃逸到堆上
3.闭包引用相同变量
package main
func test(x int) (func(),func()) {
return func() {
println(x)
x+=10
}, func() {
println(x)
}
}
func main() {
a,b:=test(100)
a()
b()
}
结果
100
110
解析
x作为一个方法内的参数,地址被存进了栈内,两个返回方法里面x指向的是同一个地址,执行a的时候,把这个地址上的值+10,变成110,b方法执行的时候,打印这个值就是110了
【append】
1.append第一个参数是切片类型
package main
import "fmt"
func main() {
list := new([]int)
list = append(list, 1)
fmt.Println(list)
}
结果 编译不通过
first argument to append must be slice; have *[]int
解析
new返回的是指针类型,append参数1是切片类型
2.append第二个参数
package main
import "fmt"
func main() {
s1 := []int{1, 2, 3}
s2 := []int{4, 5}
s1 = append(s1, s2)
fmt.Println(s1)
}
结果
编译不通过
cannot use s2 (type []int) as type int in append
解析 append函数形式
func append(slice []Type, elems ...Type) []Type
可以一个元素一个元素的给 slice = append(slice, elem1, elem2) 也可以切片后面加... slice = append(slice, anotherSlice...)
【结构体】
1.结构体比较
package main
import "fmt"
func main() {
sn1 := struct {
age int
name string
}{age: 11, name: "qq"}
sn2 := struct {
age int
name string
}{age: 11, name: "qq"}
if sn1 == sn2 {
fmt.Println("sn1 == sn2")
}
sm1 := struct {
age int
m map[string]string
}{age: 11, m: map[string]string{"a": "1"}}
sm2 := struct {
age int
m map[string]string
}{age: 11, m: map[string]string{"a": "1"}}
if sm1 == sm2 {
fmt.Println("sm1 == sm2")
}
//if reflect.DeepEqual(sm1,sm2) {
// fmt.Println("sm1 == sm2")
//}
}
结果 编译出错
invalid operation: sm1 == sm2 (struct containing map[string]string cannot be compared)
解析
结构体相同的条件有
内部属性个数,顺序相同,注意顺序不同也不相等
内部属性可以比较,像map,slice就不能比较,不能使用“==”比较,如果比较可以使用注释代码实现map比较
【nil】
1.nil不能做string的空值使用
package main
import (
"fmt"
)
func GetValue(m map[int]string, id int) (string, bool) {
if _, exist := m[id]; exist {
return "存在数据", true
}
return nil, false
}
func main() {
intmap:=map[int]string{
1:"a",
2:"bb",
3:"ccc",
}
v,err:=GetValue(intmap,3)
fmt.Println(v,err)
}
结果 编译不通过
cannot use nil as type string in return argument
nil 可以用作 interface、function、pointer、map、slice 和 channel 的“空值”,string的空值是“”
2.nil赋值于某个对象
var x = nil
结果
编译出错,nil 必须给一个明确类型的值赋值 比如 var x *int=nil
【iota】
package main
import (
"fmt"
)
const (
x = iota
y
z = "zz"
k
p = iota
)
func main() {
fmt.Println(x,y,z,k,p)
}
结果
0 1 zz zz 4
解析
iota是特殊常量,可以认为是一个可以被编译器修改的常量。
iota 在 const关键字出现时将被重置为 0(const 内部的第一行之前),const 中每新增一行常量声明将使 iota 计数一次(iota 可理解为 const 语句块中的行索引)。
【const】
1.常量定义格式
const identifier [type] = value
常量中的数据类型只可以是布尔型、数字型(整数型、浮点型和复数)和字符串型
显式类型定义: const b string = "abc"
隐式类型定义: const b = "abc"
还可以多重赋值
const a, b, c = 1, false, "str" //多重赋值
根据上面所说,下面正确的是ABD C error变量不能做常量
A.
const Pi float64 = 3.14159265358979323846
const zero= 0.0
B.
const (
size int64= 1024
eof = -1
)
C.
const (
ERR_ELEM_EXISTerror = errors.New("element already exists")
ERR_ELEM_NT_EXISTerror = errors.New("element not exists")
)
D.
const u, vfloat32 = 0, 3
const a,b, c = 3, 4, "foo"
2.常量取地址
package main
const cl = 100
var bl = 123
func main() {
println(&bl,bl)
println(&cl,cl)
}
结果 编译不通过
cannot take the address of cl
解析
常量不同于变量的在运行期分配内存,常量通常会被编译器在预处理阶段直接展开,作为指令数据使用
【:=】
package main
var(
size :=1024
max_size = size*2
)
func main() {
println(size,max_size)
}
结果 编译不通过
syntax error: unexpected :=, expecting =
解析 简短模式定义变量只能函数内部使用,去掉:就可以编译成功了 【goto】
package main
func main() {
for i:=0;i<10 ;i++ {
loop:
println(i)
}
goto loop
}
结果 编译不通过
goto loop jumps into block starting at .\main.go:5:22
解析 goto不能跳转到其他函数或者内层代码,这里跳进了循环里面,编译不通过
【type alias】
1.definition&alias
package main
import "fmt"
func main() {
type MyInt1 int
type MyInt2 = int
var i int =9
var i1 MyInt1 = i
var i2 MyInt2 = i
fmt.Println(i1,i2)
}
结果 编译不通过
cannot use i (type int) as type MyInt1 in assignment
解析
**Go 1.9 新特性 Type Alias **
基于一个类型创建一个新类型,称之为defintion;基于一个类型创建一个别名,称之为alias。 MyInt1为称之为defintion,虽然底层类型为int类型,但是不能直接赋值,需要强转; MyInt2称之为alias,可以直接赋值
2.定义方法
package main
import "fmt"
type User struct {
}
type MyUser1 User
type MyUser2 = User
func (i MyUser1) m1(){
fmt.Println("MyUser1.m1")
}
func (i User) m2(){
fmt.Println("User.m2")
}
func main() {
var i1 MyUser1
var i2 MyUser2
i1.m1()
i2.m2()
}
结果
MyUser1.m1
User.m2
解析
MyUser1是definition,和user不同的另一种类型,他有自己的方法m1可以用,MyUser2是int的别名,可以使用user的方法M2,但是i1.m2()就不行
3.重复结构
package main
import "fmt"
type T1 struct {
}
func (t T1) m1(){
fmt.Println("T1.m1")
}
type T2 = T1
type MyStruct struct {
T1
T2
}
func main() {
my:=MyStruct{}
my.m1()
}
结果 编译不通过
ambiguous selector my.m1
解析 只要有重复的方法、字段,就会有这种提示,因为不知道该选择哪个 【全局变量】
定义一个包内全局字符串变量,下面语法正确的是()
A. var str string
B. str := ""
C. str = ""
D. var str = ""
答案
AD
解析
短声明操作符不能作用于函数外部
C会报错
syntax error: non-declaration statement outside function body
【类型转换】
1.
package main
func main(){
type MyInt int
var i int = 1
var j MyInt = MyInt(i)
println(j)
}
这样是对的,(MyInt)i会编译报错 想用i.(MyInt)也可以,但是i必须是interface{}类型,所以修改
func main(){
type MyInt int
var i interface{} = 1
var j MyInt = i.(MyInt)
println(j)
}
var b bool
b=1
b=bool(1)
var int i
if i //wrong
两种赋值都是错的,在golang中int 和bool类型不能相互转换,但是在C++中是可以的
【初始化】
局部变量的几种初始化方式
var i int = 1
var i = 1
i := 1
注意i=1是不对的
【引用类型】
变量存储的是一个地址,这个地址存储最终的值。内存通常在堆上分配。通过GC回收。
指针、slice、map、chan、interface等都是引用类型。
【new和make】
相同点
都是分配内存,初始化类型
不同点
作用对象不同,new 都可以 make只能用于内置引用类型slice,map,channel(interface{}也不行)
返回值不同
new返回的是初始化零值之后变量的地址,make返回的是变量的引用,当将该变量当做函数参数时,函数内部修改会影响外部变量的值
函数不同
func make(t Type, size ...IntegerType) Type
func new(Type) *Type
new只需要一个类型即可,make 类型是切片时还需要第二个参数,类型个数,甚至还可以有第三个
make([]int,10) 一个int数组,里面10个元素值为0
make([]int,0,10) 一个数组,参数2是len目前元素个数,参数3是cap数组容量,所以元素个数现在为0 ,但能装10个
make([]int,5,10) 已经有5个元素值为0,能装10个
类型是阻塞通道或者map则不需要 make(chan int) make(map[string]int)
非阻塞通道则需要说明通道能装多少元素 make(chan int,2)
【++操作】
在Go中 ++ 和 -- 只能作为语句而非表达式
a := 1
a ++ // 注意:不能写成 ++ a 或 -- a 必须放在右边使用
b := a++ // 此处为错误的用法,不能写在一行,要单独作为语句使用
【sync.RWMutex和sync.Mutex】
Mutex互斥锁,Lock()加锁,Unlock()解锁,使用Lock()加锁后,便不能再次对其进行加锁,直到利用Unlock()解锁对其解锁后,才能再次加锁.适用于读写不确定场景,即读写次数没有明显的区别,并且只允许只有一个读或者写的场景,所以该锁叶叫做全局锁
RWMutex读写锁,该锁可以加多个读锁或者一个写锁,其经常用于读次数远远多于写次数的场景
如果在添加写锁之前已经有其他的读锁和写锁,则lock就会阻塞直到该锁可用
读锁,当有写锁时,无法加载读锁,当只有读锁或者没有锁时,可以加载读锁,读锁可以加载多个,所以适用于"读多写少"的场景
【map】
// 先声明map
var m1 map[string]string
// 再使用make函数创建一个非nil的map,nil map不能赋值
m1 = make(map[string]string)
// 最后给已声明的map赋值
m1["a"] = "aa"
m1["b"] = "bb"
// 直接创建
m2 := make(map[string]string)
// 然后赋值
m2["a"] = "aa"
m2["b"] = "bb"
// 初始化 + 赋值一体化
m3 := map[string]string{
"a": "aa",
"b": "bb",
}
// ==========================================
// 查找键值是否存在
if v, ok := m1["a"]; ok {
fmt.Println(v)
} else {
fmt.Println("Key Not Found")
}
// 遍历map
for k, v := range m1 {
fmt.Println(k, v)
}
1.判断channel是否断开
因为close的channel不会阻塞,并返回类型的nil值,会导致死循环
package main
import (
"fmt"
)
func main() {
c := make(chan int, 10)
c <- 1
c <- 2
c <- 3
close(c)
for {
fmt.Println(<-c)
}
}
判断短chan是否关闭
package main
import (
"fmt"
)
func main() {
c := make(chan int, 10)
c <- 1
c <- 2
c <- 3
close(c)
for {
i, ok := <-c
if !ok {
fmt.Println("channel closed!")
break
}
fmt.Println(i)
}
}
1.关闭通道close
ch := make(chan int)
close(ch)
ch<-9
上面会panic,往一个关闭的通道里面写数据会panic
ch := make(chan int)
ch<-9
close(ch)
for{
i := <-ch
print(i)
}
从一个关闭的通道读数据,会先读取已经发送到通道的值,然后会不断得到对应类型的零值,这里就会得到无数的0(打印9000000000...)
1.for range 能够判断channel是否关闭,通道关闭会停止阻塞,跳出循环
func main() {
c := make(chan int)
go func() {
for i := 0; i < 10; i = i + 1 {
c <- i
}
close(c)
}()
for i := range c {
fmt.Println(i)
}
fmt.Println("Finished")
}
会依次打印出来0-9,然后打印“Finished”,因为gorountine中关闭了通道,for range得知通道关闭就不再阻塞,程序往下进行 如果去掉close,for循环会一直阻塞,等到数据到来,“Finished“永远执行不到
for range 会把关闭后的通道里面的值全都拿出来之后才会跳出循环,并不会直接跳出来
【goroutine退出机制】
goroutine设计的退出机制是由goroutine自己退出,不能在外部强制结束一个正在执行的goroutine(只有一种情况正在运行的goroutine会因为其他goroutine的结束被终止,就是main函数退出或程序停止执行)
尽管标准库在遇到管道/函数等无法被序列化的内容时会发生错误,但因为本题中 d 和 e 均为小写未导出变量,因此不会发生序列化错误。
golang中大多数数据类型都可以转化为有效的JSON文本,除了channel、complex、函数等。
在golang指针中可进行隐式转换,对指针取值,对所指对象进行序列化。
【进制】
package main
import (
"fmt"
)
func main() {
fmt.Println(09)
}
结果 会报错 invalid digit '9' in octal literal
octal literal 是8进制的意思,8进制中只有0-7 所以9会报错,改成07 都不会报错
【new make区别】
new | make | |
---|---|---|
作用对象 | 任意类型 | slice,map,channel类型 |
返回 | 指向内存地址的指针 | 引用类型本身 |
内存操作 | 分配内存,没有初始化 | 分配,初始化内存 |
逃逸分析
定义
编译器根据代码的特征和生命周期,自动的把变量分配到堆或者是栈上面
时机
在编译阶段,由编译器执行
对于go1.6以上版本,如果出现【并发map读写】程序会直接以fatal error崩溃,即使同routine内有recover()也不能恢复