go面试集锦1-39

目录

1.go优缺点

优点:
	1.性能高,运行快。是python的30倍
	2.它有很大的社区支持
	3.天然支持并发,协程
	4.可以直接操作指针
	5.编译快
	
缺点:
	1.类型不能自动转换
	2.错误处理,啰嗦
	3.包严重依赖于github

2.go中常量是怎么实现

const c1 int = 1000
常量在编译时分配到只读数据段
常量、代码、初始化的全局变量都会被存储在:只读的数据段
只读的数据段:只读的数据段(read-only data segment)是一种特殊的内存区域,用于存储不可修改的数据。
只读数据段与其他内存区域的主要区别在于:
1.只读数据段中的数据是不可修改的,任何试图修改这些数据的尝试都会引发运行时错误。
2.只读数据段通常会被映射到进程的地址空间中,以供程序访问。
3.操作系统会对只读数据段进行特殊的内存页保护,防止意外修改

3.go的值传递和引用

map和slice都是指针传递(引用),即函数内部是可以改变参数的值的
数组是值传递,不管函数内部如何改变参数,都是改变的拷贝值

4.go struct能不能比较

不能。
但是结构体中的成员变量可以比较

4.1 go中哪些结构可以比较

可以比较的:基本类型,数组,结构体(字段都可以比较),指针,interface(值是可比较类型)
基本类型: 如 int、float、string、bool 
数组
a := [3]int{1, 2, 3}
b := [3]int{3, 2, 1}
fmt.Println(a == b)//可以比较,切片不可以

结构体:结构体的所有字段都是可比较的类型,那么该结构体也是可比较的
type Student struct {
	Name string
	Age  int
}
func main() {
	a := Student{
		Name: "张三",
		Age:  18,
	}
	b := Student{
		Name: "张三",
		Age:  18,
	}
	fmt.Println(a == b)//可以比较
}

type Student2 struct {
	Name string
	Age  int
    Ret  []int //不可以比较
}

func main() {
	a := Student2{
		Name: "张三",
		Age:  18,
	}
	b := Student2{
		Name: "张三",
		Age:  18,
	}
	fmt.Println(a == b)//编译报错,不可以比较,因为存在切片,切片不可以比较
}

指针:比较是否指向同一个实例
a := make(chan int, 10)
b := a
fmt.Println(a == b) //true

a2 := make(chan int, 10)
b2 := make(chan int, 10)
fmt.Println(a2 == b2) //false

接口值: 如果两个接口值的动态类型和动态值都相等,那么它们是相等的。
func main() {
	var x interface{} = 42
	var y interface{} = 42
	fmt.Println(x == y)
}

4.1 go中哪些结构不可以比较

不可以比较:切片,Map,Function,结构体(字段包含不可比较的类型),Interface(值包含不可以比较类型)
Slice: Slice 类型是不可比较的,因为 Slice 是引用类型,其内容可以动态改变。使用 == 或 != 来比较两个 Slice 会导致编译错误
Map: Map 类型也是不可比较的,原因同 Slice。Map 是引用类型,其内容可以动态改变
Function: 函数类型是不可比较的,因为函数是不可比较的引用类型
结构体:如果一个结构体包含上述不可比较的字段(如 Slice、Map 或 Function)时,该结构体也是不可比较的
Interface:如果一个接口值的动态类型包含不可比较的字段,那么该接口值也是不可比较的

如果想要比较可以:
1.自定义比较函数: 针对不可比较的结构体类型,可以编写自定义的比较函数,在函数中实现比较逻辑。
2.使用反射: 利用 Go 语言提供的反射机制,我们可以动态地比较不可比较的类型。
eg:反射比较
func main() {
	a := []int{1, 2, 3}
	b := []int{3, 2, 1}
	fmt.Println(reflect.DeepEqual(a, b))
}

5.go协程线程安全吗

不安全,需要进行资源保护。
sync互斥锁,或者redis分布式锁

6.go中关键字

func 定义函数
interface 接口
select 监听协程
case 监听协程
defer 延迟
go 启动协程
map 字典
struct 结构体
chan 管道
goto 跳转
package 包声明
const 常量关键字
range 遍历
type 类型
import 导包
return 流程控制
var 新建变量
if 流程控制
for 流程控制
continue 流程控制
break  流程控制
else 流程控制
switch 流程控制
default 流程控制
fallthrough 强制执行switch之后的case代码

7.make和new区别

简单的说,new只分配内存,make用于slice,map,和channel的初始化。
new:
	1.new只分配内存
	2.new返回的是指向类型的指针
	3.new可以分配任意类型的数据

make:
	1.make即分配内存,也初始化内存。
	2.make返回的还是引用类型本身
	3.make分配及初始化类型为slice,map,channel的数据。

8.defer

Go语言的 defer 语句会将其后面跟随的语句进行延迟处理,在 defer 归属的函数即将返回时,将延迟处理的语句按 defer 的逆序进行执行,
也就是说:
	先被 defer 的语句最后被执行
	最后被 defer 的语句,最先被执行。

9.生产者消费者模式,手写代码

package main

import (
	"fmt"
	"sync"
	"time"
)

var wg sync.WaitGroup

func main() {
	wg.Add(2)

	ch := make(chan int)

	go func() {
		for i := 0; i < 10; i++ {
			ch <- i
		}
		close(ch)
		wg.Done()
	}()

	go func() {
		time.Sleep(time.Second*1)
		for x:=range ch{
			fmt.Println(x)
		}
		wg.Done()
	}()

	defer fmt.Println("主线程结束")
	wg.Wait() //等待
}

10.recover能处理所有的异常吗

不能。
当然是不能的,我所知道的,Go 中的错误主要有三种,一个是业务相关的错误,即 error,一种是 panic,还有一种 Go 中的一些 fatal error。
panic:让程序崩溃的错误
recover只可以处理panic错误
recover可以拦截panic。它可以是当前的程序从运行时panic的状态中恢复并重新获得流程控制权。

11.看你简历写着你了解RPC啊,那你说下RPC的整个过程?

简单的说,RPC就是从一台机器(客户端)上通过参数传递的方式调用另一台机器(服务器)上的一个函数或方法(可以统称为服务)并得到返回的结果。

1、服务消费者(client客户端)通过调用本地服务的方式调用需要消费的服务;
2、客户端存根(client stub)接收到调用请求后负责将方法、入参等信息序列化(组装)成能够进行网络传输的消息体;
3、客户端存根(client stub)找到远程的服务地址,并且将消息通过网络发送给服务端;
4、服务端存根(server stub)收到消息后进行解码(反序列化操作);
5、服务端存根(server stub)根据解码结果调用本地的服务进行相关处理;
6、本地服务执行具体业务逻辑并将处理结果返回给服务端存根(server stub);
7、服务端存根(server stub)将返回结果重新打包成消息(序列化)并通过网络发送至消费方;
8、客户端存根(client stub)接收到消息,并进行解码(反序列化);
9、服务消费方得到最终结果;

12.context包的用途

Context通常被译作上下文,它是一个比较抽象的概念,其本质,是【上下上下】存在上下层的传递,上层会把内容传递给下层。在Go语言中,程序单元也就指的是Goroutine

原理说白了就是:
  当前协程取消了,可以通知所有由它创建的子协程退出
  当前协程取消了,不会影响到创建它的父级协程的状态
  扩展了额外的功能:超时取消、定时取消、可以和子协程共享数据
这就是context包的核心原理,链式传递context,基于context构造新的context

13.client如何实现长连接

和短链接基本一样,只需要循环读取server端返回的response即可

package main

import (
        "fmt"
        "io"
        "log"
        "net/http"
)

func main() {
        request, err := http.NewRequest("GET", "http://www.example.com/", nil)
        if err != nil {
                log.Fatal(err)
        }

        http_client := &http.Client{}
        response, err := http_client.Do(request)
        if err != nil {
                log.Fatal(err)
        }

        buf := make([]byte, 4096) // any non zero value will do, try '1'.
        for {
                n, err := response.Body.Read(buf)
                if n == 0 && err != nil { // simplified
                        break
                }

                fmt.Printf("%s", buf[:n]) // no need to convert to string here
        }
        fmt.Println()
}

14.slice,len,cap,共享,扩容

append函数,因为slice底层数据结构是,由数组、len、cap组成,所以,在使用append扩容时,会查看数组后面有没有连续内存快,有就在后面添加,没有就重新生成一个大的素组

15.map如何顺序读取

map不能顺序读取,是因为他是无序的,想要有序读取,首先的解决的问题就是,把key变为有序,所以可以把key放入切片,对切片进行排序,遍历切片,通过key取值。

16.实现set集合

type inter interface{}
type Set struct {
    m map[inter]bool
    sync.RWMutex
}
  
func New() *Set {
    return &Set{
    m: map[inter]bool{},
    }
}
func (s *Set) Add(item inter) {
    s.Lock()
    defer s.Unlock()
    s.m[item] = true
}

17.实现消息队列(多生产者,多消费者)

channel+sync

package main

import (
	"fmt"
	"sync"
)

// 写
func producer(out chan<-int)  {
	for i:=0;i<10;i++{
		out <-i
	}
	close(out)
	wg.Done()
}
//读
func consumer(data <-chan int)  {
	for {
		x,ok := <-data
		if ok == false{
			wg.Done()
			return
		}
		fmt.Println(x)
	}
}

var wg sync.WaitGroup
func main() {
	ch:=make(chan int)
	wg.Add(2)
	go producer(ch)
	go consumer(ch)
	wg.Wait()
}

19.TimeWait和CloseWait原因?以及TimeWait为什么等待2MSL(120秒)?

TimeWait:表示收到了对方的FIN报文,并发送出了ACK报文
CloseWait:表示正在等待关闭

TIME_WAIT状态的存在有两个理由:
1.让4次挥手关闭流程更加可靠
2.防止丢包后对后续新建的正常连接的传输造成破坏

在Linux系统下,MSL默认值为60秒,2MSL即120秒。
MSL是Maximum Segment Lifetime英文的缩写,中文可以译为“报文最大生存时间”,他是任何报文在网络上存活的最长时间,超过这个时间报文将被丢弃。而2MSL的意思就是2倍的MSL的意思。

出现TIME_WAIT状态的连接,一定出现在主动关闭连接的一方。所以,当主动关闭连接的一方,再次向对方发起连接请求的时候(例如,客户端关闭连接,客户端再次连接服务端,此时可以复用了;负载均衡服务器,主动关闭后端的连接,当有新的HTTP请求,负载均衡服务器再次连接后端服务器,此时也可以复用),可以复用TIME_WAIT状态的连接。
等待2MSL的两个理由:
1、为了保证客户端发送的最后一个ACK报文段能够到达服务器。因为这个ACK有可能丢失,从而导致处在LAST-ACK状态的服务器收不到对FIN-ACK的确认报文。服务器会超时重传这个FIN-ACK,接着客户端再重传一次确认,重新启动时间等待计时器。最后客户端和服务器都能正常的关闭。假设客户端不等待2MSL,而是在发送完ACK之后直接释放关闭,一但这个ACK丢失的话,服务器就无法正常的进入关闭连接状态。

2、他还可以防止已失效的报文段。客户端在发送最后一个ACK之后,再经过经过2MSL,就可以使本链接持续时间内所产生的所有报文段都从网络中消失。从保证在关闭连接后不会有还在网络中滞留的报文段去骚扰服务器。

注意:在服务器发送了FIN-ACK之后,会立即启动超时重传计时器。客户端在发送最后一个ACK之后会立即启动时间等待计时器。
大家知道,由于socket是全双工的工作模式,一个socket的关闭,是需要四次握手来完成的。
主动关闭连接的一方,调用close();协议层发送FIN包
被动关闭的一方收到FIN包后,协议层回复ACK;然后被动关闭的一方,进入CLOSE_WAIT状态,主动关闭的一方等待对方关闭,则进入FIN_WAIT_2状态;此时,主动关闭的一方 等待 被动关闭一方的应用程序,调用close操作
被动关闭的一方在完成所有数据发送后,调用close()操作;此时,协议层发送FIN包给主动关闭的一方,等待对方的ACK,被动关闭的一方进入LAST_ACK状态;
主动关闭的一方收到FIN包,协议层回复ACK;此时,主动关闭连接的一方,进入TIME_WAIT状态;而被动关闭的一方,进入CLOSED状态
等待2MSL时间,主动关闭的一方,结束TIME_WAIT,进入CLOSED状态

通过上面的一次socket关闭操作,你可以得出以下几点:
1.主动关闭连接的一方 - 也就是主动调用socket的close操作的一方,最终会进入TIME_WAIT状态
2.被动关闭连接的一方,有一个中间状态,即CLOSE_WAIT,因为协议层在等待上层的应用程序,主动调用close操作后才主动关闭这条连接
3.TIME_WAIT会默认等待2MSL时间后,才最终进入CLOSED状态;
4.在一个连接没有进入CLOSED状态之前,这个连接是不能被重用的!

所以,这里凭你的直觉,TIME_WAIT并不可怕(not really,后面讲),CLOSE_WAIT才可怕,因为CLOSE_WAIT很多,表示说要么是你的应用程序写的有问题,没有合适的关闭socket;要么是说,你的服务器CPU处理不过来(CPU太忙)或者你的应用程序一直睡眠到其它地方(锁,或者文件I/O等等),你的应用程序获得不到合适的调度时间,造成你的程序没法真正的执行close操作。

造成大量CLOSE_WAIT的可能原因:
1.db链接没有正确关闭,网络连接没有正确关闭
2.db链接,或者网络连接没有复用,大量重复新建,cpu处理不过来

20.基本排序,哪些是稳定的

21.Slice与数组区别,Slice底层结构

var a [3]int  // 数组
var a []int   // 切片

切片底层依附于数组。
//总结1:当切片追加值,超过了切片容量,切片容量会翻倍,在原来容量基础上乘以2
//总结2:一旦超过了原数组, 就会重新申请数组,把数据copy到新数组,切片和原数组就没有关系了
//总结3:追加值打破了原数据的容量,就不会再影响原数据

1.18版本以前:容量小于1024翻倍,大于1024增加1/4
1.18版本:容量小于1024翻倍,大于1024cap=原切片长度+新增元素个数

22.Go的反射包怎么找到对应的方法

func Poni(o interface{}) {
	t := reflect.TypeOf(o) //获取类型
	m := t.Method(0)
	fmt.Println(m.Name) // Hello
	fmt.Println(m.Type) //func(main.Users)
}

type Users struct {
	Id   int
	Name string
	Age  int
}

// 绑方法
func (u Users) Hello() {
	fmt.Println("Hello word")
}

func main() {
	u := Users{1, "jeff", 18}
	Poni(u)

}

///////////////////
Hello
func(main.Users)

23.反射的一些方法

t:=reflect.TypeOf(o) //获取类型
v:=reflect.ValueOf(o) //获取值
t.NumField() //获取结构体字段个数
t.Field(i) //取每个字段
v.Field(i).Interface() //获取字段对应的值

v = v.Elem() // 获取指针指向的元素
fild := v.FieldByName("Name")  // 取字段
v.FieldByName("Age").SetInt(20)  //改年龄

m := v.MethodByName("Hello") // 获取方法
name:=v.Field(1).Interface() // 反射获取结构体字段对应的值
args := []reflect.Value{reflect.ValueOf(name)}  //构建参数
// 没参数的情况下:var args2 []reflect.Value
m.Call(args) // 调用方法,需要传入方法的参数

24.sync.Pool用过吗,为什么使用,对象池,避免频繁分配对象(GC有关),那里面的对象是固定的吗?

	pool就是对象缓存池,用来减少堆上内存的反复申请和释放的。因为 golang 的内存是用户触发申请,runtime 负责回收。如果用户申请内存过于频繁,会导致runtime 的回收压力陡增,从而影响整体性能。
	有了pool 之后就不一样了,对象申请先看池子里有没有现成的,有就直接返回。释放的时候内存也不是直接归还,而是放进池子而已。适时释放。
	这样就能极大的减少申请内存的频率。从而减少gc压力

25.字符串解析为数字(考虑浮点型)

k, _ := strconv.Atoi("135") // 整数到十进制
f, _ := strconv.ParseFloat("1.234", 64) // string到float64

26.io模型,同步阻塞,同步非阻塞,异步

IO操作与IO模型

27.cap理论

//总结1:当切片追加值,超过了切片容量,切片容量会翻倍,在原来容量基础上乘以2
//总结2:一旦超过了原数组, 就会重新申请数组,把数据copy到新数组,切片和原数组就没有关系了
//总结3:追加值打破了原数据的容量,就不会再影响原数据

1.18版本以前:容量小于1024翻倍,大于1024增加1/4
1.18版本:容量小于1024翻倍,大于1024cap=原切片长度+新增元素个数

28.go为什么高并发好?go的调度模型

GMP:
goroutine说到底其实就是协程,但是它比线程更小,几十个goroutine可能体现在底层就是五六个线程,Go语言内部帮你实现了这些goroutine之间的内存共享

29.怎么理解go的interface

		Go语言中的接口很特别,而且提供了难以置信的一系列灵活性和抽象性。接口是一个自定义类型,它是一组方法的集合,要有方法为接口类型就被认为是该接口。从定义上来看,接口有两个特点:

	接口本质是一种自定义类型,因此不要将Go语言中的接口简单理解为C++/Java中的接口,后者仅用于声明方法签名。
	接口是一种特殊的自定义类型,其中没有数据成员,只有方法(也可以为空)。

30.怎么理解go的空interface

		空接口(interface{})不包含任何的method,正因为如此,所有的类型都实现了interface{}。interface{}对于描述起不到任何的作用(因为它不包含任何的method),但是interface{}在我们需要存储任意类型的数值的时候相当有用,因为它可以存储任意类型的数值。它有点类似于C语言的void*类型。                                                                     

空接口,空结构体

加方法,实现channel,因为interface可以接受任意类型的参数

在Go语言中,空接口(empty interface)是指没有指定任何方法签名的接口,也就是没有方法约束的接口。在Go中,空接口被定义为interface{}。

空接口对应的实际类型可以是任何类型,因为它不强制要求实现任何方法。这使得空接口非常灵活,可以用来存储任意类型的值。

实际上,空接口在Go语言中非常常见,它们被广泛用于以下几个方面:

1.存储任意类型的值:使用空接口可以在不知道具体类型的情况下,存储任意类型的值。这在需要处理各种不同类型的数据时非常有用,比如在一个函数中接收任意类型的参数。

2.作为函数参数:函数可以接受空接口作为参数,这样函数就可以接受任意类型的参数。这在需要编写通用的函数或者库时非常有用。

3.类型断言与类型判断:通过类型断言,可以将一个空接口转换为具体的类型,然后对其进行操作。这在需要处理不同类型的值时非常有用。示例代码如下:

go
Copy
func processValue(val interface{}) {
    // 类型断言
    if str, ok := val.(string); ok {
        fmt.Println("Value is a string:", str)
    } else if num, ok := val.(int); ok {
        fmt.Println("Value is an integer:", num)
    } else {
        fmt.Println("Unknown value type")
    }
}

在上述代码中,通过类型断言判断空接口中存储的具体类型,然后根据具体类型执行相应的操作。

31.Golang 中常用的并发模型

通过channel通知实现并发控制
通过sync包中的WaitGroup实现并发控制
在Go 1.7 以后引进的强大的Context上下文,实现并发控制

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

首先JSON 标准库对 nil slice 和 空 slice 的处理是不一致.

通常错误的用法,会报数组越界的错误,因为只是声明了slice,却没有给实例化的对象。

var slice []int
slice[1] = 0
此时slice的值是nil,这种情况可以用于需要返回slice的函数,当函数出现异常的时候,保证函数依然会有nil的返回值。

empty slice 是指slice不为nil,但是slice没有值,slice的底层的空间是空的,此时的定义如下:

slice := make([]int,0)
slice := []int{}
当我们查询或者处理一个空的列表的时候,这非常有用,它会告诉我们返回的是一个列表,但是列表内没有任何值。

总之,nil slice 和 empty slice是不同的东西,需要我们加以区分的.

33.Golang的内存模型,为什么小对象多了会造成gc压力

通常小对象过多会导致GC三色法消耗过多的GPU。
解决方法:
 1.减少对象分配。使用对象池,sync.poll
 2.分配到栈,不要内存逃逸到堆。因为gc会处理堆上的数据

34.说一下异步和非阻塞的区别?

异步和非阻塞的区别:
异步:调用在发出之后,这个调用就直接返回,不管有无结果;异步是过程。
非阻塞:关注的是程序在等待调用结果(消息,返回值)时的状态,指在不能立刻得到结果之前,该调用不会阻塞当前线程。

同步和异步的区别:
同步:一个服务的完成需要依赖其他服务时,只有等待被依赖的服务完成后,才算完成,这是一种可靠的服务序列。要么成功都成功,失败都失败,服务的状态可以保持一致。
异步:一个服务的完成需要依赖其他服务时,只通知其他依赖服务开始执行,而不需要等待被依赖的服务完成,此时该服务就算完成了。被依赖的服务是否最终完成无法确定,一次它是一个不可靠的服务序列。

消息通知中的同步和异步:
同步:当一个同步调用发出后,调用者要一直等待返回消息(或者调用结果)通知后,才能进行后续的执行。
异步:当一个异步过程调用发出后,调用者不能立刻得到返回消息(结果)。在调用结束之后,通过消息回调来通知调用者是否调用成功。

阻塞与非阻塞的区别:
阻塞:阻塞调用是指调用结果返回之前,当前线程会被挂起,一直处于等待消息通知,不能够执行其他业务,函数只有在得到结果之后才会返回。
非阻塞:非阻塞和阻塞的概念相对应,指在不能立刻得到结果之前,该函数不会阻塞当前线程,而会立刻返回。
同步与异步是对应的,它们是线程之间的关系,两个线程之间要么是同步的,要么是异步的。

阻塞与非阻塞是对同一个线程来说的,在某个时刻,线程要么处于阻塞,要么处于非阻塞。

阻塞是使用同步机制的结果,非阻塞则是使用异步机制的结果。

35.高可用软件是什么?

高可用:保持其服务的高度可用性.比如mysql多主多从模式,其中一个主节点挂了没关系。其他节点立马顶上去,保证服务的高可用,mysql的哨兵是当主节点挂了,把其中一个从节点选为主节点。k8s集群也是,多个master

36.配置中心如何保证一致性?

使用k8s的configmap

37.分布式一直性原则

C数据一致性:所有应用程序都能访问到相同的数据。
A数据可用性:任何时候,任何应用程序都可以读写访问。
P分区耐受性:系统可以跨网络分区线性伸缩。(通俗来说就是数据的规模可扩展)
在大型网站中通常都是牺牲C,选择AP。为了可能减小数据不一致带来的影响,都会采取各种手段保证数据最终一致。

数据强一致:各个副本的数据在物理存储中总是一致的。
数据用户一致:数据在物理存储的各个副本可能是不一致的,但是通过纠错和校验机制,会确定一个一致的且正确的数据返回给用户。
数据最终一致:物理存储的数据可能不一致,终端用户访问也可能不一致,但是一段时间内数据会达成一致。

38.哪些数据类型可以比较,哪些不可以?

1.结构体
相同的结构体可以比较
不相同的结构体不可以比较,可以比较里面的值

2.切片
切片不可以比较

3.数组
数组只能与相同维度长度的数组比较

39.gorm有哪些坑?

1.零值不更新,使用map解决
data["stock"] = 0 //零值字段
data["price"] = 35
db.Model(Food{}).Where("id = ?", 2).Updates(data)
通过结构体变量更新字段值, gorm库会忽略零值字段。就是字段值等于0, nil, "", false这些值会被忽略掉,不会更新。如果想更新零值,可以使用map类型替代结构体

2.结构体继承了gorm.model之后,删除是软删

3.gorm查询时,可能报错未找到记录。但是可能是正常的业务逻辑。以返回的结构体!=nil来判断,不以err!=nil来判断
posted @ 2022-03-02 22:04  Jeff的技术栈  阅读(79)  评论(0编辑  收藏  举报
回顶部