diff --git a/golang/go-Interview/GOALNG_INTERVIEW_COLLECTION.md b/golang/go-Interview/GOALNG_INTERVIEW_COLLECTION.md index 39e0d8de..8b1d3709 100644 --- a/golang/go-Interview/GOALNG_INTERVIEW_COLLECTION.md +++ b/golang/go-Interview/GOALNG_INTERVIEW_COLLECTION.md @@ -1,9 +1,3 @@ -- -- https://zhuanlan.zhihu.com/p/519979757?utm_medium=social&utm_oi=1128258938146394112&utm_psn=1536909217579855872&utm_source=qq -- https://m.php.cn/be/go/465769.html - - - ## 零、go与其他语言 ### 0、什么是[面向对象](https://so.csdn.net/so/search?q=面向对象&spm=1001.2101.3001.7020) @@ -190,7 +184,7 @@ func main() { 在日常工作中,基本了解这些概念就可以了。若是面试,可以针对三大特性:“封装、继承、多态” 和 五大原则 “单一职责原则(SRP)、开放封闭原则(OCP)、里氏替换原则(LSP)、依赖倒置原则(DIP)、接口隔离原则(ISP)” 进行深入理解和说明。 -### 2、go语言和python的区别: +### 4、go语言和python的区别: 1、范例 @@ -226,105 +220,90 @@ Python的语法使用缩进来指示代码块。Go的语法基于打开和关闭 为了获得相同的功能,Golang代码通常需要编写比Python代码更多的字符。 +### 5、go 与 node.js +深入对比Node.js和Golang 到底谁才是NO.1 : https://zhuanlan.zhihu.com/p/421352168 -## **一、基础部分** - -### **1、golang 中 make 和 new 的区别?(基本必问)** - -**共同点:** 给变量分配内存 - -**不同点:** - -1)作用变量类型不同,new给string,int和数组分配内存,make给切片,map,channel分配内存; +从 Node 到 Go:一个粗略的比较 : https://zhuanlan.zhihu.com/p/29847628 -2)返回类型不一样,new返回指向变量的指针,make返回变量本身; +## **一、基础部分** -3)new 分配的空间被清零。make 分配空间后,会进行初始化; +### 0、为什么选择golang -4) 字节的面试官还说了另外一个区别,就是分配的位置,在堆上还是在栈上? +**0、高性能-协程** +golang 源码级别支持协程,实现简单;对比进程和线程,协程占用资源少,能够简洁高效地处理高并发问题。 -只要代码逻辑允许,编译器总是倾向于把变量分配在栈上,比分配在堆上更高效。编译器倾向于让变量不逃逸。(逃逸分析是指当函数局部变量的生命周期超过函数栈帧的生命周期时,编译器把该局部变量由栈分配改为堆分配,即变量从栈上逃逸到堆上)。 +**1、学习曲线容易-****代码极简** -下面两个函数,返回值都是在堆上动态分配的int型变量的地址,编译器进行了逃逸分析。 +Go语言语法简单,包含了类C语法。因为Go语言容易学习,所以一个普通的大学生花几个星期就能写出来可以上手的、高性能的应用。在国内大家都追求快,这也是为什么国内Go流行的原因之一。 +Go 语言的语法特性简直是太简单了,简单到你几乎玩不出什么花招,直来直去的,学习曲线很低,上手非常快。 -```go -// go:noinline -func newInt1() *int{ - var a int - return &a -} +**2、效率:快速的编译时间,开发效率和运行效率高** -// go:noinline -func newInt2() *int{ - return new(int) -} +开发过程中相较于 Java 和 C++呆滞的编译速度,Go 的快速编译时间是一个主要的效率优势。Go拥有接近C的运行效率和接近PHP的开发效率。 -func main(){ - println(*newInt1()) -} -``` +C 语言的理念是信任程序员,保持语言的小巧,不屏蔽底层且底层友好,关注语言的执行效率和性能。而 Python 的姿态是用尽量少的代码完成尽量多的事。于是我能够感觉到,Go 语言想要把 C 和 Python 统一起来,这是多棒的一件事啊。 -​ newInt1()函数如果被分配在栈上,在函数返回后,栈帧被销毁,返回的变量a的地址会变成悬挂指针,对该地址所有读写都是不合法的,会造成程序逻辑错误或崩溃。 +**3、出身名门、血统纯正** -​ new()与堆分配无必然联系,代码如下: +之所以说Go出身名门,从Go语言的创造者就可见端倪,Go语言绝对血统纯正。其次Go语言出自Google公司,Google在业界的知名度和实力自然不用多说。Google公司聚集了一批牛人,在各种编程语言称雄争霸的局面下推出新的编程语言,自然有它的战略考虑。而且从Go语言的发展态势来看,Google对它这个新的宠儿还是很看重的,Go自然有一个良好的发展前途。 -```go -// go:noinline -func New() int{ - p := new(int) - return *p -} -``` +**4、自由高效:组合的思想、无侵入式的接口** -这个函数的new()进行栈分配,因为变量的生命周期没有超过函数栈帧的生命周期。 +Go语言可以说是开发效率和运行效率二者的完美融合,天生的并发编程支持。Go语言支持当前所有的编程范式,包括过程式编程、面向对象编程、面向接口编程、函数式编程。程序员们可以各取所需、自由组合、想怎么玩就怎么玩。 -把逻辑上没有逃逸的变量分配到堆上不会造成错误,只是效率低一些,但是把逻辑上逃逸了的变量分配到栈上就会造成悬挂指针等问题,因此编译器只有在能够确定变量没有逃逸的情况下才会把变量分配到栈上,在能够确定变量已经逃逸或者无法确定是否逃逸的情况,都要按照已经逃逸处理。 +**5、强大的标准库-****生态** +背靠谷歌,生态丰富,轻松 go get 获取各种高质量轮子。用户可以专注于业务逻辑,避免重复造轮子。 +这包括互联网应用、系统编程和网络编程。Go里面的标准库基本上已经是非常稳定了,特别是我这里提到的三个,网络层、系统层的库非常实用。Go 语言的 lib 库麻雀虽小五脏俱全。Go 语言的 lib 库中基本上有绝大多数常用的库,虽然有些库还不是很好,但我觉得不是问题,因为我相信在未来的发展中会把这些问题解决掉。 +**6、部署方便:二进制文件,Copy部署** +部署简单,源码编译成执行文件后,可以直接运行,减少了对其它插件依赖。不像其它语言,执行文件依赖各种插件,各种库,研发机器运行正常,部署到生产环境,死活跑不起来 。 -### **2、数组和切片的区别 (基本必问)** +**7、简单的并发** -**相同点:** +并行和异步编程几乎无痛点。Go 语言的 Goroutine 和 Channel 这两个神器简直就是并发和异步编程的巨大福音。像 C、C++、Java、Python 和 JavaScript 这些语言的并发和异步方式太控制就比较复杂了,而且容易出错,而 Go 解决这个问题非常地优雅和流畅。这对于编程多年受尽并发和异步折磨的编程者来说,完全就是让人眼前一亮的感觉。Go 是一种非常高效的语言,高度支持并发性。Go是为大数据、微服务、并发而生的一种编程语言。 -1)只能存储一组相同类型的数据结构 +Go 作为一门语言致力于使事情简单化。它并未引入很多新概念,而是聚焦于打造一门简单的语言,它使用起来异常快速并且简单。其唯一的创新之处是 goroutines 和通道。Goroutines 是 Go 面向线程的轻量级方法,而通道是 goroutines 之间通信的优先方式。 -2)都是通过下标来访问,并且有容量长度,长度通过 len 获取,容量通过 cap 获取 +创建 Goroutines 的成本很低,只需几千个字节的额外内存,正由于此,才使得同时运行数百个甚至数千个 goroutines 成为可能。可以借助通道实现 goroutines 之间的通信。Goroutines 以及基于通道的并发性方法使其非常容易使用所有可用的 CPU 内核,并处理并发的 IO。相较于 Python/Java,在一个 goroutine 上运行一个函数需要最小的代码。 -**区别:** +**8、稳定性** -1)数组是定长,访问和复制不能超过数组定义的长度,否则就会下标越界,切片长度和容量可以自动扩容 +Go拥有强大的编译检查、严格的编码规范和完整的软件生命周期工具,具有很强的稳定性,稳定压倒一切。那么为什么Go相比于其他程序会更稳定呢?这是因为Go提供了软件生命周期(开发、测试、部署、维护等等)的各个环节的工具,如go tool、gofmt、go test。 -2)数组是值类型,切片是引用类型,每个切片都引用了一个底层数组,切片本身不能存储任何数据,都是这底层数组存储数据,所以修改切片的时候修改的是底层数组中的数据。切片一旦扩容,指向一个新的底层数组,内存地址也就随之改变 +**9、跨平台** +很多语言都支持跨平台,把这个优点单独拿出来,貌似没有什么值得称道的,但是结合上述优点,它的综合能力就非常强了。 -**简洁的回答:** +### golang 缺点 -1)定义方式不一样 2)初始化方式不一样,数组需要指定大小,大小不改变 3)在函数传递中,数组切片都是值传递。 +**①右大括号不允许换行,否则编译报错** -**数组的定义** +**②不允许有未使用的包或变量** -var a1 [3]int +**③错误处理原始,虽然引入了defer、panic、recover处理出错后的逻辑,函数可以返回多个值,但基本依靠返回错误是否为空来判断函数是否执行成功,if err != nil语句较多,比较繁琐,程序没有java美观。**(官方解释:提供了多个返回值,处理错误方便,如加入异常机制会要求记住一些常见异常,例如IOException,go的错误Error类型较统一方便) +**④[]interface{}不支持下标操作** -var a2 [...]int{1,2,3} +**⑤struct没有构造和析构,一些资源申请和释放动作不太方便** -**切片的定义** +**⑥仍然保留C/C++的指针操作,取地址&,取值\*** -var a1 []int +### **1、golang 中 make 和 new 的区别?(基本必问)** -var a2 :=make([]int,3,5) +**共同点:**给变量分配内存 -**数组的初始化** +**不同点:** -a1 := [...]int{1,2,3} +1)作用变量类型不同,new给string,int和数组分配内存,make给切片,map,channel分配内存; -a2 := [5]int{1,2,3} +2)返回类型不一样,new返回指向变量的指针,make返回变量本身; -**切片的初始化** +3)new 分配的空间被清零。make 分配空间后,会进行初始化; -b:= make([]int,3,5) +\4) 字节的面试官还说了另外一个区别,就是分配的位置,在堆上还是在栈上?这块我比较模糊,大家可以自己探究下,我搜索出来的答案是golang会弱化分配的位置的概念,因为编译的时候会自动内存逃逸处理,懂的大佬帮忙补充下:make、new内存分配是在堆上还是在栈上? ### 2、[IO多路复用](https://zhuanlan.zhihu.com/p/115220699) @@ -699,6 +678,22 @@ func main() { ### 24、[精通Golang项目依赖Go modules](https://www.topgoer.cn/docs/golangxiuyang/golangxiuyang-1cmee13oek1e8) +### 25、Go 多返回值怎么实现的? + +答:Go 传参和返回值是通过 FP+offset 实现,并且存储在调用函数的栈帧中。FP 栈底寄存器,指向一个函数栈的顶部;PC 程序计数器,指向下一条执行指令;SB 指向静态数据的基指针,全局符号;SP 栈顶寄存器。 + +### 26、Go 语言中不同的类型如何比较是否相等? + +答:像 string,int,float interface 等可以通过 reflect.DeepEqual 和等于号进行比较,像 slice,struct,map 则一般使用 reflect.DeepEqual 来检测是否相等。 + +### 27、Go中init 函数的特征? + +答:一个包下可以有多个 init 函数,每个文件也可以有多个 init 函数。多个 init 函数按照它们的文件名顺序逐个初始化。应用初始化时初始化工作的顺序是,从被导入的最深层包开始进行初始化,层层递出最后到 main 包。不管包被导入多少次,包内的 init 函数只会执行一次。应用初始化时初始化工作的顺序是,从被导入的最深层包开始进行初始化,层层递出最后到 main 包。但包级别变量的初始化先于包内 init 函数的执行。 + +### 28、Go中 uintptr和 unsafe.Pointer 的区别? + +答:unsafe.Pointer 是通用指针类型,它不能参与计算,任何类型的指针都可以转化成 unsafe.Pointer,unsafe.Pointer 可以转化成任何类型的指针,uintptr 可以转换为 unsafe.Pointer,unsafe.Pointer 可以转换为 uintptr。uintptr 是指针运算的工具,但是它不能持有指针对象(意思就是它跟指针对象不能互相转换),unsafe.Pointer 是指针对象进行运算(也就是 uintptr)的桥梁。 + ## 二、slice ### **1、数组和切片的区别 (基本必问)** @@ -743,7 +738,7 @@ b:= make([]int,3,5) -### **2、讲讲 Go 的 slice 底层数据结构和一些特性?** +### **2、**[**讲讲 Go 的 slice 底层数据结构和一些特性?**](https://www.topgoer.cn/docs/gozhuanjia/gozhuanjiaslice) 答:Go 的 slice 底层数据结构是由一个 array 指针指向底层数组,len 表示切片长度,cap 表示切片容量。slice 的主要实现是扩容。对于 append 向 slice 添加元素时,假如 slice 容量够用,则追加新元素进去,slice.len++,返回原来的 slice。当原容量不够,则 slice 先扩容,扩容之后 slice 得到新的 slice,将元素追加进新的 slice,slice.len++,返回新的 slice。对于切片的扩容规则:当切片比较小时(容量小于 1024),则采用较大的扩容倍速进行扩容(新的扩容会是原来的 2 倍),避免频繁扩容,从而减少内存分配的次数和数据拷贝的代价。当切片较大的时(原来的 slice 的容量大于或者等于 1024),采用较小的扩容倍速(新的扩容将扩大大于或者等于原来 1.25 倍),主要避免空间浪费,网上其实很多总结的是 1.25 倍,那是在不考虑内存对齐的情况下,实际上还要考虑内存对齐,扩容是大于或者等于 1.25 倍。 @@ -757,6 +752,14 @@ https://blog.csdn.net/weixin_44387482/article/details/119763558 2. 当切片作为参数的时候穿进去的是值,也就是值传递,但是当我在函数里面修改切片的时候,我们发现源数据也会被修改,这是因为我们在切片的底层维护这一个匿名的数组,当我们把切片当成参数的时候,会重现创建一个切片,但是创建的这个切片和我们原来的数据是共享数据源的,所以在函数内被修改,源数据也会被修改 3. 数组还是切片,在函数中传递的时候如果没有指定为指针传递的话,都是值传递,但是切片在传递的过程中,有着共享底层数组的风险,所以如果在函数内部进行了更改的时候,会修改到源数据,所以我们需要根据不同的需求来处理,如果我们不希望源数据被修改话的我们可以使用copy函数复制切片后再传入,如果希望源数据被修改的话我们应该使用指针传递的方式 +### 4、从数组中取一个相同大小的slice有成本吗? + +或者这么问:从切片中取一个相同大小的数组有成本吗? + + + +从数组中截取切片:https://blog.csdn.net/weixin_42117918/article/details/81913036 + ## **三、map相关** @@ -1000,24 +1003,24 @@ hmap数据结构中oldbuckets成员指身原bucket,而buckets指向了新申 ### 1、[Go 语言与鸭子类型的关系](http://golang.design/go-questions/interface/duck-typing/) -总结一下,鸭子类型是一种动态语言的风格,在这种风格中,一个对象有效的语义,不是由继承自特定的类或实现特定的接口,而是由它"当前方法和属性的集合"决定。Go 作为一种静态语言,通过接口实现了 `鸭子类型`,实际上是 Go 的编译器在其中作了隐匿的转换工作。 +总结一下,鸭子类型是一种动态语言的风格,在这种风格中,一个对象有效的语义,不是由继承自特定的类或实现特定的接口,而是由它"当前方法和属性的集合"决定。Go 作为一种静态语言,通过接口实现了 鸭子类型,实际上是 Go 的编译器在其中作了隐匿的转换工作。 ### 2、[值接收者和指针接收者的区别](http://golang.design/go-questions/interface/receiver/) #### 方法 -方法能给用户自定义的类型添加新的行为。它和函数的区别在于方法有一个接收者,给一个函数添加一个接收者,那么它就变成了方法。接收者可以是`值接收者`,也可以是`指针接收者`。 +方法能给用户自定义的类型添加新的行为。它和函数的区别在于方法有一个接收者,给一个函数添加一个接收者,那么它就变成了方法。接收者可以是值接收者,也可以是指针接收者。 -在调用方法的时候,值类型既可以调用`值接收者`的方法,也可以调用`指针接收者`的方法;指针类型既可以调用`指针接收者`的方法,也可以调用`值接收者`的方法。 +在调用方法的时候,值类型既可以调用值接收者的方法,也可以调用指针接收者的方法;指针类型既可以调用指针接收者的方法,也可以调用值接收者的方法。 也就是说,不管方法的接收者是什么类型,该类型的值和指针都可以调用,不必严格符合接收者的类型。 实际上,当类型和方法的接收者类型不同时,其实是编译器在背后做了一些工作,用一个表格来呈现: -| - | 值接收者 | 指针接收者 | +| **-** | **值接收者** | **指针接收者** | | -------------- | ------------------------------------------------------------ | ------------------------------------------------------------ | -| 值类型调用者 | 方法会使用调用者的一个副本,类似于“传值” | 使用值的引用来调用方法,上例中,`qcrao.growUp()` 实际上是 `(&qcrao).growUp()` | -| 指针类型调用者 | 指针被解引用为值,上例中,`stefno.howOld()` 实际上是 `(*stefno).howOld()` | 实际上也是“传值”,方法里的操作会影响到调用者,类似于指针传参,拷贝了一份指针 | +| 值类型调用者 | 方法会使用调用者的一个副本,类似于“传值” | 使用值的引用来调用方法,上例中,qcrao.growUp() 实际上是 (&qcrao).growUp() | +| 指针类型调用者 | 指针被解引用为值,上例中,stefno.howOld() 实际上是 (*stefno).howOld() | 实际上也是“传值”,方法里的操作会影响到调用者,类似于指针传参,拷贝了一份指针 | #### 值接收者和指针接收者 @@ -1029,7 +1032,7 @@ hmap数据结构中oldbuckets成员指身原bucket,而buckets指向了新申 最后,只要记住下面这点就可以了: -> 如果实现了接收者是值类型的方法,会隐含地也实现了接收者是指针类型的方法。 +如果实现了接收者是值类型的方法,会隐含地也实现了接收者是指针类型的方法。 #### 两者分别在何时使用 @@ -1040,84 +1043,80 @@ hmap数据结构中oldbuckets成员指身原bucket,而buckets指向了新申 - 方法能够修改接收者指向的值。 - 避免在每次调用方法时复制该值,在值的类型为大型结构体时,这样做会更加高效。 -是使用值接收者还是指针接收者,不是由该方法是否修改了调用者(也就是接收者)来决定,而是应该基于该类型的`本质`。 +是使用值接收者还是指针接收者,不是由该方法是否修改了调用者(也就是接收者)来决定,而是应该基于该类型的本质。 -如果类型具备“原始的本质”,也就是说它的成员都是由 Go 语言里内置的原始类型,如字符串,整型值等,那就定义值接收者类型的方法。像内置的引用类型,如 slice,map,interface,channel,这些类型比较特殊,声明他们的时候,实际上是创建了一个 `header`, 对于他们也是直接定义值接收者类型的方法。这样,调用函数时,是直接 copy 了这些类型的 `header`,而 `header` 本身就是为复制设计的。 +如果类型具备“原始的本质”,也就是说它的成员都是由 Go 语言里内置的原始类型,如字符串,整型值等,那就定义值接收者类型的方法。像内置的引用类型,如 slice,map,interface,channel,这些类型比较特殊,声明他们的时候,实际上是创建了一个 header, 对于他们也是直接定义值接收者类型的方法。这样,调用函数时,是直接 copy 了这些类型的 header,而 header 本身就是为复制设计的。 -如果类型具备非原始的本质,不能被安全地复制,这种类型总是应该被共享,那就定义指针接收者的方法。比如 go 源码里的文件结构体(struct File)就不应该被复制,应该只有一份`实体`。 +如果类型具备非原始的本质,不能被安全地复制,这种类型总是应该被共享,那就定义指针接收者的方法。比如 go 源码里的文件结构体(struct File)就不应该被复制,应该只有一份实体。 ### 3、[iface 和 eface 的区别是什么](http://golang.design/go-questions/interface/iface-eface/) -`iface` 和 `eface` 都是 Go 中描述接口的底层结构体,区别在于 `iface` 描述的接口包含方法,而 `eface` 则是不包含任何方法的空接口:`interface{}`。 +iface 和 eface 都是 Go 中描述接口的底层结构体,区别在于 iface 描述的接口包含方法,而 eface 则是不包含任何方法的空接口:interface{}。 从源码层面看一下: -```go +```plain type iface struct { - tab *itab - data unsafe.Pointer + tab *itab + data unsafe.Pointer } type itab struct { - inter *interfacetype - _type *_type - link *itab - hash uint32 // copy of _type.hash. Used for type switches. - bad bool // type does not implement interface - inhash bool // has this itab been added to hash? - unused [2]byte - fun [1]uintptr // variable sized + inter *interfacetype + _type *_type + link *itab + hash uint32 // copy of _type.hash. Used for type switches. + bad bool // type does not implement interface + inhash bool // has this itab been added to hash? + unused [2]byte + fun [1]uintptr // variable sized } ``` -`iface` 内部维护两个指针,`tab` 指向一个 `itab` 实体, 它表示接口的类型以及赋给这个接口的实体类型。`data` 则指向接口具体的值,一般而言是一个指向堆内存的指针。 +iface 内部维护两个指针,tab 指向一个 itab 实体, 它表示接口的类型以及赋给这个接口的实体类型。data 则指向接口具体的值,一般而言是一个指向堆内存的指针。 -再来仔细看一下 `itab` 结构体:`_type` 字段描述了实体的类型,包括内存对齐方式,大小等;`inter` 字段则描述了接口的类型。`fun` 字段放置和接口方法对应的具体数据类型的方法地址,实现接口调用方法的动态分派,一般在每次给接口赋值发生转换时会更新此表,或者直接拿缓存的 itab。 +再来仔细看一下 itab 结构体:_type 字段描述了实体的类型,包括内存对齐方式,大小等;inter 字段则描述了接口的类型。fun 字段放置和接口方法对应的具体数据类型的方法地址,实现接口调用方法的动态分派,一般在每次给接口赋值发生转换时会更新此表,或者直接拿缓存的 itab。 这里只会列出实体类型和接口相关的方法,实体类型的其他方法并不会出现在这里。 -另外,你可能会觉得奇怪,为什么 `fun` 数组的大小为 1,要是接口定义了多个方法可怎么办?实际上,这里存储的是第一个方法的函数指针,如果有更多的方法,在它之后的内存空间里继续存储。从汇编角度来看,通过增加地址就能获取到这些函数指针,没什么影响。顺便提一句,这些方法是按照函数名称的字典序进行排列的。 +另外,你可能会觉得奇怪,为什么 fun 数组的大小为 1,要是接口定义了多个方法可怎么办?实际上,这里存储的是第一个方法的函数指针,如果有更多的方法,在它之后的内存空间里继续存储。从汇编角度来看,通过增加地址就能获取到这些函数指针,没什么影响。顺便提一句,这些方法是按照函数名称的字典序进行排列的。 -再看一下 `interfacetype` 类型,它描述的是接口的类型: +再看一下 interfacetype 类型,它描述的是接口的类型: -```go +```plain type interfacetype struct { - typ _type - pkgpath name - mhdr []imethod + typ _type + pkgpath name + mhdr []imethod } ``` -可以看到,它包装了 `_type` 类型,`_type` 实际上是描述 Go 语言中各种数据类型的结构体。我们注意到,这里还包含一个 `mhdr` 字段,表示接口所定义的函数列表, `pkgpath` 记录定义了接口的包名。 +可以看到,它包装了 _type 类型,_type 实际上是描述 Go 语言中各种数据类型的结构体。我们注意到,这里还包含一个 mhdr 字段,表示接口所定义的函数列表, pkgpath 记录定义了接口的包名。 -这里通过一张图来看下 `iface` 结构体的全貌: +这里通过一张图来看下 iface 结构体的全貌: -![iface 结构体全景](https://golang.design/go-questions/interface/assets/0.png) +![img](https://cdn.nlark.com/yuque/0/2022/png/22219483/1671113733638-8e2e9037-11a8-49af-8dd3-dfd37d7f5d21.png) -接着来看一下 `eface` 的源码: +接着来看一下 eface 的源码: -```go +```plain type eface struct { _type *_type data unsafe.Pointer } ``` -相比 `iface`,`eface` 就比较简单了。只维护了一个 `_type` 字段,表示空接口所承载的具体的实体类型。`data` 描述了具体的值。 - -![eface 结构体全景](https://golang.design/go-questions/interface/assets/1.png) - - - +相比 iface,eface 就比较简单了。只维护了一个 _type 字段,表示空接口所承载的具体的实体类型。data 描述了具体的值。 +![img](https://cdn.nlark.com/yuque/0/2022/png/22219483/1671113735267-6bcdb7c8-dd73-432c-b933-d218fc1b7480.png) ### 4、[接口的动态类型和动态值](http://golang.design/go-questions/interface/dynamic-typing/) -从源码里可以看到:`iface`包含两个字段:`tab` 是接口表指针,指向类型信息;`data` 是数据指针,则指向具体的数据。它们分别被称为`动态类型`和`动态值`。而接口值包括`动态类型`和`动态值`。 +从源码里可以看到:iface包含两个字段:tab 是接口表指针,指向类型信息;data 是数据指针,则指向具体的数据。它们分别被称为动态类型和动态值。而接口值包括动态类型和动态值。 -【引申1】接口类型和 `nil` 作比较 +【引申1】接口类型和 nil 作比较 -接口值的零值是指`动态类型`和`动态值`都为 `nil`。当仅且当这两部分的值都为 `nil` 的情况下,这个接口值就才会被认为 `接口值 == nil`。 +接口值的零值是指动态类型和动态值都为 nil。当仅且当这两部分的值都为 nil 的情况下,这个接口值就才会被认为 接口值 == nil。 ### 5、[编译器自动检测类型是否实现接口](http://golang.design/go-questions/interface/detect-impl/) @@ -1125,89 +1124,87 @@ type eface struct { ### 7、[类型转换和断言的区别](http://golang.design/go-questions/interface/assert/) -我们知道,Go 语言中不允许隐式类型转换,也就是说 `=` 两边,不允许出现类型不相同的变量。 +我们知道,Go 语言中不允许隐式类型转换,也就是说 = 两边,不允许出现类型不相同的变量。 -`类型转换`、`类型断言`本质都是把一个类型转换成另外一个类型。不同之处在于,类型断言是对接口变量进行的操作。 +类型转换、类型断言本质都是把一个类型转换成另外一个类型。不同之处在于,类型断言是对接口变量进行的操作。 #### **类型转换** -对于`类型转换`而言,转换前后的两个类型要相互兼容才行。类型转换的语法为: +对于类型转换而言,转换前后的两个类型要相互兼容才行。类型转换的语法为: -> <结果类型> := <目标类型> ( <表达式> ) +<结果类型> := <目标类型> ( <表达式> ) -```go +```plain func main() { - var i int = 9 + var i int = 9 - var f float64 - f = float64(i) - fmt.Printf("%T, %v\n", f, f) + var f float64 + f = float64(i) + fmt.Printf("%T, %v\n", f, f) - f = 10.8 - a := int(f) - fmt.Printf("%T, %v\n", a, a) + f = 10.8 + a := int(f) + fmt.Printf("%T, %v\n", a, a) } ``` - - #### 断言 -前面说过,因为空接口 `interface{}` 没有定义任何函数,因此 Go 中所有类型都实现了空接口。当一个函数的形参是 `interface{}`,那么在函数中,需要对形参进行断言,从而得到它的真实类型。 +前面说过,因为空接口 interface{} 没有定义任何函数,因此 Go 中所有类型都实现了空接口。当一个函数的形参是 interface{},那么在函数中,需要对形参进行断言,从而得到它的真实类型。 断言的语法为: -> <目标类型的值>,<布尔参数> := <表达式>.( 目标类型 ) // 安全类型断言 -> -> <目标类型的值> := <表达式>.( 目标类型 )  //非安全类型断言 +<目标类型的值>,<布尔参数> := <表达式>.( 目标类型 ) // 安全类型断言 + +<目标类型的值> := <表达式>.( 目标类型 ) //非安全类型断言 类型转换和类型断言有些相似,不同之处,在于类型断言是对接口进行的操作。 -```go +```plain type Student struct { - Name string - Age int + Name string + Age int } func main() { - var i interface{} = new(Student) - s, ok := i.(Student) - if ok { - fmt.Println(s) - } + var i interface{} = new(Student) + s, ok := i.(Student) + if ok { + fmt.Println(s) + } } ``` -断言其实还有另一种形式,就是用在利用 `switch` 语句判断接口的类型。每一个 `case` 会被顺序地考虑。当命中一个 `case` 时,就会执行 `case` 中的语句,因此 `case` 语句的顺序是很重要的,因为很有可能会有多个 `case` 匹配的情况。 +断言其实还有另一种形式,就是用在利用 switch 语句判断接口的类型。每一个 case 会被顺序地考虑。当命中一个 case 时,就会执行 case 中的语句,因此 case 语句的顺序是很重要的,因为很有可能会有多个 case 匹配的情况。 ### 8、[接口转换的原理](http://golang.design/go-questions/interface/convert/) -通过前面提到的 `iface` 的源码可以看到,实际上它包含接口的类型 `interfacetype` 和 实体类型的类型 `_type`,这两者都是 `iface` 的字段 `itab` 的成员。也就是说生成一个 `itab` 同时需要接口的类型和实体的类型。 +通过前面提到的 iface 的源码可以看到,实际上它包含接口的类型 interfacetype 和 实体类型的类型 _type,这两者都是 iface 的字段 itab 的成员。也就是说生成一个 itab 同时需要接口的类型和实体的类型。 -> ->itable + ->itable 当判定一种类型是否满足某个接口时,Go 使用类型的方法集和接口所需要的方法集进行匹配,如果类型的方法集完全包含接口的方法集,则可认为该类型实现了该接口。 -例如某类型有 `m` 个方法,某接口有 `n` 个方法,则很容易知道这种判定的时间复杂度为 `O(mn)`,Go 会对方法集的函数按照函数名的字典序进行排序,所以实际的时间复杂度为 `O(m+n)`。 +例如某类型有 m 个方法,某接口有 n 个方法,则很容易知道这种判定的时间复杂度为 O(mn),Go 会对方法集的函数按照函数名的字典序进行排序,所以实际的时间复杂度为 O(m+n)。 这里我们来探索将一个接口转换给另外一个接口背后的原理,当然,能转换的原因必然是类型兼容。 -> 1. 具体类型转空接口时,_type 字段直接复制源类型的 _type;调用 mallocgc 获得一块新内存,把值复制进去,data 再指向这块新内存。 -> 2. 具体类型转非空接口时,入参 tab 是编译器在编译阶段预先生成好的,新接口 tab 字段直接指向入参 tab 指向的 itab;调用 mallocgc 获得一块新内存,把值复制进去,data 再指向这块新内存。 -> 3. 而对于接口转接口,itab 调用 getitab 函数获取。只用生成一次,之后直接从 hash 表中获取。 +1. 具体类型转空接口时,_type 字段直接复制源类型的 _type;调用 mallocgc 获得一块新内存,把值复制进去,data 再指向这块新内存。 +2. 具体类型转非空接口时,入参 tab 是编译器在编译阶段预先生成好的,新接口 tab 字段直接指向入参 tab 指向的 itab;调用 mallocgc 获得一块新内存,把值复制进去,data 再指向这块新内存。 +3. 而对于接口转接口,itab 调用 getitab 函数获取。只用生成一次,之后直接从 hash 表中获取。 ### 9、[如何用 interface 实现多态](http://golang.design/go-questions/interface/polymorphism/) -`Go` 语言并没有设计诸如虚函数、纯虚函数、继承、多重继承等概念,但它通过接口却非常优雅地支持了面向对象的特性。 +Go 语言并没有设计诸如虚函数、纯虚函数、继承、多重继承等概念,但它通过接口却非常优雅地支持了面向对象的特性。 多态是一种运行期的行为,它有以下几个特点: -> 1. 一种类型具有多种类型的能力 -> 2. 允许不同的对象对同一消息做出灵活的反应 -> 3. 以一种通用的方式对待个使用的对象 -> 4. 非动态语言必须通过继承和接口的方式来实现 +1. 一种类型具有多种类型的能力 +2. 允许不同的对象对同一消息做出灵活的反应 +3. 以一种通用的方式对待个使用的对象 +4. 非动态语言必须通过继承和接口的方式来实现 -`main` 函数里先生成 `Student` 和 `Programmer` 的对象,再将它们分别传入到函数 `whatJob` 和 `growUp`。函数中,直接调用接口函数,实际执行的时候是看最终传入的实体类型是什么,调用的是实体类型实现的函数。于是,不同对象针对同一消息就有多种表现,`多态`就实现了。 +main 函数里先生成 Student 和 Programmer 的对象,再将它们分别传入到函数 whatJob 和 growUp。函数中,直接调用接口函数,实际执行的时候是看最终传入的实体类型是什么,调用的是实体类型实现的函数。于是,不同对象针对同一消息就有多种表现,多态就实现了。 ### 10、[Go 接口与 C++ 接口有何异同](http://golang.design/go-questions/interface/compare-to-cpp/) @@ -1215,7 +1212,7 @@ func main() { C++ 的接口是使用抽象类来实现的,如果类中至少有一个函数被声明为纯虚函数,则这个类就是抽象类。纯虚函数是通过在声明中使用 “= 0” 来指定的。例如: -```go +```plain class Shape { public: @@ -1232,10 +1229,14 @@ class Shape C++ 定义接口的方式称为“侵入式”,而 Go 采用的是 “非侵入式”,不需要显式声明,只需要实现接口定义的函数,编译器自动会识别。 -C++ 和 Go 在定义接口方式上的不同,也导致了底层实现上的不同。C++ 通过虚函数表来实现基类调用派生类的函数;而 Go 通过 `itab` 中的 `fun` 字段来实现接口变量调用实体类型的函数。C++ 中的虚函数表是在编译期生成的;而 Go 的 `itab` 中的 `fun` 字段是在运行期间动态生成的。原因在于,Go 中实体类型可能会无意中实现 N 多接口,很多接口并不是本来需要的,所以不能为类型实现的所有接口都生成一个 `itab`, 这也是“非侵入式”带来的影响;这在 C++ 中是不存在的,因为派生需要显示声明它继承自哪个基类。 +C++ 和 Go 在定义接口方式上的不同,也导致了底层实现上的不同。C++ 通过虚函数表来实现基类调用派生类的函数;而 Go 通过 itab 中的 fun 字段来实现接口变量调用实体类型的函数。C++ 中的虚函数表是在编译期生成的;而 Go 的 itab 中的 fun 字段是在运行期间动态生成的。原因在于,Go 中实体类型可能会无意中实现 N 多接口,很多接口并不是本来需要的,所以不能为类型实现的所有接口都生成一个 itab, 这也是“非侵入式”带来的影响;这在 C++ 中是不存在的,因为派生需要显示声明它继承自哪个基类。 + + ## 五**、context相关** +https://www.topgoer.cn/docs/gozhuanjia/chapter055.3-context + ### **1、context 结构是什么样的?context 使用场景和用途?** **(难,也常常问你项目中怎么用,光靠记答案很难让面试官满意,反正有各种结合实际的问题)** @@ -1579,6 +1580,12 @@ mutex 会让当前的 goroutine 去空转 CPU,在空转完后再次调用 CAS ## **九、并发相关** +### 0、讲讲 Go 中主协程如何等待其余协程退出? + +答:Go 的 sync.WaitGroup 是等待一组协程结束,sync.WaitGroup 只有 3 个方法,Add()是添加计数,Done()减去一个计数,Wait()阻塞直到所有的任务完成。Go 里面还能通过有缓冲的 channel 实现其阻塞等待一组协程结束,这个不能保证一组 goroutine 按照顺序执行,可以并发执行协程。Go 里面能通过无缓冲的 channel 实现其阻塞等待一组协程结束,这个能保证一组 goroutine 按照顺序执行,但是不能并发执行。 + +**啰嗦一句:**循环智能二面,手写代码部分时,三个协程按交替顺序打印数字,最后题目做出来了,问我代码中Add()是什么意思,我回答的不是很清晰,这家公司就没有然后了。Add()表示协程计数,可以一次Add多个,如Add(3),可以多次Add(1);然后每个子协程必须调用done(),这样才能保证所有子协程结束,主协程才能结束。 + ### 1、怎么控制并发数? **第一,有缓冲通道** @@ -1662,6 +1669,121 @@ defer func() { **所以直接研究ants底层吧,省的造轮子。** +### 4、golang实现多并发请求(发送多个get请求) + +在[go语言](https://so.csdn.net/so/search?q=go语言&spm=1001.2101.3001.7020)中其实有两种方法进行协程之间的通信。**一个是共享内存、一个是消息传递** + +[**共享内存(互斥锁)**](https://blog.csdn.net/m0_43432638/article/details/108359182) + +```go +//基本的GET请求 +package main + +import ( + "fmt" + "io/ioutil" + "net/http" + "time" + "sync" + "runtime" +) + +// 计数器 +var counter int = 0 + +func httpget(lock *sync.Mutex){ + lock.Lock() + counter++ + resp, err := http.Get("http://localhost:8000/rest/api/user") + if err != nil { + fmt.Println(err) + return + } + defer resp.Body.Close() + body, err := ioutil.ReadAll(resp.Body) + fmt.Println(string(body)) + fmt.Println(resp.StatusCode) + if resp.StatusCode == 200 { + fmt.Println("ok") + } + lock.Unlock() +} + +func main() { + start := time.Now() + lock := &sync.Mutex{} + for i := 0; i < 800; i++ { + go httpget(lock) + } + for { + lock.Lock() + c := counter + lock.Unlock() + runtime.Gosched() + if c >= 800 { + break + } + } + end := time.Now() + consume := end.Sub(start).Seconds() + fmt.Println("程序执行耗时(s):", consume) +} +``` + +问题 + +我们可以看到共享内存的方式是可以做到并发,但是我们需要利用共享变量来进行[协程](https://so.csdn.net/so/search?q=协程&spm=1001.2101.3001.7020)的通信,也就需要使用互斥锁来确保数据安全性,导致代码啰嗦,复杂话,不易维护。我们后续使用go的[消息传递](https://blog.csdn.net/m0_43432638/article/details/108349384)方式避免这些问题。 + +[**消息传递(管道)**](https://blog.csdn.net/m0_43432638/article/details/108349384) + +```go +//基本的GET请求 +package main + +import ( + "fmt" + "io/ioutil" + "net/http" + "time" +) +// HTTP get请求 +func httpget(ch chan int){ + resp, err := http.Get("http://localhost:8000/rest/api/user") + if err != nil { + fmt.Println(err) + return + } + defer resp.Body.Close() + body, err := ioutil.ReadAll(resp.Body) + fmt.Println(string(body)) + fmt.Println(resp.StatusCode) + if resp.StatusCode == 200 { + fmt.Println("ok") + } + ch <- 1 +} +// 主方法 +func main() { + start := time.Now() + // 注意设置缓冲区大小要和开启协程的个人相等 + chs := make([]chan int, 2000) + for i := 0; i < 2000; i++ { + chs[i] = make(chan int) + go httpget(chs[i]) + } + for _, ch := range chs { + <- ch + } + end := time.Now() + consume := end.Sub(start).Seconds() + fmt.Println("程序执行耗时(s):", consume) +} +``` + +**总结:** + +我们通过[go语言](https://so.csdn.net/so/search?q=go语言&spm=1001.2101.3001.7020)的管道channel来实现并发请求,能够解决如何避免传统共享内存实现并发的很多问题而且效率会高于共享内存的方法。 + ## **十、GC相关** https://www.topgoer.cn/docs/gozhuanjia/chapter044.2-garbage_collection @@ -1674,7 +1796,7 @@ https://www.topgoer.cn/docs/golangxiuyang/golangxiuyang-1cmee076rjgk7 **细分常见的三个问题:1、GC机制随着golang版本变化如何变化的?2、三色标记法的流程?3、插入屏障、删除屏障,混合写屏障(具体的实现比较难描述,但你要知道屏障的作用:避免程序运行过程中,变量被误回收;减少STW的时间)4、虾皮还问了个开放性的题目:你觉得以后GC机制会怎么优化?** -Go 的 GC 回收有三次演进过程,Go V1.3 之前普通标记清除(mark and sweep)方法,整体过程需要启动 STW,效率极低。GoV1.5 三色标记法,堆空间启动写屏障,栈空间不启动,全部扫描之后,需要重新扫描一次栈(需要 STW),效率普通。GoV1.8 三色标记法,混合写屏障机制:栈空间不启动(全部标记成黑色),堆空间启用写屏障,整个过程不要 STW,效率高。 +**Go 的 GC 回收有三次演进过程,Go V1.3 之前普通标记清除(mark and sweep)方法,整体过程需要启动 STW,效率极低。GoV1.5 三色标记法,堆空间启动写屏障,栈空间不启动,全部扫描之后,需要重新扫描一次栈(需要 STW),效率普通。GoV1.8 三色标记法,混合写屏障机制:栈空间不启动(全部标记成黑色),堆空间启用写屏障,整个过程不要 STW,效率高。** Go1.3 之前的版本所谓**标记清除**是先启动 STW 暂停,然后执行标记,再执行数据回收,最后停止 STW。Go1.3 版本标记清除做了点优化,流程是:先启动 STW 暂停,然后执行标记,停止 STW,最后再执行数据回收。 @@ -1838,105 +1960,195 @@ Channel 被设计用来实现协程间通信的组件,其作用域和生命周 ### [逃逸分析是怎么进行的](http://golang.design/go-questions/compile/escape/) + + 在编译原理中,分析指针动态范围的方法称之为逃逸分析。通俗来讲,当一个对象的指针被多个方法或线程引用时,我们称这个指针发生了逃逸。 + + Go语言的逃逸分析是编译器执行静态代码分析后,对内存管理进行的优化和简化,它可以决定一个变量是分配到堆还栈上。 + + 写过C/C++的同学都知道,调用著名的malloc和new函数可以在堆上分配一块内存,这块内存的使用和销毁的责任都在程序员。一不小心,就会发生内存泄露。 + + Go语言里,基本不用担心内存泄露了。虽然也有new函数,但是使用new函数得到的内存不一定就在堆上。堆和栈的区别对程序员“模糊化”了,当然这一切都是Go编译器在背后帮我们完成的。 + + Go语言逃逸分析最基本的原则是:如果一个函数返回对一个变量的引用,那么它就会发生逃逸。 + + 简单来说,编译器会分析代码的特征和代码生命周期,Go中的变量只有在编译器可以证明在函数返回后不会再被引用的,才分配到栈上,其他情况下都是分配到堆上。 + + Go语言里没有一个关键字或者函数可以直接让变量被编译器分配到堆上,相反,编译器通过分析代码来决定将变量分配到何处。 + + 对一个变量取地址,可能会被分配到堆上。但是编译器进行逃逸分析后,如果考察到在函数返回后,此变量不会被引用,那么还是会被分配到栈上。 + + 编译器会根据变量是否被外部引用来决定是否逃逸: -> 1. 如果函数外部没有引用,则优先放到栈中; -> 2. 如果函数外部存在引用,则必定放到堆中; + + +1. 如果函数外部没有引用,则优先放到栈中; +2. 如果函数外部存在引用,则必定放到堆中; + + Go的垃圾回收,让堆和栈对程序员保持透明。真正解放了程序员的双手,让他们可以专注于业务,“高效”地完成代码编写。把那些内存管理的复杂机制交给编译器,而程序员可以去享受生活。 + + 逃逸分析这种“骚操作”把变量合理地分配到它该去的地方。即使你是用new申请到的内存,如果我发现你竟然在退出函数后没有用了,那么就把你丢到栈上,毕竟栈上的内存分配比堆上快很多;反之,即使你表面上只是一个普通的变量,但是经过逃逸分析后发现在退出函数之后还有其他地方在引用,那我就把你分配到堆上。 + + 如果变量都分配到堆上,堆不像栈可以自动清理。它会引起Go频繁地进行垃圾回收,而垃圾回收会占用比较大的系统开销(占用CPU容量的25%)。 + + 堆和栈相比,堆适合不可预知大小的内存分配。但是为此付出的代价是分配速度较慢,而且会形成内存碎片。栈内存分配则会非常快。栈分配内存只需要两个CPU指令:“PUSH”和“RELEASE”,分配和释放;而堆分配内存首先需要去找到一块大小合适的内存块,之后要通过垃圾回收才能释放。 + + 通过逃逸分析,可以尽量把那些不需要分配到堆上的变量直接分配到栈上,堆上的变量少了,会减轻分配堆内存的开销,同时也会减少gc的压力,提高程序的运行速度。 + + ### [GoRoot 和 GoPath 有什么用](http://golang.design/go-questions/compile/gopath/) + + GoRoot 是 Go 的安装路径。mac 或 unix 是在 `/usr/local/go` 路径上,来看下这里都装了些什么: -![/usr/local/go](https://golang.design/go-questions/compile/assets/1.png) + + +![img](https://golang.design/go-questions/compile/assets/1.png) + + bin 目录下面: -![bin](https://golang.design/go-questions/compile/assets/2.png) + + +![img](https://golang.design/go-questions/compile/assets/2.png) + + pkg 目录下面: -![pkg](https://golang.design/go-questions/compile/assets/3.png) + + +![img](https://golang.design/go-questions/compile/assets/3.png) + + Go 工具目录如下,其中比较重要的有编译器 `compile`,链接器 `link`: -![pkg/tool](https://golang.design/go-questions/compile/assets/4.png) + + +![img](https://golang.design/go-questions/compile/assets/4.png) + + GoPath 的作用在于提供一个可以寻找 `.go` 源码的路径,它是一个工作空间的概念,可以设置多个目录。Go 官方要求,GoPath 下面需要包含三个文件夹: + + ```go src pkg bin ``` + + src 存放源文件,pkg 存放源文件编译后的库文件,后缀为 `.a`;bin 则存放可执行文件。 + + ### [Go 编译链接过程概述](http://golang.design/go-questions/compile/link-process/) + + Go 程序并不能直接运行,每条 Go 语句必须转化为一系列的低级机器语言指令,将这些指令打包到一起,并以二进制磁盘文件的形式存储起来,也就是可执行目标文件。 + + 从源文件到可执行目标文件的转化过程: -![compile](https://golang.design/go-questions/compile/assets/7.png) + + +![img](https://golang.design/go-questions/compile/assets/7.png) + + 完成以上各个阶段的就是 Go 编译系统。你肯定知道大名鼎鼎的 GCC(GNU Compile Collection),中文名为 GNU 编译器套装,它支持像 C,C++,Java,Python,Objective-C,Ada,Fortran,Pascal,能够为很多不同的机器生成机器码。 + + 可执行目标文件可以直接在机器上执行。一般而言,先执行一些初始化的工作;找到 main 函数的入口,执行用户写的代码;执行完成后,main 函数退出;再执行一些收尾的工作,整个过程完毕。 + + 在接下来的文章里,我们将探索`编译`和`运行`的过程。 + + Go 源码里的编译器源码位于 `src/cmd/compile` 路径下,链接器源码位于 `src/cmd/link` 路径下。 + + ### [Go 编译相关的命令详解](http://golang.design/go-questions/compile/cmd/) + + 和编译相关的命令主要是: + + ```go go build go install go run ``` + + #### go build + + `go build` 用来编译指定 packages 里的源码文件以及它们的依赖包,编译的时候会到 `$GoPath/src/package` 路径下寻找源码文件。`go build` 还可以直接编译指定的源码文件,并且可以同时指定多个。 + + 通过执行 `go help build` 命令得到 `go build` 的使用方法: + + ```go usage: go build [-o output] [-i] [build flags] [packages] ``` + + `-o` 只能在编译单个包的时候出现,它指定输出的可执行文件的名字。 + + `-i` 会安装编译目标所依赖的包,安装是指生成与代码包相对应的 `.a` 文件,即静态库文件(后面要参与链接),并且放置到当前工作区的 pkg 目录下,且库文件的目录层级和源码层级一致。 + + 至于 build flags 参数,`build, clean, get, install, list, run, test` 这些命令会共用一套: | 参数 | 作用 | @@ -1949,22 +2161,40 @@ usage: go build [-o output] [-i] [build flags] [packages] | -x | 打印命令执行过程中所涉及到的命令,并执行 | | -work | 打印编译过程中的临时文件夹。通常情况下,编译完成后会被删除 | + + 我们知道,Go 语言的源码文件分为三类:命令源码、库源码、测试源码。 -> 命令源码文件:是 Go 程序的入口,包含 `func main()` 函数,且第一行用 `package main` 声明属于 main 包。 -> 库源码文件:主要是各种函数、接口等,例如工具类的函数。 -> 测试源码文件:以 `_test.go` 为后缀的文件,用于测试程序的功能和性能。 +命令源码文件:是 Go 程序的入口,包含 `func main()` 函数,且第一行用 `package main` 声明属于 main 包。 + + + +库源码文件:主要是各种函数、接口等,例如工具类的函数。 + + + +测试源码文件:以 `_test.go` 为后缀的文件,用于测试程序的功能和性能。 + + 注意,`go build` 会忽略 `*_test.go` 文件。 + + #### go install + + `go install` 用于编译并安装指定的代码包及它们的依赖包。相比 `go build`,它只是多了一个“安装编译后的结果文件到指定目录”的步骤。 + + 还是使用之前 hello-world 项目的例子,我们先将 pkg 目录删掉,在项目根目录执行: + + ```go go install src/main.go @@ -1973,16 +2203,28 @@ go install src/main.go go install util ``` + + 两者都会在根目录下新建一个 `pkg` 目录,并且生成一个 `util.a` 文件。 + + 并且,在执行前者的时候,会在 GOBIN 目录下生成名为 main 的可执行文件。 + + 所以,运行 `go install` 命令,库源码包对应的 `.a` 文件会被放置到 `pkg` 目录下,命令源码包生成的可执行文件会被放到 GOBIN 目录。 + + `go install` 在 GoPath 有多个目录的时候,会产生一些问题,具体可以去看郝林老师的 `Go 命令教程`,这里不展开了。 + + #### go run + + `go run` 用于编译并运行命令源码文件。 ### [Go 程序启动过程是怎样的](http://golang.design/go-questions/compile/booting/) @@ -1991,8 +2233,46 @@ go install util ### Gin +文档:https://gin-gonic.com/zh-cn/docs/introduction/ + +#### 0、特性 + +1. **快速** + +1. 1. 基于 Radix 树的路由,小内存占用。没有反射。可预测的 API 性能。 + +1. **支持中间件** + +1. 1. 传入的 HTTP 请求可以由一系列中间件和最终操作来处理。 例如:Logger,Authorization,GZIP,最终操作 DB。 + +1. **Crash 处理** + +1. 1. Gin 可以 catch 一个发生在 HTTP 请求中的 panic 并 recover 它。这样,你的服务器将始终可用。例如,你可以向 Sentry 报告这个 panic! + +1. **JSON 验证** + +1. 1. Gin 可以解析并验证请求的 JSON,例如检查所需值的存在。 + +1. **路由组** + +1. 1. 更好地组织路由。是否需要授权,不同的 API 版本…… 此外,这些组可以无限制地嵌套而不会降低性能。 + +1. **错误管理** + +1. 1. Gin 提供了一种方便的方法来收集 HTTP 请求期间发生的所有错误。最终,中间件可以将它们写入日志文件,数据库并通过网络发送。 + +1. **内置渲染** + +1. 1. Gin 为 JSON,XML 和 HTML 渲染提供了易于使用的 API。 + +1. **可扩展性** + +1. 1. 新建一个中间件非常简单,去查看[示例代码](https://gin-gonic.com/zh-cn/docs/examples/using-middleware/)吧。 + #### 1、[gin目录结构](https://blog.csdn.net/qq_34877350/article/details/107959381) +文档:https://blog.csdn.net/qq_34877350/article/details/107959381 + ```plain ├── gin │ ├── Router @@ -2027,101 +2307,62 @@ go install util #### 2、[Gin框架介绍及使用 - 李文周的博客](https://www.liwenzhou.com/posts/Go/Gin_framework/#autoid-0-0-0) +文档:https://www.liwenzhou.com/posts/Go/Gin_framework/#autoid-0-0-0 +#### 3、源码 -## 十四、其他问题 - -### 0、为什么选择golang - -**1、学习曲线容易** - -Go语言语法简单,包含了类C语法。因为Go语言容易学习,所以一个普通的大学生花几个星期就能写出来可以上手的、高性能的应用。在国内大家都追求快,这也是为什么国内Go流行的原因之一。 - -Go 语言的语法特性简直是太简单了,简单到你几乎玩不出什么花招,直来直去的,学习曲线很低,上手非常快。 - -**2、效率:快速的编译时间,开发效率和运行效率高** - -开发过程中相较于 Java 和 C++呆滞的编译速度,Go 的快速编译时间是一个主要的效率优势。Go拥有接近C的运行效率和接近PHP的开发效率。 - -C 语言的理念是信任程序员,保持语言的小巧,不屏蔽底层且底层友好,关注语言的执行效率和性能。而 Python 的姿态是用尽量少的代码完成尽量多的事。于是我能够感觉到,Go 语言想要把 C 和 Python 统一起来,这是多棒的一件事啊。 - -**3、出身名门、血统纯正** - -之所以说Go出身名门,从Go语言的创造者就可见端倪,Go语言绝对血统纯正。其次Go语言出自Google公司,Google在业界的知名度和实力自然不用多说。Google公司聚集了一批牛人,在各种编程语言称雄争霸的局面下推出新的编程语言,自然有它的战略考虑。而且从Go语言的发展态势来看,Google对它这个新的宠儿还是很看重的,Go自然有一个良好的发展前途。 - -**4、自由高效:组合的思想、无侵入式的接口** - -Go语言可以说是开发效率和运行效率二者的完美融合,天生的并发编程支持。Go语言支持当前所有的编程范式,包括过程式编程、面向对象编程、面向接口编程、函数式编程。程序员们可以各取所需、自由组合、想怎么玩就怎么玩。 - -**5、强大的标准库** - -这包括互联网应用、系统编程和网络编程。Go里面的标准库基本上已经是非常稳定了,特别是我这里提到的三个,网络层、系统层的库非常实用。Go 语言的 lib 库麻雀虽小五脏俱全。Go 语言的 lib 库中基本上有绝大多数常用的库,虽然有些库还不是很好,但我觉得不是问题,因为我相信在未来的发展中会把这些问题解决掉。 +[Gin源码阅读与分析](https://www.yuque.com/iveryimportantpig/huchao/zd24cb3z2bco5304):https://www.yuque.com/iveryimportantpig/huchao/zd24cb3z2bco5304 -**6、部署方便:二进制文件,Copy部署** +### go-zero -这一点是很多人选择Go的最大理由,因为部署太方便了,所以现在也有很多人用Go开发运维程序。 +文档:https://go-zero.dev/cn/docs/introduction -**7、简单的并发** +go-zero 是一个集成了各种工程实践的 web 和 rpc 框架。通过弹性设计保障了大并发服务端的稳定性,经受了充分的实战检验。 -并行和异步编程几乎无痛点。Go 语言的 Goroutine 和 Channel 这两个神器简直就是并发和异步编程的巨大福音。像 C、C++、Java、Python 和 JavaScript 这些语言的并发和异步方式太控制就比较复杂了,而且容易出错,而 Go 解决这个问题非常地优雅和流畅。这对于编程多年受尽并发和异步折磨的编程者来说,完全就是让人眼前一亮的感觉。Go 是一种非常高效的语言,高度支持并发性。Go是为大数据、微服务、并发而生的一种编程语言。 +go-zero 包含极简的 API 定义和生成工具 goctl,可以根据定义的 api 文件一键生成 Go, iOS, Android, Kotlin, Dart, TypeScript, JavaScript 代码,并可直接运行。 -Go 作为一门语言致力于使事情简单化。它并未引入很多新概念,而是聚焦于打造一门简单的语言,它使用起来异常快速并且简单。其唯一的创新之处是 goroutines 和通道。Goroutines 是 Go 面向线程的轻量级方法,而通道是 goroutines 之间通信的优先方式。 +使用 go-zero 的好处: -创建 Goroutines 的成本很低,只需几千个字节的额外内存,正由于此,才使得同时运行数百个甚至数千个 goroutines 成为可能。可以借助通道实现 goroutines 之间的通信。Goroutines 以及基于通道的并发性方法使其非常容易使用所有可用的 CPU 内核,并处理并发的 IO。相较于 Python/Java,在一个 goroutine 上运行一个函数需要最小的代码。 - -**8、稳定性** - -Go拥有强大的编译检查、严格的编码规范和完整的软件生命周期工具,具有很强的稳定性,稳定压倒一切。那么为什么Go相比于其他程序会更稳定呢?这是因为Go提供了软件生命周期(开发、测试、部署、维护等等)的各个环节的工具,如go tool、gofmt、go test。 +- 轻松获得支撑千万日活服务的稳定性 +- 内建级联超时控制、限流、自适应熔断、自适应降载等微服务治理能力,无需配置和额外代码 +- 微服务治理中间件可无缝集成到其它现有框架使用 +- 极简的 API 描述,一键生成各端代码 +- 自动校验客户端请求参数合法性 +- 大量微服务治理和并发工具包 -### 1、Go 多返回值怎么实现的? - -答:Go 传参和返回值是通过 FP+offset 实现,并且存储在调用函数的栈帧中。FP 栈底寄存器,指向一个函数栈的顶部;PC 程序计数器,指向下一条执行指令;SB 指向静态数据的基指针,全局符号;SP 栈顶寄存器。 - -### 2、讲讲 Go 中主协程如何等待其余协程退出? - -答:Go 的 sync.WaitGroup 是等待一组协程结束,sync.WaitGroup 只有 3 个方法,Add()是添加计数,Done()减去一个计数,Wait()阻塞直到所有的任务完成。Go 里面还能通过有缓冲的 channel 实现其阻塞等待一组协程结束,这个不能保证一组 goroutine 按照顺序执行,可以并发执行协程。Go 里面能通过无缓冲的 channel 实现其阻塞等待一组协程结束,这个能保证一组 goroutine 按照顺序执行,但是不能并发执行。 - -**啰嗦一句:**循环智能二面,手写代码部分时,三个协程按交替顺序打印数字,最后题目做出来了,问我代码中Add()是什么意思,我回答的不是很清晰,这家公司就没有然后了。Add()表示协程计数,可以一次Add多个,如Add(3),可以多次Add(1);然后每个子协程必须调用done(),这样才能保证所有子协程结束,主协程才能结束。 - -### 3、Go 语言中不同的类型如何比较是否相等? - -答:像 string,int,float interface 等可以通过 reflect.DeepEqual 和等于号进行比较,像 slice,struct,map 则一般使用 reflect.DeepEqual 来检测是否相等。 - -### 4、Go 中 init 函数的特征? - -答:一个包下可以有多个 init 函数,每个文件也可以有多个 init 函数。多个 init 函数按照它们的文件名顺序逐个初始化。应用初始化时初始化工作的顺序是,从被导入的最深层包开始进行初始化,层层递出最后到 main 包。不管包被导入多少次,包内的 init 函数只会执行一次。应用初始化时初始化工作的顺序是,从被导入的最深层包开始进行初始化,层层递出最后到 main 包。但包级别变量的初始化先于包内 init 函数的执行。 - -### 5、Go 中 uintptr 和 unsafe.Pointer 的区别? - -答:unsafe.Pointer 是通用指针类型,它不能参与计算,任何类型的指针都可以转化成 unsafe.Pointer,unsafe.Pointer 可以转化成任何类型的指针,uintptr 可以转换为 unsafe.Pointer,unsafe.Pointer 可以转换为 uintptr。uintptr 是指针运算的工具,但是它不能持有指针对象(意思就是它跟指针对象不能互相转换),unsafe.Pointer 是指针对象进行运算(也就是 uintptr)的桥梁。 - -### 6、golang共享内存(互斥锁)方法实现发送多个get请求 - -待补充 - -### 7、从数组中取一个相同大小的slice有成本吗? - -或者这么问:从切片中取一个相同大小的数组有成本吗? +### 字节-CloudWeGo -这是爱立信的二面题目,这个问题我至今还没搞懂,不知道从什么切入点去分析,欢迎指教。 +文档:https://www.cloudwego.io/zh/docs/ -PS:爱立信面试都要英文自我介绍,以及问答,如果英文回答不上来,会直接切换成中文。 +### HTTP-Hertz -### 8、PHP能实现并发处理事务吗? +文档:https://www.cloudwego.io/zh/docs/hertz/overview/ -**多进程:pcntl扩展** +是一个 Golang 微服务 HTTP 框架,在设计之初参考了其他开源框架 [fasthttp](https://github.com/valyala/fasthttp)、[gin](https://github.com/gin-gonic/gin)、[echo](https://github.com/labstack/echo) 的优势, 并结合字节跳动内部的需求,使其具有高易用性、高性能、高扩展性等特点,目前在字节跳动内部已广泛使用。 如今越来越多的微服务选择使用 Golang,如果对微服务性能有要求,又希望框架能够充分满足内部的可定制化需求,Hertz 会是一个不错的选择。 -[php pcntl用法-PHP问题-PHP中文网www.php.cn/php-ask-473095.html](https://link.zhihu.com/?target=https%3A//www.php.cn/php-ask-473095.html) +**特点** -**多线程:** +- 高易用性在开发过程中,快速写出来正确的代码往往是更重要的。因此,在 Hertz 在迭代过程中,积极听取用户意见,持续打磨框架,希望为用户提供一个更好的使用体验,帮助用户更快的写出正确的代码。 +- 高性能Hertz 默认使用自研的高性能网络库 Netpoll,在一些特殊场景相较于 go net,Hertz 在 QPS、时延上均具有一定优势。关于性能数据,可参考下图 Echo 数据。四个框架的对比:![img](https://cdn.nlark.com/yuque/0/2023/png/22219483/1675414683589-8ae9d18c-b2e6-43bd-943f-7392415e0e74.png)三个框架的对比:![img](https://cdn.nlark.com/yuque/0/2023/png/22219483/1675414685005-e51955bc-2290-48b8-8782-11f6a26f4efc.png)关于详细的性能数据,可参考 https://github.com/cloudwego/hertz-benchmark。 +- 高扩展性Hertz 采用了分层设计,提供了较多的接口以及默认的扩展实现,用户也可以自行扩展。同时得益于框架的分层设计,框架的扩展性也会大很多。目前仅将稳定的能力开源给社区,更多的规划参考 [RoadMap](https://github.com/cloudwego/hertz/blob/main/ROADMAP.md)。 +- 多协议支持Hertz 框架原生提供 HTTP1.1、ALPN 协议支持。除此之外,由于分层设计,Hertz 甚至支持自定义构建协议解析逻辑,以满足协议层扩展的任意需求。 +- 网络层切换能力Hertz 实现了 Netpoll 和 Golang 原生网络库 间按需切换能力,用户可以针对不同的场景选择合适的网络库,同时也支持以插件的方式为 Hertz 扩展网络库实现。 -1)swoole(常用,生态完善) +### RPC-Kitex -2)pthread扩展(不常用) +文档:https://www.cloudwego.io/zh/docs/kitex/overview/ -[为什么php多线程没人用?23 赞同 · 18 评论回答](https://www.zhihu.com/question/371492817/answer/1029696815) +字节跳动内部的 Golang 微服务 RPC 框架,具有**高性能**、**强可扩展**的特点,在字节内部已广泛使用。如果对微服务性能有要求,又希望定制扩展融入自己的治理体系,Kitex 会是一个不错的选择。 +**框架特点** +- **高性能**使用自研的高性能网络库 [Netpoll](https://github.com/cloudwego/netpoll),性能相较 go net 具有显著优势。 +- **扩展性**提供了较多的扩展接口以及默认扩展实现,使用者也可以根据需要自行定制扩展,具体见下面的框架扩展。 +- **多消息协议**RPC 消息协议默认支持 **Thrift**、**Kitex Protobuf**、**gRPC**。Thrift 支持 Buffered 和 Framed 二进制协议;Kitex Protobuf 是 Kitex 自定义的 Protobuf 消息协议,协议格式类似 Thrift;gRPC 是对 gRPC 消息协议的支持,可以与 gRPC 互通。除此之外,使用者也可以扩展自己的消息协议。 +- **多传输协议**传输协议封装消息协议进行 RPC 互通,传输协议可以额外透传元信息,用于服务治理,Kitex 支持的传输协议有 **TTHeader**、**HTTP2**。TTHeader 可以和 Thrift、Kitex Protobuf 结合使用;HTTP2 目前主要是结合 gRPC 协议使用,后续也会支持 Thrift。 +- **多种消息类型**支持 **PingPong**、**Oneway**、**双向 Streaming**。其中 Oneway 目前只对 Thrift 协议支持,双向 Streaming 只对 gRPC 支持,后续会考虑支持 Thrift 的双向 Streaming。 +- **服务治理**支持服务注册/发现、负载均衡、熔断、限流、重试、监控、链路跟踪、日志、诊断等服务治理模块,大部分均已提供默认扩展,使用者可选择集成。 +- **代码生成**Kitex 内置代码生成工具,可支持生成 **Thrift**、**Protobuf** 以及脚手架代码。 ## 参考并致谢