首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Go面经

Go面经

作者头像
公众号guangcity
发布2022-12-02 20:35:42
3180
发布2022-12-02 20:35:42
举报
文章被收录于专栏:光城(guangcity)光城(guangcity)

前段时间一直在找工作,将自己的Go总结分享出来,期待大家交流~

目录

  • 1.time.Now()返回的是什么时间?这样做的决定因素是什么?
  • 2.选择三个常见 golang 组件
  • 3.noCopy
  • 4.sync.Pool源码分析
  • 5.请列举两种不同的观察 GC 占用 CPU 程度的方法,观察方法无需绝对精确,但需要可实际运用于 profiling 和 optimization。
  • 6.JSON 标准库对 nil slice 和 空 slice 的处理是一致的吗
  • 7.string与[]byte转换
  • 8.模拟gdb调试
  • 9.INT_MAX值
  • 10.rune与byte
  • 11.字符串拼接
  • 12.类型比较
  • 13.格式化输出
  • 15.切片扩容
  • 16.有无缓冲区chan
  • 17.协程泄漏
  • 18.Go 可以限制运行时操作系统线程的数量吗? 常见的goroutine操作函数有哪些?
  • 19.defer底层原理
  • 20.make和new
  • 21.panic和recover
  • 22.map
  • 23.context
  • 25.接口
  • 26.reflect反射
  • 27.http
  • 28.主协程如何优雅等待子协程
  • 29.Go中map如何顺序读取
  • 30.变量地址
  • 31.goto
  • 32.type alias
  • 33.读取大文件

1.time.Now()返回的是什么时间?这样做的决定因素是什么?

Wall clock(time) VS Monotonic clock(time)

Wall clock(time)就是我们一般意义上的时间,就像墙上钟所指示的时间。

Monotonic clock(time)字面意思是单调时间,实际上它指的是从某个点开始后(比如系统启动以后)流逝的时间,jiffies一定是单调递增的!

而特别要强调的是计算两个时间点的差值一定要用Monotonic clock(time),因为Wall clock(time)是可以被修改的,比如计算机时间被回拨(比如校准或者人工回拨等情况),或者闰秒( leap second),会导致两个wall clock(time)可能出现负数。(因为操作系统或者上层应用不一定完全支持闰秒,出现闰秒后系统时间会在后续某个点会调整为正确值,就有可能出现时钟回拨(当然也不是一定,比如ntpdate就有可能出现时钟回拨,但是ntpd就不会))

2.选择三个常见 golang 组件

channel, goroutine, [], map, sync.Map等,

列举它们常见的严重伤害性能的 anti-pattern?

select 很多 channel 的时候,并发较高时会有性能问题。因为 select 本质是按 chan 地址排序,顺序加锁。lock1->lock2->lock3->lock4 活跃 goroutine 数量较多时,会导致全局的延迟不可控。比如 99 分位惨不忍睹。slice append 的时候可能触发扩容,初始 cap 不合适会有大量 growSlice。map hash collision,会导致 overflow bucket 很长,但这种基本不太可能,hash seed 每次启动都是随机的。此外,map 中 key value 超过 128 字节时,会被转为 indirectkey 和 indirectvalue,会对 GC 的扫描阶段造成压力,如果 k v 多,则扫描的 stw 就会很长。sync.Map 有啥性能问题?写多读少的时候?

3.noCopy

noCopy原理:

type noCopy struct {
}

func (*noCopy) Lock()   {}
func (*noCopy) Unlock() {}

4.sync.Pool源码分析

https://www.cnblogs.com/qcrao-2018/p/12736031.html https://xiaorui.cc/archives/5878

  • Get 的整个过程就非常清晰了:

首先,调用 p.pin() 函数将当前的 goroutine 和 P 绑定,禁止被抢占,返回当前 P 对应的 poolLocal,以及 pid。然后直接取 l.private,赋值给 x,并置 l.private 为 nil。判断 x 是否为空,若为空,则尝试从 l.shared 的头部 pop 一个对象出来,同时赋值给 x。如果 x 仍然为空,则调用 getSlow 尝试从其他 P 的 shared 双端队列尾部“偷”一个对象出来。Pool 的相关操作做完了,调用 runtime_procUnpin() 解除非抢占。最后如果还是没有取到缓存的对象,那就直接调用预先设置好的 New 函数,创建一个出来。

  • Put 的逻辑也很清晰:

先绑定 g 和 P,然后尝试将 x 赋值给 private 字段。如果失败,就调用 pushHead 方法尝试将其放入 shared 字段所维护的双端队列中

5.请列举两种不同的观察 GC 占用 CPU 程度的方法,观察方法无需绝对精确,但需要可实际运用于 profiling 和 optimization。

  • pprof 的 cpuprofile 火焰图 启动时传入环境变量 Debug=gctrace
  • go tool trace,可以看到 p 绑定的 g 实际的 GC 动作和相应时长,以及阻塞时间

6.JSON 标准库对 nil slice 和 空 slice 的处理是一致的吗

package main

import (
    "encoding/json"
    "log"
)

type S struct {
    A []string
}

func main() {
    data := &S{}
    data2 := &S{A: []string{}}
    buf, err := json.Marshal(&data)
    log.Println(string(buf), err)
    buf2, err2 := json.Marshal(&data2)
    log.Println(string(buf2), err2)
}

输出:

2009/11/10 23:00:00 {"A":null} <nil>
2009/11/10 23:00:00 {"A":[]} <nil>

7.string与[]byte转换

结论:string与[]byte转换过程中的Data都会发生拷贝,例如:

// 可能的结果:
// 1374390312984
// 1374390312984 1374390230744
// 说明,string to slice,一定发生内容copy
func checkToSlice() {
 str := fmt.Sprint(rand.Int63())
 strHeader := (*reflect.StringHeader)(unsafe.Pointer(&str))
 fmt.Println(strHeader.Data)

 toSlice := []byte(str)
 sliceHeader := (*reflect.SliceHeader)(unsafe.Pointer(&toSlice))
 fmt.Println(strHeader.Data, sliceHeader.Data)
}

// 可能的结果:
// 1374390152904
// 1374390152904 1374390152872
// 说明,slice to string,一定发生内容copy
func checkToString() {
 slice := []byte(fmt.Sprint(rand.Int63()))
 sliceHeader := (*reflect.SliceHeader)(unsafe.Pointer(&slice))
 fmt.Println(sliceHeader.Data)

 toStr := string(slice)
 strHeader := (*reflect.StringHeader)(unsafe.Pointer(&toStr))
 fmt.Println(sliceHeader.Data, strHeader.Data)
}

像如下代码会进行优化。

func Equal(a, b []byte) bool {
 // Neither cmd/compile nor gccgo allocates for these string conversions.
 return string(a) == string(b)
}
  • []byte转string没问题 提供的String方法就是将[]byte转换为string类型,这里为了避免内存拷贝的问题,使用了强制转换来避免内存拷贝:
func (b *Builder) String() string {
 return *(*string)(unsafe.Pointer(&b.buf))
}
  • string转[]byte有问题 这种直接转换有风险,string只有raw ptr和length,没有capacity的概念,当reslice或者append等操作时,需要capacity,此时会panic。
func NoAllocBytes(buf string) []byte {
 return *(*[]byte)(unsafe.Pointer(&buf))
}

正确做法:

func NoAllocBytes(buf string) []byte {
 x := (*reflect.StringHeader)(unsafe.Pointer(&buf))
 h := reflect.SliceHeader{x.Data, x.Len, x.Len}
 return *(*[]byte)(unsafe.Pointer(&h))
}

8.模拟gdb调试

https://github.com/go-delve/delve

brew install dlv/yum install dlv

9.INT_MAX值

const maxInt = int(^uint(0) >> 1)

10.rune与byte

rune = uint32 
byte = uint8

11.字符串拼接

    • 使用+操作符进行拼接时,会对字符串进行遍历,计算并开辟一个新的空间来存储原来的两个字符串。
  • fmt 由于采用了接口参数,必须要用反射获取值,因此有性能损耗。
  • strings.Builder 用WriteString()进行拼接,内部实现是指针+切片,同时String()返回拼接后的字符串,它是直接把[]byte转换为string,从而避免变量拷贝。strings.Builder使用问题,内部有addr(是否指向自身,copyCheck检查),buf([]byte数组,存放内容,写入时append方式)。

注意点:两个strings.Builder禁止在写入之后拷贝,写入之前可以拷贝,如下例子,主要原因是写入会触发copyCheck检查。

var b1 strings.Builder
b1.WriteString("ABC")
b2 := b1
// b2.WriteString("DEF") 失败 在写操作会进行copyCheck -> 如果内存空 会操作addr,不空则会判断地址是否一致
fmt.Println(b1, b2)
var b3, b4 strings.Builder
b4 = b3 // 一开始都为空 所以可以进行copy
b3.WriteString("123")
b4.WriteString("456")
fmt.Println(b3.String(), b4.String())
  • bytes.Buffer bytes.Buffer是一个一个缓冲byte类型的缓冲器,这个缓冲器里存放着都是byte,包含一个offset变量,控制尾部写入数据,从头部读取数据,
  • strings.Join 基于strings.Builder构建,支持分隔符拼接,内部调用时会先Grow, 以提前进行容量分配可以减少内存分配,很高效。strings.Join ≈ strings.Builder > bytes.Buffer > "+" > fmt.Sprintf

12.类型比较

Go 结构体有时候并不能直接比较,当其基本类型包含:slice、map、function 时,是不能比较的。若强行比较,就会导致出现例子中的直接报错的情况。reflect.DeepEqual来判断不可比较类型,如:map、slice、func等

type Value struct {
    Name   string
    Gender *string
}

func main() {
    v1 := Value{Name: "1", Gender: new(string)}
    v2 := Value{Name: "1", Gender: new(string)}
    if v1 == v2 {
        fmt.Println("yes")
        return
    }

    fmt.Println("no")
}

输出:no,因为地址不一样

13.格式化输出

%v、%+v、%#v %v输出结构体各成员的值;%+v输出结构体各成员的名称和值;%#v输出结构体名称和结构体各成员的名称和值

%v的方式 = &{test 123456} %+v的方式 = &{name:test id:123456} %#v的方式 = &main.student{name:"test", id:123456}

14.Go垃圾回收机制

垃圾回收机制是Go一大特(nan)色(dian)。Go1.3采用标记清除法, Go1.5采用三色标记法,Go1.8采用三色标记法+混合写屏障。

标记清除法(mark and sweep):

分为两个阶段:标记和清除 第一步,暂停程序业务逻辑, 分类出可达和不可达的对象 第二步, 开始标记,程序找出它所有可达的对象,并做上标记 第三步, 标记完了之后,然后开始清除未标记的对象. 第四步, 停止暂停,让程序继续跑。然后循环重复这个过程,直到process程序生命周期结束

缺点:

  • STW,stop the world;让程序暂停,程序出现卡顿
  • 标记需要扫描整个heap;
  • 清除数据会产生heap碎片。早期优化,缩短STW时间,原来STW包括:启动STW、标记、清除、停止STW,优化为启动STW、标记、停止STW,清除操作与程序并行,而不被包含在STW过程中。

三色标记法:

第一步 , 每次新创建的对象,默认的颜色都是标记为“白色”, 第二步, 每次GC回收开始, 会从根节点开始遍历所有对象,把遍历到的对象从白色集合放入"灰色"集合。第三步, 遍历灰色集合,将灰色对象引用的对象从白色集合放入灰色集合,之后将此灰色对象放入黑色集合。第四步, 重复第三步, 直到灰色中无任何对象。第五步: 回收所有的白色标记表的对象. 也就是回收垃圾。我们将全部的白色对象进行删除回收,剩下的就是全部依赖的黑色对象。

不加STW,会遇到对象丢失问题:

  • 条件1: 一个白色对象被黑色对象引用(白色被挂在黑色下)
  • 条件2: 灰色对象与它之间的可达关系的白色对象遭到破坏(灰色同时丢了该白色) 如果当以上两个条件同时满足时,就会出现对象丢失现象!

屏障机制:

强弱三色不变式:
1.强三色不变式
强制不允许黑色对象引用白色对象,目的在于破坏条件1。
2.弱三色不变式
黑色对象允许引用白色对象,白色对象存在其他灰色对象引用,或者可达它的链路上存在灰色对象,目的在于破坏条件2。

因此,在三色标级中满足强三色不变式或弱三色不变式之一,即可保证对象不丢失。

1.插入屏障 (为了保证栈的速度,不在栈上使用) 
具体操作: 在A对象引用B对象的时候,B对象被标记为灰色。(将B挂在A下游,B必须被标记为灰色)
满足: 强三色不变式. (不存在黑色对象引用白色对象的情况了, 因为白色会强制变成灰色)
插入屏障不会在栈上操作,堆上处理没问题,但是如果栈不添加,当全部三色标记扫描之后,栈上有可能依然存在白色对象被引用的情况(黑色引用白色对象).  所以要对栈重新进行三色标记扫描, 但这次为了对象不丢失, 要对本次标记扫描启动STW暂停. 直到栈空间的三色标记结束.
所以插入屏障,最后会对栈上的所有对象进行一次三色标记法+STW保护。
2. 删除屏障
具体操作: 被删除的对象,如果自身为灰色或者白色,那么被标记为灰色。
满足: 弱三色不变式. (保护灰色对象到白色对象的路径不会断)
这种方式的回收精度低,一个对象即使被删除了最后一个指向它的指针也依旧可以活过这一轮,在下一轮GC中被清理掉。

Go V1.8的混合写屏障(hybrid write barrier)机制

插入写屏障和删除写屏障的短板:

  • 插入写屏障:结束时需要STW来重新扫描栈,标记栈上引用的白色对象的存活;- 删除写屏障:回收精度低,GC开始时STW扫描堆栈来记录初始快照,这个过程会保护开始时刻的所有存活对象。Go V1.8版本引入了混合写屏障机制(hybrid write barrier),避免了对栈re-scan的过程,极大的减少了STW的时间。结合了两者的优点。1)混合写屏障规则 1、GC开始将栈上的对象全部扫描并标记为黑色(之后不再进行第二次重复扫描,无需STW), 2、GC期间,任何在栈上创建的新对象,均为黑色。3、被删除的对象标记为灰色。4、被添加的对象标记为灰色。满足: 变形的弱三色不变式. 这里我们注意, 屏障技术是不在栈上应用的,因为要保证栈的运行效率。

15.切片扩容

  • 如果期望容量大于当前容量的两倍就会使用期望容量;
  • 如果当前切片的长度小于 1024 就会将容量翻倍;
  • 如果当前切片的长度大于 1024 就会每次增加 25% 的容量,直到新容量大于期望容量;

16.有无缓冲区chan

有缓冲的channel是异步的,而无缓冲channel是同步的。

17.协程泄漏

  • 缺少接收器,导致发送阻塞
  • 缺少发送器,导致接收阻塞
  • 死锁。多个协程由于竞争资源导致死锁。
  • WaitGroup Add()和Done()不相等,前者更大。

18.Go 可以限制运行时操作系统线程的数量吗?常见的goroutine操作函数有哪些?

runtime三大函数:runtime.Gosched()、runtime.Goexit()、runtime.GOMAXPROCS() 可以,使用runtime.GOMAXPROCS(num int)可以设置线程数目。该值默认为CPU逻辑核数,如果设的太大,会引起频繁的线程切换,降低性能。runtime.Gosched(),用于让出CPU时间片,让出当前goroutine的执行权限,调度器安排其它等待的任务运行,并在下次某个时候从该位置恢复执行。runtime.Goexit(),调用此函数会立即使当前的goroutine的运行终止(终止协程),而其它的goroutine并不会受此影响。runtime.Goexit在终止当前goroutine前会先执行此goroutine的还未执行的defer语句。请注意千万别在主函数调用runtime.Goexit,因为会引发panic。

19.defer底层原理

  • 后调用的 defer 函数会先执行:
  • 后调用的 defer 函数会被追加到 Goroutine _defer 链表的最前面;
  • 运行 runtime._defer 时是从前到后依次执行;defer nil -> panic defer运行时就会立刻将传递给延迟函数的参数保存起来 for loop defer need a func wrap channel
  • 同步 Channel — 不需要缓冲区,发送方会直接将数据交给(Handoff)接收方;
  • 异步 Channel — 基于环形缓存的传统生产者消费者模型;
  • chan struct{} 类型的异步 Channel — struct{} 类型不占用内存空间,不需要实现缓冲区和直接发送(Handoff)的语义;runtime.hchan包含缓冲区指针、元素个数、循环队列长度、发送操作处理到的位置、 接收操作处理到的位置、收发元素类型/大小、sendq 和 recvq 存储了当前 Channel 由于缓冲区空间不足而阻塞的 Goroutine 列 makechan
  • 如果当前 Channel 中不存在缓冲区,那么就只会为 runtime.hchan 分配一段内存空间;
  • 如果当前 Channel 中存储的类型不是指针类型,会为当前的 Channel 和底层的数组分配一块连续的内存空间;
  • 在默认情况下会单独为 runtime.hchan 和缓冲区分配内存;发送数据的过程中包含几个会触发 Goroutine 调度的时机:
  • 发送数据时发现 Channel 上存在等待接收数据的 Goroutine,立刻设置处理器的 runnext 属性,但是并不会立刻触发调度;
  • 发送数据时并没有找到接收方并且缓冲区已经满了,这时会将自己加入 Channel 的 sendq 队列并调用 runtime.goparkunlock 触发 Goroutine 的调度让出处理器的使用权;从 Channel 接收数据时,会触发 Goroutine 调度的两个时机:
  • 当 Channel 为空时;
  • 当缓冲区中不存在数据并且也不存在数据的发送者时;

chan panic触发时机:

  • 向已经关闭的channel写。
  • 关闭已经关闭的channel。

chan 阻塞触发时机:

无缓存channel:

  • 通道中无数据,但执行读通道。
  • 通道中无数据,向通道写数据,但无协程读取。

有缓存channel:

  • 通道的缓存无数据,但执行读通道。
  • 通道的缓存已经占满,向通道写数据,但无协程读。

20.make和new

make 和 new 关键字的实现原理,make 关键字的作用是创建切片、哈希表和 Channel 等内置的数据结构,而 new 的作用是为类型申请一片内存空间,并返回指向这片内存的指针。

21.panic和recover

panic 函数可以被连续多次调用,它们之间通过 link 可以组成链表,内部包含:argp 是指向 defer 调用时参数的指针;arg 是调用 panic 时传入的参数;link 指向了更早调用的 runtime._panic 结构;recovered 表示当前 runtime._panic 是否被 recover 恢复;aborted 表示当前的 panic 是否被强行终止;

  • 编译器会负责做转换关键字的工作;将 panic 和 recover 分别转换成 runtime.gopanic 和 runtime.gorecover;○ 将 defer 转换成 runtime.deferproc 函数;○ 在调用 defer 的函数末尾调用 runtime.deferreturn 函数;
  • 在运行过程中遇到 runtime.gopanic 方法时,会从 Goroutine 的链表依次取出 runtime._defer 结构体并执行;
  • 如果调用延迟执行函数时遇到了 runtime.gorecover 就会将 _panic.recovered 标记成 true 并返回 panic 的参数;在这次调用结束之后,runtime.gopanic 会从 runtime._defer 结构体中取出程序计数器 pc 和栈指针 sp 并调用 runtime.recovery 函数进行恢复程序;○ runtime.recovery 会根据传入的 pc 和 sp 跳转回 runtime.deferproc;○ 编译器自动生成的代码会发现 runtime.deferproc 的返回值不为 0,这时会跳回 runtime.deferreturn 并恢复到正常的执行流程;
  • 如果没有遇到 runtime.gorecover 就会依次遍历所有的 runtime._defer,并在最后调用 runtime.fatalpanic 中止程序、打印 panic 的参数并返回错误码 2;

22.map

  • 开放地址法 线性探测,装载因子=元素数量/数组长度

index := hash("author") % array.len

  • 拉链法 装载因子:=元素数量/桶数量 实现拉链法一般会使用数组加上链表,不过一些编程语言会在拉链法的哈希中引入红黑树以优化性能,拉链法会使用链表数组作为哈希底层的数据结构,我们可以将它看成可以扩展的二维数组 在一般情况下使用拉链法的哈希表装载因子都不会超过 1,当哈希表的装载因子较大时会触发哈希的扩容,创建更多的桶来存储哈希中的元素,保证性能不会出现严重的下降。如果有 1000 个桶的哈希表存储了 10000 个键值对,它的性能是保存 1000 个键值对的 1/10,但是仍然比在链表中直接读写好 1000 倍。1.创建map
  • 计算哈希占用的内存是否溢出或者超出能分配的最大值;
  • 调用 runtime.fastrand 获取一个随机的哈希种子;
  • 根据传入的 hint 计算出需要的最小需要的桶的数量;
  • 使用 runtime.makeBucketArray 创建用于保存桶的数组;2.扩容
  • 装载因子已经超过 6.5;
  • 哈希使用了太多溢出桶;根据触发的条件不同扩容的方式分成两种,如果这次扩容是溢出的桶太多导致的,那么这次扩容就是等量扩容 sameSizeGrow,sameSizeGrow 是一种特殊情况下发生的扩容,当我们持续向哈希中插入数据并将它们全部删除时,如果哈希表中的数据量没有超过阈值,就会不断积累溢出桶造成缓慢的内存泄漏4。runtime: limit the number of map overflow buckets 引入了 sameSizeGrow 通过复用已有的哈希扩容机制解决该问题,一旦哈希中出现了过多的溢出桶,它会创建新桶保存数据,垃圾回收会清理老的溢出桶并释放内存

哈希在存储元素过多时会触发扩容操作,每次都会将桶的数量翻倍,扩容过程不是原子的,而是通过 runtime.growWork 增量触发的,在扩容期间访问哈希表时会使用旧桶,向哈希表写入数据时会触发旧桶元素的分流。除了这种正常的扩容之外,为了解决大量写入、删除造成的内存泄漏问题,哈希引入了 sameSizeGrow 这一机制,在出现较多溢出桶时会整理哈希的内存减少空间的占用。

哈希表的每个桶都只能存储 8 个键值对,一旦当前哈希的某个桶超出 8 个,新的键值对就会存储到哈希的溢出桶中。随着键值对数量的增加,溢出桶的数量和哈希的装载因子也会逐渐升高,超过一定范围就会触发扩容,扩容会将桶的数量翻倍,元素再分配的过程也是在调用写操作时增量进行的,不会造成性能的瞬时巨大抖动。

23.context

1.Context接口

Deadline()
Done()
Err()
Value()
  • context.Background 是上下文的默认值,所有其他的上下文都应该从它衍生出来;
  • context.TODO 应该仅在不确定应该使用哪种上下文时使用;context.Background与context.TODO实现都一致,都是emptyCtx(int列名类型) 2.WithCancel从context.Context衍生出一个新的子上下文并返回用于取消该上下文的函数 WithCancel返回一个bool,CancelFunc。CancelFunc为匿名函数,内部调用cancelCtx的cancel方法。cancelCtx结构里面包含了:
type cancelCtx struct {
 Context // 传入的context

 mu       sync.Mutex            // 保护接下来的三个字段
 done     chan struct{}         // created lazily, closed by first cancel call
 children map[canceler]struct{} // set to nil by the first cancel call
 err      error                 // set to non-nil by the first cancel call
}

一旦我们执行返回的取消函数,当前上下文以及它的子上下文都会被取消,所有的 Goroutine 都会同步收到这一取消信号 3.WithValue WithValue从父上下文中创建一个子上下文,返回valueCtx

type valueCtx struct {
 Context
 key, val interface{}
}

重写Value方法,从父上下文中获取 val 4.WithDeadline WithDeadline创建可以被取消的计时器上下文timerCtx

type timerCtx struct {
 cancelCtx
 timer *time.Timer // Under cancelCtx.mu.

 deadline time.Time
}

context.WithDeadline 在创建 context.timerCtx 的过程中判断了父上下文的截止日期与当前日期,并通过 time.AfterFunc 创建定时器,当时间超过了截止日期后会调用 context.timerCtx.cancel 同步取消信号。context.timerCtx 内部不仅通过嵌入 context.cancelCtx 结构体继承了相关的变量和方法,还通过持有的定时器 timer 和截止时间 deadline 实现了定时取消的功能 5.WithDeadline WithTimeout会调用WithDeadline

func WithTimeout(parent Context, timeout time.Duration) (Context, CancelFunc) {
 return WithDeadline(parent, time.Now().Add(timeout))
}

24.数组与切片的区别 数组是值类型,定长数组, StringHeader

type StringHeader struct {
 Data uintptr
 Len  int
}

切片是引用类型,动态指向数组的指针,不定长指向数组array, SliceHeader

type SliceHeader struct {
 Data uintptr
 Len  int
 Cap  int
}

25.接口

Go 语言根据接口类型是否包含一组方法将接口类型分成了两类:

  • 使用 runtime.iface 结构体表示包含方法的接口
type iface struct { // 16 字节
 tab  *itab
 data unsafe.Pointer
}
  • 使用 runtime.eface 结构体表示不包含任何方法的 interface{} 类型;
type eface struct { // 16 字节
 _type *_type
 data  unsafe.Pointer
}

接口类型interface{}

type emptyInterface struct {
 typ  *rtype
 word unsafe.Pointer
}

26.reflect反射

  • reflect.TypeOf能获取类型信息 reflect.TypeOf 的实现原理其实并不复杂,它只是将一个 interface{} 变量转换成了内部的 reflect.emptyInterface 表示,然后从中获取相应的类型信息。
  • reflect.ValueOf能获取到数据的运行时信息 reflect.Type为接口,reflect.Value为结构体 私有成员,对外暴露公共方法。我们通过 reflect.TypeOf、reflect.ValueOf 可以将一个普通的变量转换成反射包中提供的 reflect.Type 和 reflect.Value,随后就可以使用反射包中的方法对它们进行复杂的操作。用于获取接口值 reflect.Value 的函数 reflect.ValueOf 实现也非常简单,在该函数中我们先调用了 reflect.escapes 保证当前值逃逸到堆上,然后通过 reflect.unpackEface 从接口中获取 reflect.Value 结构体。

原则1: 接口到任意对象,入参均是interface{},出参为任意对象。

原则2: 反射对象获取interface{}变量。

可以通过reflect.Value.Interface完成这项工作。

v := reflect.ValueOf(1)
v.Interface().(int)

上述两个原则转换:

  • 从接口值到反射对象:○ 从基本类型到接口类型的类型转换;○ 从接口类型到反射对象的转换;
  • 从反射对象到接口值:○ 反射对象转换成接口类型;○ 通过显式类型转换变成原始类型;
  • 调用 reflect.ValueOf 获取变量指针;
  • 调用 reflect.Value.Elem 获取指针指向的变量;
  • 调用 reflect.Value.SetInt 更新变量的值:
func main() {
 i := 1
 v := reflect.ValueOf(&i)
 v.Elem().SetInt(10)
 fmt.Println(i)
}

27.http

按照HTTP/1.1的规范,Go http包的http server和client的实现默认将所有连接视为长连接,无论这些连接上的初始请求是否带有Connection: keep-alive。1.http.Client 如果要关闭keep-alive, 需要通过http.Transport设置:

tr := &http.Transport{ DisableKeepAlives: true, } c := &http.Client{ Transport: tr, }

// 发送请求 rsp, err:= c.Do(req) defer rsp.Body.Close() // 读取数据 ioutil.ReadAll(rsp.Body)

2.http.Server server端完全不支持keep-alive连接方式:

s := http.Server{ Addr: ":8080", Handler: xxx, } s.SetKeepAliveEnabled(false) s.ListenAndServe()

闲置连接超时控制:

s := http.Server{ Addr: ":8080", Handler: xxx, IdleTimeout: 5 * time.Second, } s.ListenAndServe()

客户端和服务端响应的次数

  • 长连接:可以多次。
  • 短链接:一次。传输数据的方式
  • 长连接:连接--数据传输--保持连接
  • 短连接:连接--数据传输--关闭连接 长连接和短链接的优缺点
  • 长连接 优点 省去较多的TCP建立和关闭的操作,从而节约时间。■ 性能比较好。(因为客户端一直和服务端保持联系) ○ 缺点 当客户端越来越多的时候,会将服务器压垮。■ 连接管理难。■ 安全性差。(因为会一直保持着连接,可能会有些无良的客户端,随意发送数据等)
  • 短链接 优点 服务管理简单。存在的连接都是有效连接 ○ 缺点 请求频繁,在TCP的建立和关闭操作上浪费时间

28.主协程如何优雅等待子协程

  • channel进行同步
  • sync.WaitGroup同步

29.Go中map如何顺序读取

由于map底层实现与 slice不同, map底层使用hash表实现,插入数据位置是随机的, 所以遍历过程中新插入的数据不能保证遍历。主要是对 key 排序,那么我们便可将 map 的 key 全部拿出来,放到一个数组中,然后对这个数组排序后对有序数组遍历,再间接取 map 里的值就行了。

30.变量地址

package main

const cl  = 100

var bl    = 123

func main()  {
    println(&bl,bl)
    println(&cl,cl) // 不可取cl地址
}

常量 常量不同于变量的在运行期分配内存,常量通常会被编译器在预处理阶段直接展开,作为指令数据使用,

31.goto

goto不可以跳转到其他函数或内层代码

package main

func main()  {

    for i:=0;i<10 ;i++  {
    loop:
        println(i)
    }
    goto loop
}

32.type alias

type aType  int // defintion
type bType = int // alias
var i int
var i1 aType = i // 报错 需要强转
var i2 bType = i // 可以直接赋值

33.读取大文件

f, err := os.Open(xxx)
bufio.NewReader(f) // 默认 4k
buf := bufio.NewReaderSize(f, xxx)
for {
 line, prefix, err := buf.ReadLine()
 // dosomething
 if err != nil {
  if err == io.EOF {
   return nil
  }
  return err
 }
}
本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2022-08-10,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 光城 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1.time.Now()返回的是什么时间?这样做的决定因素是什么?
  • 2.选择三个常见 golang 组件
  • 3.noCopy
  • 4.sync.Pool源码分析
  • 5.请列举两种不同的观察 GC 占用 CPU 程度的方法,观察方法无需绝对精确,但需要可实际运用于 profiling 和 optimization。
  • 6.JSON 标准库对 nil slice 和 空 slice 的处理是一致的吗
  • 7.string与[]byte转换
  • 8.模拟gdb调试
  • 9.INT_MAX值
  • 10.rune与byte
  • 11.字符串拼接
  • 12.类型比较
  • 13.格式化输出
  • 15.切片扩容
  • 16.有无缓冲区chan
  • 17.协程泄漏
  • 18.Go 可以限制运行时操作系统线程的数量吗?常见的goroutine操作函数有哪些?
  • 19.defer底层原理
  • 20.make和new
  • 21.panic和recover
  • 22.map
  • 23.context
  • 25.接口
  • 26.reflect反射
  • 27.http
  • 28.主协程如何优雅等待子协程
  • 29.Go中map如何顺序读取
  • 30.变量地址
  • 31.goto
  • 32.type alias
  • 33.读取大文件
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档