2024年最全最新golang语言面试题总结(二)_person{28}(1),为了跳槽强刷1000道Golang真题

img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上Go语言开发知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

如果你需要这些资料,可以戳这里获取

ch <- 1 //这里一直阻塞,运行不到下面
<-ch
}

4. *无限死循环(infinite loops)*
5. I/O 操作上的堵塞也可能造成泄露,例如发送请求到 API 服务器,而没有使用超时;或者程序单纯地陷入死循环中。



30、Go中深拷贝和浅拷贝

一、概念:

go语言的值类型复制都是深拷贝,引用类型一般都是浅拷贝。深浅拷贝的本质,就是看拷贝内容是数据还是数据的地址。

1)深拷贝(Deep Copy):

拷贝的数据,拷贝时创建一个新对象,开辟一个新的内存空间,把原对象的数据复制过来,新对象修改数据时不会影响原对象的值。既然数据内存地址不同,释放内存地址时,需要分别释放。

2)浅拷贝(Shallow Copy):

拷贝的是数据地址,拷贝时创建一个新对象,然后复制指向的对象的指针。此时新对象和原对象指向的地址都是一样的,因此,新对象修改数组时,会影响原来对象。

注意值类型的数据拷贝默认都是深拷贝引用类型的数据拷贝一般是浅拷贝

二、演示:

示例1:深拷贝(拷贝数据)

type student struct {
	name string
	age  int
}

func main() {
	fmt.Println("演示浅拷贝,内存地址是相同的")
	stu1 := student{
		name: "深1",
		age:  1,
	}
	stu2 := stu1
	fmt.Printf("stu1的地址:%p,值:%v\n", &stu1, stu1)
	fmt.Printf("stu2的地址:%p,值:%v\n", &stu2, stu2)
	stu2.name = "深2"
	fmt.Printf("stu1的地址:%p,值:%v\n", &stu1, stu1)
	fmt.Printf("stu2的地址:%p,值:%v\n", &stu2, stu2)
}
结果:演示浅拷贝,内存地址是相同的
stu1的地址:0xc000004078,值:{深1 1}
stu2的地址:0xc000004090,值:{深1 1}
stu1的地址:0xc000004078,值:{深1 1}
stu2的地址:0xc000004090,值:{深2 1}

示例2:浅拷贝 (拷贝指针)

type student struct {
	name string
	age  int
}

func main() {
	fmt.Println("演示浅拷贝,内存地址是相同的")
	stu1 := new(student)
	{
		stu1.name = "浅1"
		stu1.age = 1
	}
	stu2 := stu1
	fmt.Printf("stu1的地址:%p,值:%v\n", stu1, stu1)
	fmt.Printf("stu2的地址:%p,值:%v\n", stu2, stu2)
	stu2.name = "浅2"
	fmt.Printf("stu1的地址:%p,值:%v\n", stu1, stu1)
	fmt.Printf("stu2的地址:%p,值:%v\n", stu2, stu2)
}
演示浅拷贝,内存地址是相同的
stu1的地址:0xc000004078,值:&{浅1 1}
stu2的地址:0xc000004078,值:&{浅1 1}
stu1的地址:0xc000004078,值:&{浅2 1}
stu2的地址:0xc000004078,值:&{浅2 1}

31、go中CAS

CAS(compare and swap)有道词典CAS(比较和交换)

  1. go中CAS操作具有原子性,在解决多线程操作共享变量安全上可以有效的减少使用锁所带来的开销,但是这是使用cpu资源做交换的
  2. go中的Cas操作与java中类似,都是借用了CPU提供的原子性指令来实现。CAS操作修改共享变量时候不需要对共享变量加锁,而是通过类似乐观锁的方式进行检查,本质还是不断的占用CPU 资源换取加锁带来的开销(比如上下文切换开销) 原子操作主要由硬件提供支持,锁一般是由操作系统提供支持,比起直接使用锁,使用CAS这个过程不需要形成临界区和创建互斥量,所以会比使用锁更加高效。

从硬件层面来实现原子操作,有两种方式:

1、总线加锁:因为CPU和其他硬件的通信都是通过总线控制的,所以可以通过在总线加LOCK#锁的方式实现原子操作,但这样会阻塞其他硬件对CPU的访问,开销比较大。

2、缓存锁定:频繁使用的内存会被处理器放进高速缓存中,那么原子操作就可以直接在处理器的高速缓存中进行而不需要使用总线锁,主要依靠缓存一致性来保证其原子性下面一个例子使用CAS来实现计数器


var counter int32 = 0

func main() {
	wg := sync.WaitGroup{}
	wg.Add(2)
	go func() {
		defer wg.Done()
		for i := 0; i < 10; i++ {
			atomic.AddInt32(&counter, 1)
		}
	}()
	go func() {
		defer wg.Done()
		for i := 0; i < 10; i++ {
			atomic.AddInt32(&counter, 1)
		}
	}()
	wg.Wait()
	fmt.Println(counter)
}
结果:20

CAS的缺陷
1.循环开销大
可以看到,方法内部用不断循环的方式实现修改。如果CAS长时间一直不成功,可能会给CPU带来很大的开销。

2.只能保证一个共享变量的原子操作
需要对多个共享变量操作时,循环CAS就无法保证操作的原子性。
解决方法:可以把多个变量放在一个对象里来进行CAS操作。

3.ABA问题
CAS需要在操作值的时候,检查值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么CAS进行检查的时候发现它的值没有发生变化,但是实质上它已经发生了改变 。可能会造成数据的缺失

32、go中被多次问到的GC

什么是GC

GC
堆内存上分配的数据对象,不会再使用时,不会自动释放内存,就变成垃圾,在程序的运行过程中,如果不能及时清理,会导致越来越多的内存空间被浪费,导致系统性能下降。

因此需要内存回收,内存回收分为两种方式

1.手动释放占用的内存空间

程序代码中也可以使用runtime.GC()来手动触发GC。这主要用于GC性能测试和统计

2.自动内存回收

(一)内存分配量达到阀值触发GC
每次内存分配时都会检查当前内存分配量是否已达到阀值,如果达到阀值则立即启动GC。
阀值 = 上次GC内存分配量 * 内存增长率
内存增长率由环境变量GOGC控制,默认为100,即每当内存扩大一倍时启动GC。

(二)定期触发GC
默认情况下,最长2分钟触发一次GC,这个间隔在src/runtime/proc.go:forcegcperiod变量中被声明:

// forcegcperiod is the maximum time in nanoseconds between garbage
// collections. If we go this long without a garbage collection, one
// is forced to run.
//
// This is a variable for testing purposes. It normally doesn't change.
var forcegcperiod int64 = 2 * 60 * 1e9

首先记住三种:

go 1.3 之前采用标记清除法,需要STW(stop the world)需要暂停用户所有操作
go 1.5 采用三色标记法,插入写屏障机制(只在堆内存中生效),最后仍需对栈内存进行STW
go 1.8 采用混合写屏障机制,屏障限制只在堆内存中生效。避免了最后节点对栈进行STW的问题,提升了GC效率

第一个阶段
STW:stop the word,指程序执行过程中,中断暂停程序逻辑,专门去进行垃圾回收。

标记清除法
把根数据段上的数据作为root,基于他们进行进一步的追踪,追踪到的数据就进行标记,最后把没有标记的对象当作垃圾进行释放。

开启STW,
从根节点出发,标记所有可达对象
停止STW,然后回收所有未标记的对象。

第二个阶段

三色标记法

其实实际应用场景是没有三色这个概念,这只是为了人们方便理解这种,抽象出来的一种说法而已,这里的三色对应的就是垃圾回收的三种状态

1、白色:初始状态下所有的对象都是白色,gcmarkBits对应的位为0(该对象会被清理)
2、灰色:对象被进行标记,但是这个对象的子对象(也就是它引用的对象)未被进行标记
3、黑色:对象被标记同时这个对象引用的子对象也被进行标记gcmarkBits对应的位为1(该对象不会被清理)

始状态下所有对象都是白色的。
接着开始扫描根对象a、b:

第三阶段

三色标记——混合屏障

因为go支持并行GC, GC的扫描和go代码可以同时运行, 这样带来的问题是GC扫描的过程中go代码有可能改变了对象的依赖树。

例如开始扫描时发现根对象A和B, B拥有C的指针。

GC先扫描A,A放入黑色
B把C的指针交给A
GC再扫描B,B放入黑色
C在白色,会回收;但是A其实引用了C。
为了避免这个问题, go在GC的标记阶段会启用写屏障(Write Barrier).

启用了写屏障(Write Barrier)后,在GC第三轮rescan阶段,根据写屏障标记将C放入灰色,防止C丢失。

33、说一下乐观锁和悲观锁在go中如何使用

乐观锁:乐观锁是一种乐观思想,即认为读多写少,遇到并发写的可能性低,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,采取在写时先读出当前版本号,然后加锁操作(比较跟上一次的版本号,如果一样则更新),如果失败则要重复读-比较-写的操作

var Rlock sync.RWMutex

func Read(wg *sync.WaitGroup) {
	defer wg.Done()
	Rlock.RLock()
	fmt.Println("开始读取")
	time.Sleep(time.Second)
	fmt.Println("读取成功")
	Rlock.RUnlock()
}
func main1() {

	var wg = sync.WaitGroup{}
	wg.Add(6)
	for i := 0; i < 5; i++ {
		go Read1(&wg)
	}
	go Write1(&wg)
	wg.Wait()
}

**悲观锁:**悲观锁是就是悲观思想,即认为写多,遇到并发写的可能性高,每次去拿数据的时候都认为别人会修改,所以每次在读写数据的时候都会上锁,这样别人想读写这个数据就会block直到拿到锁。

func Write1(wg *sync.WaitGroup) {
	defer wg.Done()
	Rlock.Lock()
	fmt.Println("开始写")
	time.Sleep(time.Second * 10)
	fmt.Println("写入成功")
	Rlock.Unlock()
}

func main1() {

	var wg = sync.WaitGroup{}
	wg.Add(6)
	for i := 0; i < 5; i++ {
		go Read1(&wg)
	}
	go Write1(&wg)
	wg.Wait()
}

自旋锁
自旋锁原理非常简单,如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞挂起状态,它们只需要等一等(自旋),等持有锁的线程释放锁后即可立即获取锁,这样就避免用户线程和内核的切换的消耗。线程自旋是需要消耗 cup 的,说白了就是让 cup 在做无用功,如果一直获取不到锁,那线程也不能一直占用 cup 自旋做无用功,所以需要设定一个自旋等待的最大时间。
如果持有锁的线程执行的时间超过自旋等待的最大时间扔没有释放锁,就会导致其它争用锁的线程在最大等待时间内还是获取不到锁,这时争用线程会停止自旋进入阻塞状态。

34、面试遇到的题总结

1、下面代码,有几次错误
func main() {
	var x string = nil
	if x == nil {
		x = "default"
	}
	fmt.Println(x)
}

答:两次 var x string = nil和 if x == nil 

2、下面代码段输出什么?

type Person struct {
    age int
}

func main() {
    person := &Person{28}

    // 1. 
    defer fmt.Println(person.age)

    // 2.
    defer func(p *Person) {
        fmt.Println(p.age)
    }(person)  

    // 3.
    defer func() {
        fmt.Println(person.age)
    }()

    person.age = 29
}
答:29 29 28

3、下面这段代码输出什么?为什么?
type People interface {
    Speak(string) string
}

type Student struct{}

func (stu *Student) Speak(think string) (talk string) {
    if think == "speak" {
        talk = "speak"
    } else {
        talk = "hi"
    }
    return
}

func main() {
    var peo People = Student{}
    think := "speak"
    fmt.Println(peo.Speak(think))
}
答:编译错误 Student does not implement People (Speak method has pointer receiver),值类型 Student 没有实现接口的 Speak() 方法,而是指针类型 *Student 实现该方法。

4、下面输出什么?
type Student struct {
	Name string
}

func main() {
	fmt.Println(&Student{Name: "xxx"} == &Student{Name: "xxx"})
	fmt.Println(Student{Name: "xxx"} == Student{Name: "xxx"})
}
答:指针是取地址
false
true
5、下面代码会报错吗?
func main() {
	fmt.Println([...]string{"1"} == [...]string{"1"})
	fmt.Println([]string{"1"} == []string{"1"})
}
答:数组只能与相同纬度⻓度以及类型的其他数组⽐较,切⽚之间不能直接⽐较。

35、面试中被问到WebSocket与Socket、TCP、HTTP的关系及区别

1.什么是WebSocket及原理

WebSocket是HTML5中新协议、新API。 WebSocket从满足基于Web的日益增长的实时通信需求应运而生,解决了客户端发起多个Http请求到服务器资源浏览器必须要在经过长时间的轮询问题,实现里多路复用,是全双工、双向、单套接字连接,在WebSocket协议下服务器和客户端可以同时发送信息。

原理:

WebSocket 同 HTTP 一样也是应用层的协议,但是它是一种双向通信协议,是建立在 TCP 之上的。

2.理解各种协议和通信层、套接字的含义

IP:网络层协议;(高速公路)

TCP和UDP:传输层协议;(卡车)

HTTP:应用层协议;(货物)。HTTP(超文本传输协议)是建立在TCP协议之上的一种应用。HTTP连接最显著的特点是客户端发送的每次请求都需要服务器回送响应,在请求结束后,会主动释放连接。从建立连接到关闭连接的过程称为“一次连接”。

SOCKET:套接字,TCP/IP网络的API。(港口码头/车站)Socket是应用层与TCP/IP协议族通信的中间软件抽象层,它是一组接口。socket是在应用层和传输层之间的一个抽象层,它把TCP/IP层复杂的操作抽象为几个简单的接口供应用层调用已实现进程在网络中通信。

Websocket:同HTTP一样也是应用层的协议,但是它是一种双向通信协议,是建立在TCP之上的,解决了服务器与客户端全双工通信的问题,包含两部分:一部分是“握手”,一部分是“数据传输”。握手成功后,数据就直接从 TCP 通道传输,与 HTTP 无关了。

*注:什么是单工、半双工、全工通信?
数据只能单向传送为单工;
数据能双向传送但不能同时双向传送称为半双工;
数据能够同时双向传送则称为全双工。

TCP/UDP区别:
TCP(传输控制协议,Transmission Control Protocol):(类似打电话)
面向连接、传输可靠(保证数据正确性)、有序(保证数据顺序)、传输大量数据(流模式)、速度慢、对系统资源的要求多,程序结构较复杂,
每一条TCP连接只能是点到点的,
TCP首部开销20字节。
UDP(用户数据报协议,User Data Protocol):(类似发短信)
面向非连接 、传输不可靠(可能丢包)、无序、传输少量数据(数据报模式)、速度快,对系统资源的要求少,程序结构较简单 ,
UDP支持一对一,一对多,多对一和多对多的交互通信,
UDP的首部开销小,只有8个字节。简化的TCP/IP四层模型主要分为:应用层、传输层、网络层、数据链路层。

3.WebSocket和Http的关系和异同点

每个WebSocket连接都始于一个HTTP请求。 具体来说,WebSocket协议在第一次握手连接时,通过HTTP协议在传送WebSocket支持的版本号,协议的字版本号,原始地址,主机地址等等一些列字段给服务器端.

img
img

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

d6ea94e249e.png)

3.WebSocket和Http的关系和异同点

每个WebSocket连接都始于一个HTTP请求。 具体来说,WebSocket协议在第一次握手连接时,通过HTTP协议在传送WebSocket支持的版本号,协议的字版本号,原始地址,主机地址等等一些列字段给服务器端.

[外链图片转存中…(img-dymqMPXW-1715408312177)]
[外链图片转存中…(img-D89uTraz-1715408312177)]

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加戳这里获取

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值