golang 面试题

310 阅读19分钟

goroutine 为什么性能好

  • 协程占用内存小, 初始2kb, 动态扩容,线程需要8M(线程最小1MB,linux默认stack size 为 8M或10M)
  • 协程之间的调度、切换发生在用户态,(切换耗时只有100ns多一些,比进程切换要高30倍)

字符串拼接

拼接字符串的方式有:+ , fmt.Sprintf , strings.Builderbytes.Bufferstrings.Join

1 "+"

使用+操作符进行拼接时,会对字符串进行遍历,计算并开辟一个新的空间来存储原来的两个字符串。

2 fmt.Sprintf

由于采用了接口参数,必须要用反射获取值,因此有性能损耗。

3 strings.Builder:

用WriteString()进行拼接,String()返回拼接后的字符串,String()方法直接return *(*string)(unsafe.Pointer(&b.buf)),从而避免变量拷贝。

4 bytes.Buffer

bytes.Buffer是一个一个缓冲byte类型的缓冲器,这个缓冲器里存放着都是byte

bytes.buffer底层也是一个[]byte切片。

性能比较

strings.Builder > bytes.Buffer > "+" > fmt.Sprintf

结构体打印时,%v 和 %+v 的区别

%v输出结构体各成员的值;

%+v输出结构体各成员的名称

%#v输出结构体名称和结构体各成员的名称和值

type Author struct {
   Name         int      `json:Name`
   Publications []string `json:Publication,omitempty`
}

func TestBasic(test *testing.T) {
   a := Author{}
   fmt.Printf("%v\n", a)//{0 []}
   fmt.Printf("%+v\n", a)//{Name:0 Publications:[]}
   fmt.Printf("%#v\n", a)//basic.Author{Name:0, Publications:[]string(nil)}
}

空 struct{} 的用途

用map模拟一个set,那么就要把值置为struct{},struct{}本身不占任何空间,可以避免任何多余的内存分配。

有时候给通道发送一个空结构体,channel<-struct{}{},也是节省了空间。

仅有方法的结构体

init

The init function is a function that takes no argument and returns nothing. This function executes after the package is imported and maintains the order of execution. That means multiple init functions can be defined in a file and they will be called one after another maintaining the order.

init()函数是go初始化的一部分,初始化不是按照从上到下的导入顺序,而是按照解析的依赖关系,没有依赖的包最先初始化。

每个包首先初始化包作用域的常量和变量(常量优先于变量),然后执行包的init()函数。同一个包,甚至是同一个源文件可以有多个init()函数。

执行顺序:import –> const –> var –>init()–>main()

逃逸

The storage location does have an effect on writing efficient programs. When possible, the Go compilers will allocate variables that are local to a function in that function's stack frame. However, if the compiler cannot prove that the variable is not referenced after the function returns, then the compiler must allocate the variable on the garbage-collected heap to avoid dangling pointer errors. Also, if a local variable is very large, it might make more sense to store it on the heap rather than the stack.

Golang程序中是在编译阶段确定逃逸的,而非运行时,因此我们可以使用go build的相关工具来进行逃逸分析.

分析工具:

  • 1.通过编译工具查看详细的逃逸分析过程(go build -gcflags '-m -l' main.go)
  • 2.通过反编译命令查看go tool compile -S main.go

compile 参数介绍

go tool compile -help
  • -m print optimization decisions
  • -l disable inlinin

避免逃逸的好处:

  • 1.减少gc的压力,不逃逸的对象分配在栈上,当函数返回时就回收了资源,不需要gc标记清除
  • 2.逃逸分析完后可以确定哪些变量可以分配在栈上,栈的分配比堆快,性能好(系统开销少)
  • 3.减少动态分配所造成的内存碎片

函数传递指针真的比传值效率高吗? 我们知道传递指针可以减少底层值的拷贝,可以提高效率,但是如果拷贝的数据量小,由于指针传递会产生逃逸,可能会使用堆,也可能会增加GC的负担,所以传递指针不一定是高效的。

哪些类型可以比较

  • 整型,浮点,bool, 字符串, 常量 可以用 == 比较

  • 一个数组的元素类型是可以相互比较的,那么数组类型也是可以相互比较的,这时候我们可以直接通过==比较运算符来比较两个数组,只有当两个数组的所有元素都是相等的时候数组才是相等的

    • 数组长度、类型也需要相等,否则compile error image.png
  • slice之间不能比较,不能使用==操作符来判断,slice唯一合法的比较操作是和nil比较

  • 和slice一样,map之间也不能进行相等比较;唯一的例外是和nil进行比较

  • 如果结构体的全部成员都是可以比较的,那么结构体也是可以比较的

2 个 nil 可能不相等吗?

两个nil只有在类型相同时才相等

GC(垃圾回收)的工作原理

目前的go GC采用三色标记法混合写屏障技术。

Go GC有个阶段:

  • STW,开启混合写屏障
  • 将所有对象加入白色集合,从根对象开始,将其放入灰色集合。每次从灰色集合取出一个对象标记为黑色,然后遍历其子对象,标记为灰色,放入灰色集合;
  • 如此循环直到灰色集合为空。剩余的白色对象就是需要清理的对象。
  • STW,关闭混合写屏障;
  • 在后台进行GC(并发)。

[GC 触发时机]

主动触发:调用 runtime.GC

被动触发

  • sysmon触发,该触发条件由 runtime.forcegcperiod 变量控制,默认为2 分钟。当超过两分钟没有产生任何 GC 时,强制触发 GC。

  • SetGCPercent

// SetGCPercent sets the garbage collection target percentage: // a collection is triggered when the ratio of freshly allocated data // to live data remaining after the previous collection reaches this percentage.

内存分配

Go also divides Memory Pages into a block of 67 different classes by size starting at 8 bytes up to 32 kilobytes .

Large objects(Object of Size > 32kb) are allocated directly from mheap. These large requests come at an expense of central lock,

Allocation is done using size segregated per P allocation areas to minimize fragmentation while eliminating locks in the common case.

mheap 结构体中有:

allspans []*mspan // all spans out there

central [numSpanClasses]struct {
   mcentral mcentral
   pad      [(cpu.CacheLinePadSize - unsafe.Sizeof(mcentral{})%cpu.CacheLinePadSize) % cpu.CacheLinePadSize]byte
}
  • mspan, 对应到一个size class,start page 和 number of pages
  • mcentral, groups spans of same size class together

p 结构体中有:

mcache      *mcache

// Cache of mspan objects from the heap.
mspancache struct {
   // We need an explicit length here because this field is used
   // in allocation codepaths where write barriers are not allowed,
   // and eliminating the write barrier/keeping it eliminated from
   // slice updates is tricky, more so than just managing the length
   // ourselves.
   len int
   buf [128]*mspan
}
  • mcache, Per-thread (in Go, per-P) cache for small objects.

非接口的任意类型 T() 都能够调用 *T 的方法吗?反过来呢?

一个T类型的值可以调用*T类型声明的方法,当且仅当T是可寻址的

反之:*T 可以调用T()的方法,因为指针可以解引用。

但是T没有*T的方法集合

无缓冲的 channel 和有缓冲的 channel 的区别?

对于无缓冲区channel:

发送的数据如果没有被接收方接收,那么发送方阻塞; 如果一直接收不到发送方的数据,接收方阻塞

有缓冲的channel: 发送方在缓冲区满的时候阻塞,接收方不阻塞;接收方在缓冲区为空的时候阻塞,发送方不阻塞。

Go 常见内存泄漏的情况

go101.org/article/mem…

  • 获取长字符串中的一段导致长字符串未释放

  • 获取长slice中的一段导致长slice未释放

  • goroutine泄漏

    • 不合理的使用可能会导致大量 goroutine 无法结束,资源也无法被释放,随着时间推移造成了内存的泄漏。

    • The causes of Goroutine leaks are usually:

      • dead lock (blocked):
        Read/write operations such as channel/mutex are being performed inside Goroutine, but due to logic problems, they are blocked all the time in some cases.
      • dead loop:
        The business logic within the Goroutine enters a dead loop and resources are never released.
      • long wait:
        The business logic within the Goroutine goes into a long wait, with new Goroutines constantly being added to the wait.
      • 没有调用 ticker.Stop()
    • 排查

      • 访问 http://host:port/debug/pprof/goroutine?debug=1, PProf will return a list of all Goroutines with stack traces.
  • time.Ticker未关闭导致泄漏

    • When a time.Timer value is not used any more, it will be garbage collected after some time. But this is not true for a time.Ticker value. We should stop a time.Ticker value when it is not used any more.
  • Using Finalizers Improperly

    • Setting a finalizer for a value which is a member of a cyclic reference group may [prevent all memory blocks allocated for the cyclic reference group from being collected].
    • // SetFinalizer sets the finalizer associated with obj to the provided // finalizer function. When the garbage collector finds an unreachable block // with an associated finalizer, it clears the association and runs // finalizer(obj) in a separate goroutine. This makes obj reachable again, // but now without an associated finalizer. Assuming that SetFinalizer // is not called again, the next time the garbage collector sees // that obj is unreachable, it will free obj.
    • // A single goroutine runs all finalizers for a program, sequentially. // If a finalizer must run for a long time, it should do so by starting // a new goroutine.
  • Deferring Function Call 导致泄漏

    • A very large deferred call queue may also consume much memory, and some resources might not get released in time if some calls are delayed too much.

[GMP 调度过程中存在哪些阻塞]

  • syscall、I/O
  • channel、select
  • mutex

常见的goroutine操作函数有哪些

runtime.Gosched()

// Gosched yields the processor, allowing other goroutines to run. It does not
// suspend the current goroutine, so execution resumes automatically.
func Gosched() {
   checkTimeouts()
   mcall(gosched_m)
}

runtime.Goexit()

// Goexit terminates the goroutine that calls it. No other goroutine is affected.
// Goexit runs all deferred calls before terminating the goroutine. Because Goexit
// is not a panic, any recover calls in those deferred functions will return nil.
//
// Calling Goexit from the main goroutine terminates that goroutine
// without func main returning. Since func main has not returned,
// the program continues execution of other goroutines.
// If all other goroutines exit, the program crashes.

如何控制协程数目。

GOMAXPROCS 限制的是同时执行用户态 Go 代码的操作系统线程的数量,但是对于被系统调用阻塞的线程数量是没有限制的。GOMAXPROCS 的默认值等于 CPU 的逻辑核数,同一时间,一个核只能绑定一个线程,然后运行被调度的协程。因此对于 CPU 密集型的任务,若该值过大,例如设置为 CPU 逻辑核数的 2 倍,会增加线程切换的开销,降低性能。对于 I/O 密集型应用,适当地调大该值,可以提高 I/O 吞吐率。

另外对于协程,可以用带缓冲区的channel来控制,下面的例子是协程数为1024的例子

var wg sync.WaitGroup
ch := make(chan struct{}, 1024)
for i:=0; i<20000; i++{
	wg.Add(1)
	ch<-struct{}{}
	go func(){
		defer wg.Done()
		<-ch
	}
}
wg.Wait()

此外还可以用协程池:其原理无外乎是将上述代码中通道和协程函数解耦,并封装成单独的结构体。常见第三方协程池库,比如tunny等。

面向对象

Java defines OOP concepts as follows:

  • Abstraction.  Using simple things to represent complexity. We all know how to turn the TV on, but we don’t need to know how it works in order to enjoy it. In Java, abstraction means simple things like objectsclasses and variables represent more complex underlying code and data. This is important because it lets you avoid repeating the same work multiple times.

  • Encapsulation. The practice of keeping fields within a class private, then providing access to those fields via public methods. Encapsulation is a protective barrier that keeps the data and code safe within the class itself. We can then reuse objects like code components or variables without allowing open access to the data system-wide.

  • Inheritance. A special feature of Object-Oriented Programming in Java, Inheritance lets programmers create new classes that share some of the attributes of existing classes. Using Inheritance lets us build on previous work without reinventing the wheel.

  • Polymorphism. Allows programmers to use the same word in Java to mean different things in different contexts. One form of polymorphism is method overloading (方法重载: 如果有两个方法的方法名相同,但参数不一致,那么可以说一个方法是另一个方法的重载。). The other form is method overriding(方法覆盖: 如果在子类中定义一个方法,其名称、返回类型及参数签名正好与父类中某个方法的名称、返回类型及参数签名相匹配,那么可以说,子类的方法覆盖了父类的方法。).

Go面向对象是如何实现的

Go实现面向对象的两个关键是struct和interface。

如何理解go 语言中的interface ?

  1. interface 是方法声明的集合
  2. 任何类型的对象实现了在 interface 接口中声明的全部方法,则表明该类型实现了该接口。
  3. interface 可以作为一种数据类型,实现了该接口的任何对象都可以给对应的接口类型变量赋值。

uint型变量值分别为 1,2,它们相减的结果是多少?

	var a uint = 1
	var b uint = 2
	fmt.Println(a - b)

结果会溢出,如果是32位系统,结果是2^32-1,如果是64位系统,结果2^64-1.

maxint32

fmt.Println(math.MaxInt32, math.MaxInt64)//2147483647,9223372036854775807

长度分别为10,19

下面这句代码是什么作用,为什么要定义一个空值?

type GobCodec struct{
	conn io.ReadWriteCloser
	buf *bufio.Writer
	dec *gob.Decoder
	enc *gob.Encoder
}

type Codec interface {
	io.Closer
	ReadHeader(*Header) error
	ReadBody(interface{})  error
	Write(*Header, interface{}) error
}

var _ Codec = (*GobCodec)(nil)

答:将nil转换为GobCodec类型,然后再转换为Codec接口,如果转换失败,说明GobCodec没有实现Codec接口的所有方法。

mutex有几种模式?

mutex有两种模式:normal 和 starvation

正常模式

所有goroutine按照FIFO的顺序进行锁获取,被唤醒的goroutine和新请求锁的goroutine同时竞争锁,通常新请求锁的goroutine更容易获取锁(持续占有cpu),被唤醒的goroutine则不容易获取到锁。公平性:否。

饥饿模式 所有尝试获取锁的goroutine进行等待排队,新请求锁的goroutine不会进行锁获取(禁用自旋),而是加入队列尾部等待获取锁。公平性:是。

go竞态条件了解吗?

所谓竞态竞争,就是当两个或以上的goroutine访问相同资源时候,对资源进行读/写。

比如var a int = 0,有两个协程分别对a+=1,我们发现最后a不一定为2.这就是竞态竞争。

通常我们可以用go run -race xx.go来进行检测。

解决方法是,对临界区资源上锁,或者使用原子操作(atomics),原子操作的开销小于上锁。

如果若干个goroutine,有一个panic会怎么做?

有一个panic,那么剩余goroutine也会退出,程序退出。如果不想程序退出,那么必须通过defer + recover 方法来捕获 panic 并恢复将要崩掉的程序。

// The panic built-in function stops normal execution of the current
// goroutine. When a function F calls panic, normal execution of F stops
// immediately. Any functions whose execution was deferred by F are run in
// the usual way, and then F returns to its caller. To the caller G, the
// invocation of F then behaves like a call to panic, terminating G's
// execution and running any deferred functions. This continues until all
// functions in the executing goroutine have stopped, in reverse order. At
// that point, the program is terminated with a non-zero exit code. This
// termination sequence is called panicking and can be controlled by the
// built-in function recover.

Go解析Tag是怎么实现的?

var a Author
t := reflect.TypeOf(a)

for i := 0; i < t.NumField(); i++ {
   log.Println(t.Field(i).Name, t.Field(i).Tag)
}

优雅的启停 热重启

  • 停止的时候,停止接收新的连接 + 处理完既有连接
  • 新的进程启动并「接管」旧进程

Http.Server内置的Shutdown方法支持优雅关机

// Shutdown gracefully shuts down the server without interrupting any
// active connections. Shutdown works by first closing all open
// listeners, then closing all idle connections, and then waiting
// indefinitely for connections to return to idle and then shut down.

优雅重启用endless,原理如下 cloud.tencent.com/developer/a…

image.png

  1. 监听 SIGHUP 信号;
  2. 收到信号时 fork 子进程(使用相同的启动命令),将服务监听的 socket 文件描述符传递给子进程;
  3. 子进程监听父进程的 socket,这个时候父进程和子进程都可以接收请求;
  4. 子进程启动成功之后发送 SIGTERM 信号给父进程,父进程停止接收新的连接,等待旧连接处理完成(或超时);
  5. 父进程退出,升级完成;

如何避免死锁

  • 使用锁的顺序: 如果我们的程序使用了多个锁,确保所有的goroutine都按照相同的顺序获取和释放锁,这可以避免死锁。

  • 避免无限制的等待: 设计程序以避免goroutine永久等待某些事件。可以使用带有超时的通道操作,或者使用 context 包来设置超时和取消操作。

atomic底层怎么实现的.

调用操作系统的原子命令 Cas,Xch,Xadd,Load,Store

[什么操作叫做原子操作]

Atomic operations execute without interruption of any other process in between their execution phase. They execute at the lowest level and can’t be broken down further. Formally, when an atomic operation is in its executing process, no other process can read, modify, or interrupt that operation’s data.

在 Go中,一条普通的赋值语句其实不是一个原子操作。例如,在32 位机器上写 int64 类型的变量就会有中间状态,因为他会被拆成两次写操作(MOV)——写低32 位和写高32 位。

[原子操作和锁的区别]

原子操作由底层硬件支持,而锁则由操作系统的调度器实现。锁应当用来保护一段逻辑,对于一个变量更新的保护,原子操作通常会更有效率,并且更能利用计算机多核的优势,如果要更新的是一个复合对象,则应当使用 atomic.Value 封装好的实现。

[sync.Pool 有什么用]

对于很多需要重复分配、回收内存的地方,sync.Pool 是一个很好的选择。频繁地分配、回收内存会给 GC 带来一定的负担,严重的时候会引起 CPU 的毛刺,而 sync.Pool 可以将暂时不用的对象缓存起来,待下次需要的时候直接使用,不用再次经过内存分配,复用对象的内存,减轻 GC 的压力,提升系统的性能。

go的调试/分析工具用过哪些。

go的自带工具链相当丰富,

  • pprof:用于性能调优,
  • race:用于竞争检测;
  • go vet: Vet examines Go source code and reports suspicious constructs

当select监控多个chan同时到达就绪态时,如何先执行某个任务?

可以在子case再加一个 select语句。

func priority_select(ch1, ch2 <-chan string) {
	for {
		select {
		case val := <-ch1:
			fmt.Println(val)
		case val2 := <-ch2:
		priority:
			
				select {
				case val1 := <-ch1:
					fmt.Println(val1)

				default:
					break priority
				}
			
			fmt.Println(val2)
		}
	}

}

参考: zhuanlan.zhihu.com/p/471490292

runtime包里面的方法

runtime.Gosched()
runtime.Stack()//formats a stack trace of the calling goroutine into buf
runtime.NumCPU()

fork

fork() creates a new process by duplicating the calling process. The new process is referred to as the child process. The calling process is referred to as the parent process.

The child process and the parent process run in separate memory spaces. At the time of fork() both memory spaces have the same content. Memory writes, file mappings (mmap(2)), and unmappings (munmap(2)) performed by one of the processes do not affect the other.

The child process is an exact duplicate of the parent process except for the following points:

(以下是其中的一部分)

   •   process ID
   
   •  The child does not inherit its parent's memory locks
      (mlock(2), mlockall(2)).

    •  The child's set of pending signals is initially empty
      (sigpending(2)).

   •  The child does not inherit outstanding asynchronous I/O
      operations from its parent (aio_read(3), aio_write(3)), nor
      does it inherit any asynchronous I/O contexts from its parent
      (see io_setup(2)).
      

copy on write

fork creates a copy of the parent's page table for the child and marks the physical pages read only, so if any of the two processes tries to write it will trigger a page fault and copy the page.

可重入锁

  • 在加锁上:如果是可重入互斥锁,当前尝试加锁的线程如果就是持有该锁的线程时,加锁操作就会成功。
  • 在解锁上:可重入互斥锁一般都会记录被加锁的次数,只有执行相同次数的解锁操作才会真正解锁。

Go 官方不支持可重入锁 其实 Russ Cox 于 2010 年在《[Experimenting with GO]》就给出了答复,认为递归(又称:重入)互斥是个坏主意,这个设计并不好。

可重入的设计违反了前面所提到的设计理念,也就是:“要保证这些变量的不变性保持,不会在后续的过程中被破坏”。

json包变量不加tag会怎么样?

  • 如果变量首字母小写,则为private。无论如何不能转,因为取不到反射信息

  • 如果变量首字母大写,则为public

    • 不加tag,可以正常转为json里的字段,json内字段名跟结构体内字段原名一致
    • 加了tag,从structjson的时候,json的字段名就是tag里的字段名,原字段名已经没用。

ioutil.ReadAll vs io.Copy

juejin.cn/post/697764…

相比于io.ReadAllio.Copy有以下优势:

  • 如果srcdst分别是WriterToReaderFrom,那么就省去了中间buf缓存的环节,数据直接从srcdst

  • 使用固定长度的buffer作为临时缓冲区,不会出现slice的频繁扩容。

打印变量类型

fmt.Println(reflect.TypeOf(var)) 
fmt.Printf("%T\n", v)

pkg.go.dev/fmt

字符串的两种遍历

a := "abc"
for _, v := range a {
   fmt.Printf("%T\n", v) //int32
}
for i := 0; i < len(a); i++ {
   v := a[i]
   fmt.Printf("%T\n", v) //uint8
}

uintptr vs unsafe,Pointer

// uintptr is an integer type that is large enough to hold the bit pattern of
// any pointer.
type uintptr uintptr
// Pointer represents a pointer to an arbitrary type. There are four special operations
// available for type Pointer that are not available for other types:
// - A pointer value of any type can be converted to a Pointer.
// - A Pointer can be converted to a pointer value of any type.
// - A uintptr can be converted to a Pointer.
// - A Pointer can be converted to a uintptr.
// Pointer therefore allows a program to defeat the type system and read and write
// arbitrary memory. It should be used with extreme care.

在第i个位置插入1

 // 在第i个位置插入1
a = append(a[:i], append([]int{1}, a[i:]...)...)   

生成n个从a到b不重复随机数

//生成count个[start,end)结束的不重复的随机数
func generateRandomNumber(start int, end int, count int) []int {
   //范围检查
   if end < start || (end-start) < count {
      return nil
   }
   //存放结果的slice
   nums := make([]int, 0)
   //随机数生成器,加入时间戳保证每次生成的随机数不一样
   r := rand.Seed(time.Now().UnixMilli())

   for len(nums) < count {
      //生成随机数
      num := r.Intn((end - start)) + start
      //查重
      exist := false
      for _, v := range nums {
         if v == num {
            exist = true
            break
         }
      }
      if !exist {
         nums = append(nums, num)
      }
   }
   return nums
}

翻转含有中文、数字、英文字母的字符串

func main() {
	src := "你好abc啊哈哈"
	dst := reverse([]rune(src))
	fmt.Printf("%v\n", string(dst))
}

func reverse(s []rune) []rune {
	for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 {
		s[i], s[j] = s[j], s[i]
	}
	return s
}
  • rune关键字,从 golang 源码中看出,它是 int32 的别名(-2^31 ~ 2^31-1),比起 byte(-128 ~ 127),可表示更多的字符
  • 由于 rune 可表示的范围更大,所以能处理一切字符,当然也包括中文字符。在平时计算中文字符,可用 rune。
  • 因此将字符串转为rune的切片,再进行翻转,完美解决。

交替打印数字和字母

var ch1, ch2, ch3 chan struct{}

func TestBasic(test *testing.T) {
   ch1 = make(chan struct{})
   ch2 = make(chan struct{})
   ch3 = make(chan struct{})
   go func() {
      ch2 <- struct{}{}
   }()

   go printNum()
   go printChar()

   _ = <-ch3
}

func printNum() {
   for i := 0; i < 26; i++ {
      _ = <-ch2
      fmt.Print(i)
      ch1 <- struct{}{}
   }
}

func printChar() {
   for i := 0; i < 26; i++ {
      _ = <-ch1
      fmt.Printf("%c ", 97+i%26)
      if i < 25 {
         ch2 <- struct{}{}
      } else {
         ch3 <- struct{}{}
      }
   }
}