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 uintptr
    Len  int
    Cap  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 := true
   for i:=0; i<=len(arr)-1; i++{
      for j:=0; j<len(arr)-1-i; j++{
         if arr[j]> arr[j+1]{
            sortedArray = false
            break
         }
      }
   }
   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 main

import "fmt"

type User struct {
	Name string
}

func main() {
	var user *User
	user = 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 main

import (
 "fmt"
 "unsafe"
)

type W struct {
 b int32
 c int64
}

func main() {
 var w *W = new(W)
 //这时w的变量打印出来都是默认值0,0
 fmt.Println(w.b,w.c)

 //现在我们通过指针运算给b变量赋值为10
 b := unsafe.Pointer(uintptr(unsafe.Pointer(w)) + unsafe.Offsetof(w.b))
 *((*int)(b)) = 10
 //此时结果就变成了10,0
 fmt.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 main

import "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 int64
	c int32
	d 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 int64
	c int32
	_ int32
	d int64
}

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

活体人脸识别技术总结及实践

文章目录 1、背景2、人脸反伪装技术2.1 活体人脸识别常见模式2.2 学术上反伪装研究 3、工程实现3.1 Silent-Face3.2 Silent-Face模型转rknn3.3 Silent-Face模型的限制 1、背景 1.1 什么是活体检测&#xff1f; 在人脸识别之前&#xff0c;先判断一下屏幕前摄像头捕捉到的人脸是…

三、语法分析,《编译原理》(本科教学版),第2版

文章目录 一、Antlr-v4 设计语法分析器1.1 Cymbol.g41.1 antlr-v4 代码实现1.2 二义性1.2.1 悬空的else1.2.2 运算符结合性带来的二义性1.2.3 运算符的优先级带来的二义性 1.3 函数调用图1.4 walker 的 时机1.5 ParseTreeWalker 与 Listener 二、上下文无关法2.1 定义2.2 语义2…

Nginx超简洁知识:负载均衡-反向代理,动静分离,配置文件

首先介绍一下为什么需要nginx&#xff1f; 在低并发场景下&#xff08;也就是用户量特别少的情况下&#xff09;&#xff0c;我们只需要部署一台服务器就能满足用户数量少的需求。 但是如果用户量逐渐增多&#xff0c;只有一台服务器是不够的。于是我们需要部署多台服务器。 …

【算法】归并排序概念及例题运用

&#x1f4e2;博客主页&#xff1a;https://blog.csdn.net/2301_779549673 &#x1f4e2;欢迎点赞 &#x1f44d; 收藏 ⭐留言 &#x1f4dd; 如有错误敬请指正&#xff01; &#x1f4e2;本文由 JohnKi 原创&#xff0c;首发于 CSDN&#x1f649; &#x1f4e2;未来很长&#…

linux链接、目标文件全解析

内容目录 内容目录 链接 1. 静态链接2. 目标文件3. 可重定位目标文件4. 符号和符号表5. 符号解析 5.1 链接器如何解析多重定义的符号5.2 与静态库链接5.3 链接器如何使用静态库来解析引用 6. 重定位 6.1 重定位条目 - 6.2 重定位符号引用 6.2.1 重定位PC相对引用6.2.2 重定位…

计算机系统的层次

目录 计算机系统的层次ISA&#xff08;指令集体系结构&#xff09; 计算机系统的层次 计算机硬件是基础指令集体系结构&#xff1a;将硬件的功能封装从指令供软件使用操作系统&#xff1a;提供人机交互界面、提供服务功能的内核例程语言处理系统&#xff1a; 语言处理程序&…

群晖通过 Docker 安装 GitLab

Docker 配置容器步骤都是大同小异的&#xff0c;可以参考&#xff1a; 群晖通过 Docker 安装 Gitea-CSDN博客 1. 在 Docker 文件夹中创建 GitLab&#xff0c;并创建子文件夹 2. 设置权限 3. 打开 Docker 应用&#xff0c;并在注册表搜索 gitlab-ce 4. 选择 gitlab-ce 映像运行…

什么是不同类型的微服务测试?

大家好&#xff0c;我是锋哥。今天分享关于【什么是不同类型的微服务测试&#xff1f;】面试题&#xff1f;希望对大家有帮助&#xff1b; 什么是不同类型的微服务测试&#xff1f; 1000道 互联网大厂Java工程师 精选面试题-Java资源分享网 微服务架构中的测试可以分为多种类…

多尺度建模:从理论到实践的深入探讨

#1024程序员节 | 征文# 引言 在现代科学与工程中&#xff0c;很多现象和过程在不同的空间和时间尺度上展现出复杂性。因此&#xff0c;能够有效地进行多尺度建模&#xff0c;已经成为了许多领域&#xff08;如物理、生物、工程、环境科学等&#xff09;研究的一个重要方向。本…

vue后台管理系统从0到1(5)

文章目录 vue后台管理系统从0到1&#xff08;5&#xff09;完善侧边栏修改bug渲染header导航栏 vue后台管理系统从0到1&#xff08;5&#xff09; 接上一期&#xff0c;我们需要完善我们的侧边狼 完善侧边栏 我们在 element 组件中可以看见&#xff0c;这一个侧边栏是符合我们…

【操作系统】06.进程控制

一、进程创建 1.1 认识fork函数 在linux中fork函数是非常重要的函数&#xff0c;它从已存在进程中创建一个新进程。新进程为子进程&#xff0c;而原进程为父进程。 进程调用fork&#xff0c;当控制转移到内核中的fork代码后&#xff0c;内核将 分配新的内存块和内核数据结构…

Aspose.PDF功能演示:使用 JavaScript 从 PDF 中提取文本

在数据提取、业务文档自动化和文本挖掘方面&#xff0c;使用 JavaScript 从PDF中提取文本非常有用。它允许开发人员自动执行从 PDF 收集信息的过程&#xff0c;从而显著提高处理大量文档的生产力和效率。在这篇博文中&#xff0c;我们将学习如何使用 JavaScript 从 PDF 中提取文…

人工智能的未来应用与发展前景

随着人工智能&#xff08;AI&#xff09;技术的快速进步&#xff0c;我们正亲历着它在各行各业中带来的巨大变革。无论是医疗、企业管理&#xff0c;还是日常生活&#xff0c;AI 技术都在改变着我们的工作和生活方式。那么&#xff0c;人工智能的应用前景究竟如何&#xff1f;它…

【消息队列】RabbitMQ实现消费者组机制

目录 1. RabbitMQ 的 发布订阅模式 2. GRPC 服务间的实体同步 2.1 生产者服务 2.2 消费者服务 3. 可靠性 3.1 生产者丢失消息 3.2 消费者丢失消息 3.3 RabbitMQ 中间件丢失消息 1. RabbitMQ 的 发布订阅模式 https://www.rabbitmq.com/tutorials/tutorial-three-go P 生…

winUI3 c++ 入门 2、 样式

目录 一、winUI3 基本概念及样式 1、边距 2、如何使用样式 1)、布局控件内定义样式 2)、APP.xmal定义全局样式 3)、单独的样式文件 3.1)、新增字典资源 xmal 3.2)、在里面设置样式 3.3)、引用样式 3、更多样式修改 1)、修改默认属性 2)、修改所有的默认颜色…

垃圾收集器与内存分配机制(一)

目录 一、为什么我们要去了解垃圾收集和内存分配 二、对象已死&#xff1f; 1. 引用计数算法 2. 可达性分析算法 3. 再谈引用 4. 生存还是死亡 5. 回收方法区 三、垃圾收集算法 1. 简介 2. 分代收集理论 2.1. 弱分代/强分代假说 2.2. 前面两代假说的缺陷 3. 标记-清…

智能去毛刺:2D视觉引导机器人如何重塑制造业未来

机器人技术已经深入到各个工业领域中&#xff0c;为制造业带来了前所未有的变革。其中&#xff0c;2D视觉引导机器人技术以其精准、高效的特点&#xff0c;在去毛刺工艺中发挥着越来越重要的作用。本文将为您介绍2D视觉引导机器人技术的基本原理及其在去毛刺工艺中的应用&#…

blender 理解 积木组合 动画制作 学习笔记

一、学习blender视频教程链接 案例2&#xff1a;积木组合_动画制作_哔哩哔哩_bilibilihttps://www.bilibili.com/video/BV1Bt4y1E7qn?vd_sourced0ea58f1127eed138a4ba5421c577eb1&p10&spm_id_from333.788.videopod.episodes 二、说明 之前已经学习了如何制作积木组…

20 Shell Script输入与输出

标出输入、标准输出、错误输出 一、程序的基本三个IO流 一&#xff09;文件描述符 ​ 任何程序在Linux系统中都有3个基本的文件描述符 ​ 比如: ​ cd/proc/$$/fd ​ 进入当前shell程序对于内核在文件系统的映射目录中: [rootlocalhost ~]# cd /proc/$$/fd [rootlocalhos…

Ubuntu22.04环境搭建MQTT服务器

官网&#xff1a; https://mosquitto.org 1.引入库 sudo apt-add-repository ppa:mosquitto-dev/mosquitto-ppa2.升级安装工具 sudo apt-get update 3.安装 sudo apt-get install mosquitto 4.安装客户端 sudo apt-get install mosquitto-clients5.添加修改配置文件 进…