Golang笔记_day08

Go面试题(一)

1、空切片 和 nil 切片 区别

空切片

     空切片是指长度和容量都为0的切片。它不包含任何元素,但仍然具有切片的容量属性。在Go语言中,可以使用内置的make函数创建一个空切片,例如:

emptySlice := make([]int)

     这个语句创建了一个长度为0、容量为0的空切片。需要注意的是,空切片与nil切片不同,它具有容量属性,即分配了内存空间

用途:可以使用空切片作为初始值或者作为函数的返回值,注意扩容开销

nil切片

      nil切片是指长度和容量都为0的切片,并且没有指向任何底层数组。在Go语言中,可以使用内置的make函数创建一个长度和容量都为0的切片,并将其值赋给一个nil切片的变量,例如:

nilSlice := make([]int)
nilSlice = nil

     在这个例子中,nilSlice最初被赋值为一个长度和容量都为0的切片。然后,我们将其值设置为nil,使其成为nil切片。需要注意的是,nil切片不具有容量属性,即没有分配内存空间。

用途:表示一个没有值的切片,将变量赋值为nil可以清除其原有的值和容量信息

    总结:使用空切片时需要注意容器的扩容开销;使用nil切片时需要注意长度和容量的初始化问题。通过正确地理解和使用这两种切片类型。

2、字符串转成 byte 数组,会发生内存拷贝吗

    在 Go 语言中,字符串是不可变的字节序列,而字节数组是可变的字节序列。当将字符串转换为字节数组时,会发生内存拷贝。

   需要注意的是,由于发生了内存拷贝,所以在将字符串转换为字节数组时会产生额外的内存开销。在处理大型字符串时,这可能会对性能和内存利用率产生一定的影响,特别是在频繁转换的情况下。因此,在性能敏感的场景中,需要谨慎使用字符串到字节数组的转换,并根据实际需求进行优化。

3、拷贝大切片一定比小切片代价大吗

     并不是,所有切片的大小相同;三个字段(一个 uintptr,两个int)。切片中的第一个字是指向切片底层数组的指针,这是切片的存储空间,第二个字段是切片的长度,第三个字段是容量。将一个 slice 变量分配给另一个变量只会复制三个机器字。所以 拷贝大切片跟小切片的代价应该是一样的。

解释
    SliceHeader是切片在go的底层结构。

type SliceHeader struct {Data uintptrLen  intCap  int
}

       大切片跟小切片的区别无非就是 Len和 Cap的值比小切片的这两个值大一些,如果发生拷贝,本质上就是拷贝上面的三个字段。

4、空map和未初始化map区别

        可以对未初始化的map进行取值,但取出来的东西是空:

var m1 map[string]string
fmt.Println(m1["1"])

        不能对未初始化的map进行赋值,这样将会抛出一个异常:panic: assignment to entry in nil map

var m1 map[string]string
m1["1"] = "1"

        通过fmt打印map时,空map和nil map结果是一样的,都为map[]。所以,这个时候别断定map是空还是nil,而应该通过map == nil来判断。

5、map最大容量

        在Go语言的标准库中,map是一种常用的数据结构。Map的特点是无序的键值对,其中键是唯一的。在Golang中,map的最大容量是由无符号整数uint32类型表示的,在Go语言中为2^31-1,这使得map的容量达到了非常惊人的程度。

6、map 的 iterator 是否安全  能不能一边 delete 一边遍历 

        map 并不是一个线程安全的数据结构。同时读写一个 map 是未定义的行为,如果被检测到,会直接 panic。

        上面说的是发生在多个协程同时读写同一个 map 的情况下。 如果在同一个协程内边遍历边删除,并不会检测到同时读写,理论上是可以这样做的。但是,遍历的结果就可能不会是相同的了,有可能结果遍历结果集中包含了删除的 key,也有可能不包含,这取决于删除 key 的时间:是在遍历到 key 所在的 bucket 时刻前或者后。

一般而言,这可以通过读写锁来解决:sync.RWMutex

        读之前调用 RLock() 函数,读完之后调用 RUnlock() 函数解锁;写之前调用 Lock() 函数,写完之后,调用 Unlock() 解锁。

        另外,sync.Map 是线程安全的 map,也可以使用。

7、检查一个给定的数组是否被排序

冒泡排序

package main
import "fmt"
func checkSortedArray(arr []int){sortedArray := truefor i:=0; i<=len(arr)-1; i++{for j:=0; j<len(arr)-1-i; j++{if arr[j]> arr[j+1]{sortedArray = falsebreak}}}if sortedArray{fmt.Println("Given array is already sorted.")} else {fmt.Println("Given array is not sorted.")}
}func main(){checkSortedArray([]int{1, 3, 5, 6, 7, 8})checkSortedArray([]int{1, 3, 5, 9, 4, 2})checkSortedArray([]int{9, 7, 4, 2, 1, -1})
}

8、深拷贝和浅拷贝比较

        深拷贝和浅拷贝是编程中处理对象或数据结构复制时的两种主要策略。理解它们之间的基本概念和差异对于避免潜在的数据共享和修改冲突至关重要。        

(1)、定义:

浅拷贝 是对对象的表面层次的复制。它创建一个新的对象,并复制原始对象的所有非引用类型字段的值。然而,对于引用类型的字段(如切片、映射、通道、接口和指向结构体或数组的指针),浅拷贝仅仅复制了引用的地址,而非引用的实际内容。这意味着新对象和原始对象共享相同的引用类型字段的数据

深拷贝 深拷贝则是对对象的完全复制,包括对象引用的其他对象。它递归地遍历原始对象的所有字段,并创建新的内存空间来存储这些字段的值,包括引用类型字段所指向的实际数据。这样,深拷贝后的对象与原始对象在内存中是完全独立的,对其中一个对象的修改不会影响另一个对象。

(2)、区别

主要区别在于它们处理引用类型字段的方式

浅拷贝仅仅复制了引用的地址,因此新对象和原始对象共享相同的数据

深拷贝则创建了新的内存空间来存储引用类型字段的数据,确保新对象与原始对象完全独立

深拷贝需要递归地复制对象的所有字段,包括引用的其他对象,因此它通常比浅拷贝更加耗时和消耗内存,浅拷贝则更加高效;

(3)、比较

        为什么需要浅拷贝:性能更好  内存使用更少   共享状态

        为什么需要深拷贝:独立性   生命周期管理   避免内存泄漏  数据安全性

9、slice 深拷贝和浅拷贝

        slice 有[]T{}new 、make三种声明方式;

        slice 会在变量赋值时发生浅复制;

        copy() 可以让 slice 进行深复制;

        append 再操作切片时,切片空闲容量不足时会发生扩容。

10、几种深度拷贝(deepcopy)方法的性能对比

        Go语言中所有赋值操作都是值传递,如果结构中不含指针,则直接赋值就是深度拷贝;

        如果结构中含有指针(包括自定义指针,以及切片,map等使用了指针的内置类型),则数据源和拷贝之间对应指针会共同指向同一块内存,这时深度拷贝需要特别处理。

目前,有三种方法(在性能要求较高的情况下应该尽量避免使用前两者)

一是用gob序列化成字节序列再反序列化生成克隆对象;

二是先转换成json字节序列,再解析字节序列生成克隆对象;

三是针对具体情况,定制化拷贝。

11、map 扩容机制

(1)、概述

        Map在编程过程中往往需要存储大量的键值对数据。当存储的键值对数量超过了map的初始容量时,map就会自动进行扩容。扩容是指当map达到一定的负载因子时,系统自动重新分配更大的内存,并将原有的键值对重新映射到新的内存空间上。这样可以避免因为数据过多而导致的性能下降。

(2)、Map扩容的触发条件

在Golang中,map的扩容是基于两个主要的触发条件:

  1. 当map存储的键值对数量超过了当前map的容量(cap),即loadFactor * cap。
  2. 当插入新的键值对到map中,而当前map的创建时间距离上一次扩容的时间小于2个tick。

     loadFactor是指map目前已存储键值对数量与当前容量之间的比例。当键值对数量超过这个比例时,就会触发map的扩容。这个比例在Golang中默认为6.5,即map的键值对数量超过容量的6.5倍时,就会触发扩容。

(3)、Map扩容的过程

当触发了map的扩容条件后,Golang会进行以下操作:

  1. 计算新的容量,并分配新的内存空间。
  2. 将原有的键值对重新映射到新的内存空间上。
  3. 释放原有的内存空间。

        具体来说,当map需要进行扩容时,会先根据当前map容量(cap)和键值对数量计算出新的容量(newCap)。然后,根据新的容量(newCap)分配新的内存空间,将原有的键值对重新映射到新的内存空间上。最后,释放原有的内存空间。

(4)、Map扩容对性能的影响

map的扩容机制虽然可以在存储大量键值对时保证性能的稳定,但是扩容过程本身是需要耗费时间和内存的。因此,在编写程序时,我们应尽量避免频繁地对map进行扩容操作,以提高程序的性能。

(5)、Map扩容的发生时机

        在Golang中,map的扩容是非确定性的,即我们无法精确控制map的扩容时机。Golang会根据map的使用情况和当前存储的键值对数量来决定是否扩容。

        在某些特殊情况下,我们可以通过手动触发map的扩容来控制扩容时机。可以通过向map插入一个空结构体或nil值来触发扩容。当然,这种做法需要谨慎使用,必要时才进行手动扩容,以避免不必要的性能开销。

(6)、扩容策略

        map扩容时使用渐进式扩容:翻倍扩容   等量扩容

        翻倍扩容

        count/(2^B) > 6.5:当负载因子超过6.5时就会触发翻倍扩容。

        等量扩容

        虽然没有超过负载因子限制,但是使用溢出桶过多,就会触发等量扩容,创建和旧桶数目一样多的新桶,然后把原来的键值对迁移到新桶中。

12、array 和 slice 的区别

(1)、数组(Array)
数组是Go语言中的基础数据结构,用于存储固定数量的同一类型的元素。数组的长度是数组类型的一部分,因此 [5]int 和 [10]int 是不同的类型。一旦定义,数组的长度就不能改变。

示例代码
var arr [5]int // 声明一个长度为5的整数数组
arr[0] = 1     // 给第一个元素赋值
arr[1] = 2     // 给第二个元素赋值
// ...

数组的缺点
固定长度:一旦声明,数组的长度就不能改变,这限制了其灵活性。
不便的传递:当你需要将数组作为参数传递给函数时,你会传递数组的副本,这可能会导致性能问题,特别是当数组很大时。
(2)、切片(Slice)
    切片是对数组的抽象,提供了动态大小的、灵活的、可变的序列。切片本身并不存储数据,而是描述了一个底层数组的一部分(或全部)。切片有一个长度和一个容量,长度是切片当前包含的元素数量,容量是底层数组从切片起始位置到数组末尾的元素数量。

示例代码
arr := [5]int{1, 2, 3, 4, 5} // 声明一个长度为5的整数数组
slice := arr[1:4]             // 从数组arr中创建一个切片,包含元素2, 3, 4

切片的优点
    动态大小:切片的长度可以在运行时改变,使其比数组更加灵活。
    引用传递:当切片作为参数传递给函数时,传递的是对底层数组的引用,而不是数组的副本,这可以提高性能。
    更方便的操作:Go语言标准库提供了许多内置函数和操作符来操作切片,使得对切片进行排序、搜索等操作变得更加容易。
(3)、场景

        数组适用于那些确实需要固定大小序列的场景,比如算法竞赛中的静态数组

        切片则更适用于那些需要动态大小序列的场景,比如处理用户输入的数据或构建复杂的数据结构。由于切片本身只是一个小的数据结构,包含指向底层数组的指针、长度和容量,因此传递切片实际上是非常高效的。

13、make 和 new 什么区别

   (1)make 

       是一个用于创建切片、映射(map)和通道(channel)的引用类型的内置函数。make 的主要作用是为这些引用类型分配内存并进行初始化。

        创建切片(slice)

        创建映射(map)

        创建通道(channel)

注意事项:

        make 只能用于引用类型的数据结构,不能用于值类型(例如结构体)的创建。

        make 返回被初始化的引用类型实例,而不是指针。

         对于切片和映射,make 除了分配内存,还会初始化内部的数据结构,确保它们可以被直接使用。

         对于通道,make 会创建并返回一个未被缓冲的通道。      

   (2)new         

      是一个用于为值类型分配内存并返回指向新分配的零值实例的指针的内置函数。new 主要用于创建值类型的实例,例如结构体创建值类型实例创建结构体实例

注意事项:
        new 返回一个指向新分配内存的零值实例的指针。
        对于值类型,new 分配的内存会被初始化为零值。
        new 接受一个参数,即要分配内存的类型,并返回一个指向该类型的零值的指针。
        new 不适用于引用类型(如切片、映射和通道),只能用于值类型的创建。
        new 分配的内存不会被清理,需要程序员负责释放。

14、for 循环select时,通道已经关闭会怎样  如果select中的case只有一个,会怎样

        for循环select时,如果其中一个case通道已经关闭,则每次都会执行到这个case。

        如果select里边只有一个case,而这个case被关闭了,则会出现死循环

        如果没有default字句,select将有可能阻塞,直到某个通道有值可以运行,所以select里最好有一个default,否则将有一直阻塞的风险。

15、如何避免内存逃逸

      内存逃逸是指原本应该在栈上分配的内存被分配到了堆上。这意味着即使函数返回后,这部分内存也不会被自动释放,需要等待垃圾回收器来回收。

package mainimport "fmt"type User struct {Name string
}func main() {var user *Useruser = getUser()fmt.Println(user.Name)
}func getUser() *User {u := User{Name: "Alice"}return &u
}

      getUser 函数创建了一个 User 类型的局部变量 u,并返回了它的地址。由于 u 的引用在函数外部被使用(即在 `main` 函数中),所以会发生逃逸

如何避免内存逃逸

  • 严格限制变量的作用域。如果一个变量只在函数内部使用,就不要将其返回或赋值给外部变量。
  • 使用值而不是指针,当不必要的时候,尽量使用值传递而不是指针传递。
  • 池化对象,对于频繁创建和销毁的对象,考虑使用对象池技术进行复用,减少在堆上分配和回收对象的次数。
  • 尽量避免在循环或频繁调用的函数中创建闭包,以减少外部变量的引用和堆分配,避免使用不必要的闭包,闭包可能会导致内存逃逸。
  • 优化数据结构,使用固定大小的数据结构,避免使用动态大小的切片和 map。比如使用数组而不是切片,因为数组的大小在编译时就已确定。
  • 预分配切片和 map 的容量,如果知道切片或 map 的大小,预先分配足够的容量可以避免在运行时重新分配内存。

16、内存泄漏的原因和处理方法

        即使有垃圾回收机制,但在编写Go程序时仍然可能发生内存泄漏。内存泄漏是指程序中不再使用的内存没有被正确释放,最终导致内存占用过高。下面是一些常见的导致内存泄漏的原因以及相应的处理方法:

 循环引用

        循环引用指的是两个或多个对象之间相互引用,导致它们无法被垃圾回收器正确地回收。为了解决循环引用导致的内存泄漏,可以使用弱引用(Weak Reference)来替代强引用(Strong Reference),或者手动将其中一个对象的引用置为空。

忘记关闭文件或网络连接

        在使用文件或网络资源时,如果忘记关闭这些资源,会导致文件描述符或网络连接句柄没有被释放,最终导致内存泄漏。为了避免这种情况发生,可以使用defer语句或者io.Closer接口来确保资源的正确关闭。

大量创建临时对象

        在循环中大量创建临时对象,并未及时释放,会导致内存占用过高。为了避免这种情况,可以通过复用对象或者使用对象池来减少对象的创建和销毁次数。

 Goroutine泄漏

        如果Goroutine在执行完毕后没有正确退出,会导致Goroutine所占用的资源无法释放,从而引起内存泄漏。为了避免这种情况发生,可以使用sync.WaitGroup来等待所有Goroutine执行完毕,或者使用context.Context来控制Goroutine的生命周期。

 最佳实践

以下是一些使用Go语言进行内存管理的最佳实践:

  • 避免不必要的内存分配,尽量复用对象或者使用对象池。
  • 及时释放不再使用的资源,如文件、网络连接等。
  • 避免循环引用导致的内存泄漏,及时将无用对象置为空。
  • 使用defer语句或者io.Closer接口来确保资源的正确关闭。
  • 使用sync.WaitGroup等待所有Goroutine执行完毕,避免Goroutine泄漏。

17、简单介绍sync.Pool使用场景

       sync.Pool 是 Golang 内置的对象池技术,可用于缓存临时对象,以缓解因频繁建立临时对象带来的性能损耗以及对 GC 带来的压力。

但sync.Pool 缓存的对象随时可能被无通知的清除,因此不能将 sync.Pool 用于存储持久对象的场景。

        所有sync.Pool的缓存对象数量是没有限制的(只受限于内存),因此使用sync.pool是没办法做到控制缓存对象数量的个数的。

        sync.Pool 本质用途是增加临时对象的重用率,减少 GC 负担。划重点:临时对象。所以说,像 socket 这种带状态的,长期有效的资源是不适合 Pool 的。

总结:

  1. sync.Pool 本质用途是增加临时对象的重用率,减少 GC 负担;

  2. 不能对 Pool.Get 出来的对象做预判,有可能是新的(新分配的),有可能是旧的(之前人用过,然后 Put 进去的);

  3. 不能对 Pool 池里的元素个数做假定,你不能够;

  4. sync.Pool 本身的 Get, Put 调用是并发安全的,sync.New 指向的初始化函数会并发调用,里面安不安全只有自己知道;

  5. 当用完一个从 Pool 取出的实例时候,一定要记得调用 Put,否则 Pool 无法复用这个实例,通常这个用 defer 完成;

18、sync.map 的优缺点和使用场景

  • 优点:Go 官方所出;通过读写分离,降低锁时间来提高效率;线程安全的map;

  • 缺点:不适用于大量写的场景,这样会导致 read map 读不到数据而进一步加锁读取,同时 dirty map 也会一直晋升为 read map,整体性能较差,甚至没有单纯的 map+metux 高。

        适用场景:读多写少的场景。

        通过这种读写分离的设计,解决了并发场景下的写入安全,又使读取速度在大部分情况可以接近内建 map,非常适合读多写少的情况

19、uintptr和unsafe.Pointer的区别

  • unsafe.Pointer只是单纯的通用指针类型,用于转换不同类型指针,它不可以参与指针运算;
  • 而uintptr是用于指针运算的,GC 不把 uintptr 当指针,也就是说 uintptr 无法持有对象, uintptr 类型的目标会被回收;
  • unsafe.Pointer 可以和 普通指针 进行相互转换;
  • unsafe.Pointer 可以和 uintptr 进行相互转换。

案例

package mainimport ("fmt""unsafe"
)type W struct {b int32c int64
}func main() {var w *W = new(W)//这时w的变量打印出来都是默认值0,0fmt.Println(w.b,w.c)//现在我们通过指针运算给b变量赋值为10b := unsafe.Pointer(uintptr(unsafe.Pointer(w)) + unsafe.Offsetof(w.b))*((*int)(b)) = 10//此时结果就变成了10,0fmt.Println(w.b,w.c)
}
  • uintptr(unsafe.Pointer(w)) 获取了 w 的指针起始值
  • unsafe.Offsetof(w.b) 获取 b 变量的偏移量
  • 两个相加就得到了 b 的地址值,将通用指针 Pointer 转换成具体指针 ((*int)(b)),通过 * 符号取值,然后赋值。*((*int)(b)) 相当于把 (*int)(b) 转换成 int 了,最后对变量重新赋值成 10,这样指针运算就完成了。

20 、协程和线程的差别

        进程:进程是操作系统对一个正在运行的程序的一种抽象,进程是资源分配的最小单位;进程就是应用程序的启动实例。比如我们运行一个游戏,打开一个软件,就是开启了一个进程。

        线程:线程从属于进程,是程序的实际执行者。一个进程至少包含一个主线程,也可以有更多的子线程。多线程比多进程之间更容易共享数据,在上下文切换中线程一般比进程更高效

        协程:是用户态的线程。通常创建协程时,会从进程的堆中分配一段内存作为协程的栈, 是一种比线程更加轻量级的存在。正如一个进程可以拥有多个线程一样,一个线程也可以拥有多个协程。最重要的是,协程不是被操作系统内核所管理,而完全是由程序所控制(也就是在用户态中执行)。

        线程的栈有 8 MB,而协程栈的大小通常只有 KB,而 Go 语言的协程更夸张,只有 2-4KB,非常的轻巧。

协程的优势如下

        节省 CPU:避免系统内核级的线程频繁切换,造成的 CPU 资源浪费。好钢用在刀刃上。而协程是用户态的线程,用户可以自行控制协程的创建于销毁,极大程度避免了系统级线程上下文切换造成的资源浪费。

        节约内存:在 64 位的Linux中,一个线程需要分配 8MB 栈内存和 64MB 堆内存,系统内存的制约导致我们无法开启更多线程实现高并发。而在协程编程模式下,可以轻松有十几万协程,这是线程无法比拟的。

        稳定性:前面提到线程之间通过内存来共享数据,这也导致了一个问题,任何一个线程出错时,进程中的所有线程都会跟着一起崩溃。

        开发效率:使用协程在开发程序之中,可以很方便的将一些耗时的IO操作异步化,例如写文件、耗时 IO 请求等。

        协程本质上就是用户态下的线程,所以也有人说协程是 “轻线程”,但我们一定要区分用户态和内核态的区别,很关键。

21、开源库里会有一些类似下面这种奇怪的用法:var _ io.Writer = (*myWriter)(nil),是为什么?

        上述赋值语句会发生隐式地类型转换,在转换的过程中,编译器会检测等号右边的类型是否实现了等号左边接口所规定的函数。

        总结一下,可通过在代码中添加类似如下的代码,用来检测类型是否实现了接口:

package mainimport "io"type myWriter struct {}/*func (w myWriter) Write(p []byte) (n int, err error) {return
}*/func main() {// 检查 *myWriter 类型是否实现了 io.Writer 接口var _ io.Writer = (*myWriter)(nil)// 检查 myWriter 类型是否实现了 io.Writer 接口var _ io.Writer = myWriter{}
}

注释掉为 myWriter 定义的 Write 函数后,运行程序:

src/main.go:14:6: cannot use (*myWriter)(nil) (type *myWriter) as type io.Writer in assignment:*myWriter does not implement io.Writer (missing Write method)
src/main.go:15:6: cannot use myWriter literal (type myWriter) as type io.Writer in assignment:myWriter does not implement io.Writer (missing Write method)

报错信息:*myWriter/myWriter 未实现 io.Writer 接口,也就是未实现 Write 方法。

解除注释后,运行程序不报错。

22、协程之间是怎么调度的

要点:GMP模型

Gorutine从入队到执行

(1)当我们创建一个G对象,就是 gorutine,它会加入到本地队列或者全局队列
(2)如果还有空闲的P,则创建一个M 绑定该 P ,注意!这里,P 此前必须还没绑定过M 的,否则不满足空闲的条件。细节点:
        先找到一个空闲的P,如果没有则直接返回
        P 个数不会占用超过自己设定的cpu个数
        P 在被 M 绑定后,就会初始化自己的 G 队列,此时是一个空队列
        注意这里的一个点!
                无论在哪个 M 中创建了一个 G,只要 P 有空闲的,就会引起新 M 的创建
                不需考虑当前所在 M 中所绑的 P 的 G 队列是否已满
                新创建的 M 所绑的 P 的初始化队列会从其他 G 队列中取任务过来

这里留下第一个问题:
如果一个G任务执行时间太长,它就会一直占用 M 线程,由于队列的G任务是顺序执行的,其它G任务就会阻塞,如何避免该情况发生?

--协程的切换时间片是10ms,也就是说 goroutine 最多执行10ms就会被 M 切换到下一个 G。这个过程,又被称为 中断,挂起

(3)M 会启动一个底层线程,循环执行能找到的 G 任务。这里的寻找的 G 从下面几方面找:
        当前 M 所绑的 P 队列中找
        去别的 P 的队列中找
        去全局 G 队列中找
(4)G任务的执行顺序是,先从本地队列找,本地没有则从全局队列找
(5)程序启动的时候,首先跑的是主线程,然后这个主线程会绑定第一个 P
(6)入口 main 函数,其实是作为一个 goroutine 来执行。

23、gc 的 stw 是怎么回事

     停止-世界(Stop-The-World, STW): 停止-世界是垃圾回收过程中的一种情况,此时程序的所有正常执行都会被暂停,以便垃圾回收器能够执行,比如标记(marking)和清除(sweeping)内存中的不再使用的对象。STW事件的时间越短,对用户体验的影响就越小,尤其是在交互式应用和实时系统中。

       Go语言中的垃圾回收: Go语言设计了一个并发的、低延迟的垃圾回收器,其特点是在尽可能不影响程序运行的情况下,执行内存的垃圾回收。Go的GC设计选择了牺牲一部分吞吐量来换取更短的STW时间。这意味着Go的垃圾回收器可能比那些优化了吞吐量的回收器更频繁地运行,但每次停止程序的时间非常短,因此用户几乎感觉不到延迟。

   Golang Gc回收算法:三色标记算法+混合写屏障机制

24、两个interface能否比较

        在Go语言中,接口是一种引用类型。如果我们需要比较两个接口对象是否相等,实际上是在比较它们指向内存区域的地址。如果两个接口指向的对象在内存中的地址相同,那么它们就是相等的。否则,它们是不相等的。

因此,在Go语言中,两个接口可以进行比较,但实际上比较的是它们指向的内存地址。例如:

var a interface{} = "hello"
var b interface{} = "hello"fmt.Println(a == b) // 输出 false

        在上述代码中,我们声明了两个interface{}类型的变量a和b,它们都指向同一个字符串对象"hello"。尽管它们指向的对象相同,但由于它们指向的内存地址不同,所以在比较时会返回false。

注意事项:

        在比较两个接口对象时,我们需要注意以下几点:

        (1).接口比较实际上是比较它们指向的内存地址,而不是比较它们的值。

        (2).如果我们需要比较接口对象的值,可以在接口类型中定义一个Equal方法,根据具体的情况进行比较。

        (3).在实现Equal方法时,需要将other参数转换为具体的类型,并逐个比较属性值。如果other不能转换为当前类型,应该返回false。

25、必须要手动对齐内存的情况

        Go 语言内存对齐机制是为了优化内存访问和提高性能而设计的。为了能让CPU可以更快的存取到各个字段,Go编译器会帮你把struct结构体做数据的对齐。所谓的数据对齐,是指内存地址是所存储数据大小(按字节为单位)的整数倍,以便CPU可以一次将该数据从内存中读取出来。编译器通过在结构体的各个字段之间填充一些空白已达到对齐的目的。

        绝大部分情况下,go编译器会帮我们自动内存对齐,我们不需要关心内存是否对齐,但是在有一种情况下,需要手动对齐。

        在 x86 平台上原子操作 64bit 指针。之所以要强制对齐,是因为在 32bit 平台下进行 64bit 原子操作要求必须 8 字节对齐,否则程序会 panic。

type T3 struct {b int64c int32d int64
}func main() {a := T3{}atomic.AddInt64(&a.d, 1)
}

        原因就是 T3 在 32bit 平台上是 4 字节对齐,而在 64bit 平台上是 8 字节对齐。在 64bit 平台上其内存布局为:
Figure 4: T3在 amd64 的内存布局

但是在I386 的布局为:
Figure 5: T3在 i386的内存布局

        为了解决这种情况,我们必须手动 padding T3,让其 “看起来” 像是 8 字节对齐的:

type T3 struct {b int64c int32_ int32d int64
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/451497.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【思维导图】C语言—常见概念

hello&#xff0c;友友们&#xff0c;今天我们进入一个新的专栏——思维导图&#xff01; 思维导图帮助我们复习知识的同时建构出一个清晰的框架&#xff0c;我往后会不断更新各个专栏的思维导图&#xff0c;关注我&#xff0c;一起加油&#xff01; 今天我们回顾C语言中的常见…

【C++贪心】2712. 使所有字符相等的最小成本|1791

本文涉及知识点 C贪心 LeetCode2712. 使所有字符相等的最小成本 给你一个下标从 0 开始、长度为 n 的二进制字符串 s &#xff0c;你可以对其执行两种操作&#xff1a; 选中一个下标 i 并且反转从下标 0 到下标 i&#xff08;包括下标 0 和下标 i &#xff09;的所有字符&am…

【从零到一的笔试突破】——day1笔试巅峰(6道笔试题)ACM模式让笔试更有感觉

文章目录 数字统计&#xff08;数学模拟&#xff09;两个数组的交集&#xff08;哈希&#xff09;点击消除&#xff08;栈&#xff09;牛牛的快递&#xff08;模拟&#xff09;最小花费爬楼梯&#xff08;动态规划&#xff09;数组中两个字符串的最小距离&#xff08;滑动窗口o…

开放式蓝牙耳机排行榜第一名是哪款?推荐五款热门开放式耳机!

​在当今的耳机市场上&#xff0c;开放式耳机因其时尚的外观和舒适的佩戴体验&#xff0c;已经成为广受欢迎的日常选择。然而&#xff0c;面对众多品牌和参差不齐的质量&#xff0c;选择一款合适的开放式耳机确实让人头疼。作为一名拥有三年耳机评测经验的博主&#xff0c;同时…

238.除自身以外数组的乘积

目录 题目解法思路&#xff1a;步骤&#xff1a;代码实现&#xff1a;解释&#xff1a;示例&#xff1a;输出&#xff1a; 除nums[i]之外的其他数如何快速找到其索引&#xff0c;不用遍历的方法&#xff1f;前缀积是什么&#xff1f;为什么会想到前缀积和后缀积的方法&#xff…

ssm医院交互系统+vue

系统包含&#xff1a;源码论文 所用技术&#xff1a;SpringBootVueSSMMybatisMysql 免费提供给大家参考或者学习&#xff0c;获取源码请私聊我 需要定制请私聊 目 录 摘要 I Abstract II 1绪论 1 1.1研究背景与意义 1 1.1.1研究背景 1 1.1.2研究意义 1 1.2国内外研究…

开发一个微信小程序要多少钱?

在当今数字化时代&#xff0c;微信小程序成为众多企业和个人拓展业务、提供服务的热门选择。那么&#xff0c;开发一个微信小程序究竟需要多少钱呢&#xff1f; 开发成本主要取决于多个因素。首先是功能需求的复杂程度。如果只是一个简单的信息展示小程序&#xff0c;功能仅限…

录微课专用提词器,不会被录进视频中的提词器,还能显示PPT中备注的内容

不坑提词器&#xff0c;全称&#xff1a;不坑隐形提词器。是一款能够在截图、录屏、直播过程中隐藏界面的提词器软件。 系统要求&#xff1a;Win10 1024 以上&#xff08;特别提醒&#xff1a;Win7状态下不可隐身&#xff09; ⏬下载 提词器默认放在不坑盒子的安装目录下&…

MySQL—事务

目录 1.事务的简介&#xff1a; 2.使用事务 2.1 开启事务 2.2 自动提交 2.3 使用范围 2.4 事务的属性 1.事务的简介&#xff1a; 介绍事务之前&#xff0c;我们先来看一个经典的场景&#xff1a;银行转账。 假如a想要把自己的账户上的10万块钱转到b账户上&#xff0c;这…

实现uniapp天地图边界范围覆盖

前言&#xff1a; 在uniapp中&#xff0c;难免会遇到使用地图展示的功能&#xff0c;但是百度谷歌这些收费的显然对于大部分开源节流的开发者是不愿意接受的&#xff0c;所以天地图则是最佳选择。 此篇文章&#xff0c;详细的实现地图展示功能&#xff0c;并且可以自定义容器宽…

Win10、Win11一段时间不操作电脑,屏幕点击无反应假死,粘贴失效,任务栏失效等解决方法

网上找到的方法基本都是说在任务管理器中找到资源管理器的进程进行重启即可&#xff0c;这样确实能解决燃眉之急&#xff0c;可是这个问题还是会反反复复出现&#xff0c;无法根治。 本人测试了多种方案后&#xff0c;最终发现设置电源选项的硬盘关闭时间可以根治此问题。 设置…

Scala的内部类

Scala中的内部类&#xff08;Inner Class&#xff09;是指定义在另一个类的内部的类。 内部类可以访问外部类的成员&#xff08;包括私有成员&#xff09;&#xff0c;并且可以与外部类的实例紧密地绑定在一起。 内部类在Scala中非常有用&#xff0c;尤其是在需要封装特定功能…

企业一级流程架构规划方法

在之前关于企业业务流程规划的系列文章中&#xff0c;我们已经分别对企业业务流程规划的价值和原则、企业的业务流程架构的应用、两种常见的企业总体业务流程架构模式等进行了比较深入的分析和阐述&#xff0c;相信大多数企业同仁&#xff0c;已经对企业的业务流程规划&#xf…

【原创】java+springboot+mysql在线文件管理系统设计与实现

个人主页&#xff1a;程序猿小小杨 个人简介&#xff1a;从事开发多年&#xff0c;Java、Php、Python、前端开发均有涉猎 博客内容&#xff1a;Java项目实战、项目演示、技术分享 文末有作者名片&#xff0c;希望和大家一起共同进步&#xff0c;你只管努力&#xff0c;剩下的交…

ubuntu下安装mysql遇到的问题

ubuntu下安装mysql sudo apt install -y mysql-server 出现问题 ……by process 3455 解决 安装 启动 systemctl status mysql.service sudo mysql -u root -p 如何修改密码 与datagrip的连接 查看IP ifconfig 若没安装 参考 Windows10的DataGrip2024.1.4连接ubuntu22.04中的M…

Threejs 实现3D 地图(01)创建基本场景

"d3": "^7.9.0", "three": "^0.169.0", "vue": "^3.5.10" <script setup> import { onMounted,ref } from vue import * as THREE from three import * as d3 from "d3"; //莫开托坐标 矫正地图…

使用Radzen Blazor组件库开发的基于ABP框架炫酷UI主题

一、项目简介 使用过ABP框架的童鞋应该知道它也自带了一款免费的Blazor UI主题&#xff0c;它的页面是长这样的&#xff1a; 个人感觉不太美观&#xff0c;于是网上搜了很多Blazor开源组件库&#xff0c;发现有一款样式非常不错的组件库&#xff0c;名叫&#xff1a;Radzen&am…

【Linux系统编程】第三十四弹---使用匿名管道构建简易Linux进程池

✨个人主页&#xff1a; 熬夜学编程的小林 &#x1f497;系列专栏&#xff1a; 【C语言详解】 【数据结构详解】【C详解】【Linux系统编程】 目录 1、引言 2、进程池的基本概念 3、管道在进程池中的应用 4、进程池的实现 4.1、master类定义 4.2、测试信道 4.3、通过cha…

一文读懂JPA及Mybatis的原理和机制(面试经)

导览 前言Q&#xff1a;什么是JPA1. 简介2. 原理 Q&#xff1a;JPA持久化框架—Mybatis1. 内部组成与关系2. 各组件作用域和生命周期3. 动态SQL3.1 if语句3.2 choose-when-otherwise 4. mapper映射XML4.1 select4.2 insert 5. $与#的区别5.1 #{...}5.2 ${...} 结语精彩回顾 前言…

明日周刊-第23期

十月已过半&#xff0c;气温也转凉了&#xff0c;大家注意保温哦。冬吃萝卜&#xff0c;夏吃姜&#xff0c;在快要到来的冬季大家可以选择多吃点萝卜。 配图是本周末去商场抓娃娃的时候拍的照片&#xff0c;现在抓娃娃单次普遍都控制在1块钱以下了&#xff0c;还记得多年前的抓…