likes
comments
collection
share

Go基础语法问题总结

作者站长头像
站长
· 阅读数 5

什么是闭包?闭包有什么缺陷?

闭包的工作原理

在Go语言中,闭包通常是通过匿名函数实现的。一个闭包可以访问并绑定其外部函数作用域中的变量,即便这个外部作用域的函数已经结束执行。

例如:

goCopy code
func outerFunction() func() int {
    var x int = 10
    return func() int {
        x++
        return x
    }
}

func main() {
    closure := outerFunction()
    fmt.Println(closure()) // 输出 11
    fmt.Println(closure()) // 输出 12
}

在这个例子中,outerFunction 返回一个匿名函数,这个匿名函数形成了一个闭包。闭包捕获了变量 x,并在每次调用时修改它。即使outerFunction的执行已经结束,变量x的状态仍然被闭包保留。

闭包的用途

  1. 数据封装:闭包可以用于创建私有变量,这在Go中尤其有用,因为Go没有提供像其他一些语言那样的类和对象的封装机制。
  2. 回调函数:在异步编程或事件驱动编程中,闭包常被用作回调函数,它们能够捕获并操作外部的状态。
  3. 实现生成器和迭代器:通过闭包,可以轻松地实现生成器和迭代器模式,用于生成序列或遍历集合。

需要注意的问题

  1. 内存泄漏:如果闭包长时间存在,并且捕获了大量数据或复杂的对象,可能会阻止这些对象被垃圾回收,从而导致内存泄漏。
  2. 变量共享和并发问题:在闭包中使用外部变量时,特别是在并发环境下,需要小心处理共享变量的并发访问,以避免竞态条件。
  3. 生命周期管理:需要明确闭包及其捕获变量的生命周期,确保在不再需要时能够及时释放资源。

小结

闭包在Go中是一种强大的构造,它提供了函数式编程的某些特性,如高阶函数和状态封装。然而,正确理解和管理闭包是非常重要的,特别是在涉及到内存和并发的时候,错误的使用可能会导致程序中出现难以发现的bug。正确使用闭包,可以使得代码更加简洁、灵活,并且富有表现力。

什么情况下会出现栈溢出

o 语言中栈溢出(stack overflow)通常发生在以下几种情况:

  1. 深度递归调用:最常见的栈溢出原因是深度递归调用。在Go中,每个goroutine初始只有一个小的栈空间(通常在几KB左右),尽管这个栈是可以动态增长的,但是如果递归调用过深,栈的大小会超过系统允许的最大限制,从而导致栈溢出。例如,一个未正确处理终止条件的递归函数可能导致栈溢出。
  2. 过大的栈变量:在Go中,局部变量是存储在栈上的。如果你在函数中声明了非常大的局部变量(比如,大数组或结构体),它可能会占用过多的栈空间,导致栈溢出。
  3. 高并发的goroutines:虽然每个goroutine开始时栈很小,但创建大量goroutine可能会导致系统总的栈空间不足。这种情况虽然不常见,但在处理高并发任务时可能发生。
  4. 无限递归与互相调用:如果两个或多个函数互相递归调用,且没有适当的退出条件,也可能导致栈空间被耗尽。

为避免栈溢出,需要确保递归函数有有效的终止条件,避免在栈上分配过大的数据结构,以及合理控制goroutine的数量。如果确实需要处理深度递归或大量并发,应考虑使用堆分配(使用new或make),或其他方法来避免过多占用栈空间。在设计程序时,考虑栈空间的限制是非常重要的,尤其是在处理复杂的数据结构或并发场景时。

什么是不定参数?调用方法的时候,不定参数可以传入0个值吗?方法内部怎么使用不定参数?

不定参数(Variadic Parameters)是一种特殊的函数参数,它允许你向函数传递零个或多个同类型的参数。在Go语言中,这是通过在参数类型前加上省略号 ... 来实现的。不定参数主要用于情况不定或数量不定的参数。

可以传入0个值吗?

是的,可以传入0个值。不定参数允许调用时不传递任何参数,这意味着参数列表可以为空。

如何在函数内部使用不定参数?

在函数内部,这些参数表现为同类型的切片。因此,你可以像处理任何切片一样处理它们,例如使用循环遍历所有元素。

示例

下面是一个Go语言中使用不定参数的示例:

goCopy code
package main

import "fmt"

// sum 接受不定数量的int类型参数
func sum(nums ...int) int {
    total := 0
    for _, num := range nums {
        total += num
    }
    return total
}

func main() {
    fmt.Println(sum(1, 2, 3)) // 输出 6
    fmt.Println(sum())        // 输出 0,没有传入任何参数
    fmt.Println(sum(10, 20))  // 输出 30
}

在这个例子中,函数 sum 接受不定数量的 int 类型参数。在 sum 函数内部,nums 是一个 int 类型的切片,我们可以遍历它来计算总和。可以看到,即使没有传递任何参数,该函数也能正常调用。

注意事项

  • 不定参数必须是函数签名中的最后一个参数。
  • 函数内部将不定参数视为相应类型的切片。
  • 也可以将一个切片直接作为不定参数传递给函数,但需要在切片后加上省略号(例如 sum(slice...))。

什么是defer?你能解释一下defer的运作机制吗?

在 Go 语言中,defer 语句的实现自 Go 1.13 和 1.14 版本以来经历了几次优化,这些优化显著降低了在大多数情况下 defer 的性能开销。这是因为每个版本都为 defer 添加了新的机制,允许编译器根据版本和情况在编译时为每个 defer 语句选择不同的机制,以更轻量的方式运行调用。

defer 的三种实现机制

  1. 堆分配(Heap Allocation) :

    • 在 Go 1.13 之前的版本中,所有的 defer 都在堆上分配。这种机制涉及两个步骤:

      • defer 语句的位置插入 runtime.deferproc
      • 在函数返回前的位置插入 runtime.deferreturn。执行时,延迟调用会从 Goroutine 的链接列表中提取并执行,多个延迟调用作为递归调用依次执行。
  2. 栈分配(Stack Allocation) :

    • Go 1.13 通过引入 deferprocStack,以栈上分配的形式取代 deferproc,在函数返回时释放 _defer,从而消除了与内存分配相关的性能开销,只需简单维护 _defer 的链表。
  3. 开放编码(Open Coding) :

    • Go 1.14 继续引入了开放编码优化,这消除了在运行时调用 deferprocdeferprocStack 的需要。在这种机制下,延迟调用直接插入到函数返回之前,而在运行时 deferreturn 不再进行尾递归调用,而是通过循环迭代执行所有延迟的函数。
    • 这种机制使 defer 的开销几乎可以忽略不计,唯一的运行时成本是存储参与延迟调用的信息。

使用条件

  • 如果编译器优化没有被禁用(即未设置 -gcflags "-N")。
  • 在函数中的 defer 语句数量不超过 8 个,且 return 语句和 defer 语句的乘积不超过 15。
  • defer 关键字不能在循环中执行。

这些优化使得 Go 语言中 defer 语句的使用在绝大多数情况下都变得更为高效。不过,根据具体的代码结构和编译器版本,使用的具体机制可能有所不同

选择不同 defer 实现机制的条件

在 Go 语言中,编译器会根据不同的情况选择适用于 defer 语句的最优机制。以下是选择不同 defer 实现机制的条件:

  1. 堆分配(Heap Allocation) :

    • 在 Go 1.13 之前,默认使用的是堆分配机制。
    • 如果 defer 语句在循环中使用,或者在函数中有大量 defer 调用的情况下,可能仍然会使用堆分配。
  2. 栈分配(Stack Allocation) :

    • Go 1.13 引入的栈分配机制,适用于大多数常规情况,特别是当 defer 语句不在循环中,且函数中的 defer 调用数量适中。
    • 栈分配的主要优势是减少内存分配的开销,因为 _defer 结构在函数返回时一并释放。
  3. 开放编码(Open Coding) :

    • Go 1.14 引入的开放编码机制用于那些更为轻量级的场景,其中 defer 调用的数量相对较少,通常不超过 8 个,且 defer 语句不位于循环中。
    • 当编译器优化没有被禁用(例如未设置 -gcflags "-N")时,会优先考虑使用这种机制。
    • 这种机制几乎消除了所有的运行时开销,因为延迟调用直接嵌入到函数返回的位置。

总的来说,Go 编译器会根据 defer 语句的上下文以及编译器的优化设置来选择最合适的机制。在日常使用中,你通常不需要担心这些细节,因为 Go 编译器会自动为你选择最高效的实现方式。这些优化确保了即使频繁使用 defer,性能开销也被最小化。

1. 堆分配(Heap Allocation)

优点:

  • 通用性:在早期版本的 Go 中,这是唯一的实现方式,适用于所有情况。
  • 完整性:支持所有类型的 defer 使用场景,包括复杂的函数结构和循环内部的 defer

缺点:

  • 性能开销:每个 defer 语句都需要进行内存分配,这增加了运行时开销。
  • 效率低下:由于涉及堆内存的操作,这种机制在性能方面不是最优的。

2. 栈分配(Stack Allocation)

优点:

  • 性能提升:通过在栈上分配 _defer 结构,减少了内存分配的开销,提高了性能。
  • 延迟调用优化:相比于堆分配,栈分配更高效,因为它避免了不必要的内存分配和释放。

缺点:

  • 适用性限制:在某些复杂的场景(如循环中的 defer 或过多的 defer 调用)中,可能仍需回退到堆分配机制。

3. 开放编码(Open Coding)

优点:

  • 最小化开销:基本消除了 defer 的性能开销,使 defer 在大多数情况下成本非常低。
  • 简化实现:通过在编译时直接在函数退出前插入延迟调用的代码,简化了运行时的处理。

缺点:

  • 使用限制:仅在 defer 的数量和复杂度较低的情况下使用。例如,函数中 defer 的数量不超过 8,且不在循环中。
  • 编译器优化依赖:如果编译器优化被禁用,这种机制将不会被使用。

总的来说,随着 Go 语言版本的发展,defer 的实现机制越来越高效,尤其是在 Go 1.14 引入的开放编码机制,显著降低了 defer 的性能开销。在日常使用中,大多数情况下开发者不需要担心这些细节,因为编译器会自动选择最合适的实现。

一个方法内部defer能不能超过8个?

根据开放编码的使用条件解释

defer内部能不能修改返回值?怎么改?

在函数返回时,返回值已经被确定,但如果使用命名返回值,defer 可以修改这些返回值。命名返回值是在函数定义时就给出的返回变量名。

defer 的执行顺序是后进先出,即最后一个 defer 语句将首先执行。如果有多个 defer 修改返回值,后执行的 defer 将影响最终的返回值。

数组和切片有什么区别?

在Go语言中,数组和切片是两种不同的数据类型,它们有一些重要的区别:

  1. 长度

    • 数组:数组的长度是固定的,它是数组类型的一部分。一旦声明,数组的大小不能更改。
    • 切片:切片是对数组的抽象。切片的长度是可变的,它提供了对数组子序列的灵活访问。
  2. 声明方式

    • 数组:在声明数组时,你需要指定元素的数量。例如,var a [5]int 声明了一个包含5个整数的数组。
    • 切片:切片的声明不需要指定元素数量。例如,var s []int 声明了一个整数切片。
  3. 内部结构

    • 数组:数组是值类型。当你将一个数组赋值给另一个数组时,会复制所有元素。
    • 切片:切片是引用类型。它实际上包含了三个部分:指向底层数组的指针、切片的长度和容量。当你将一个切片赋值给另一个切片时,两个切片将引用同一个数组。
  4. 零值

    • 数组:数组的零值是其类型的零值构成的数组。例如,[5]int 的零值是 [0, 0, 0, 0, 0]
    • 切片:切片的零值是 nil。一个 nil 切片的长度和容量都是0。
  5. 性能

    • 数组:由于数组是值类型,所以在作为参数传递给函数时会复制整个数组,这可能导致性能问题。
    • 切片:切片是引用类型,传递切片给函数时只会复制切片本身,而不会复制其底层数组。
  6. 使用场景

    • 数组:当你需要固定数量的同类型元素时使用数组。
    • 切片:更加常用,适用于需要动态长度的序列。切片更灵活,是Go中处理序列的主要方式。

简而言之,数组是固定长度的同类型元素序列,而切片则是对数组的封装,提供了更多的灵活性和方便性。在Go的日常编程中,切片的使用频率远高于数组。

切片怎么扩容

Go语言中的切片(slice)扩容是一个自动的过程,当现有切片的容量不足以容纳更多元素时,它会自动进行。理解切片的扩容机制,需要先理解切片的内部结构:切片由三个主要部分构成,指向底层数组的指针、切片的长度(len)和切片的容量(cap)。

切片扩容的原理:

当向切片追加元素,且现有底层数组无法容纳更多元素时(即 len(slice) == cap(slice)),Go会自动进行扩容。这个过程如下:

  1. 分配新数组:Go会创建一个新的数组,其容量通常是原来容量的两倍(对于较大的切片,增长因子可能小于2,但这是由具体实现和切片的当前大小决定的)。//现版本256为分界线
  2. 复制元素:原切片中的元素会被复制到这个新数组中。
  3. 返回新切片:返回的新切片将指向这个新的数组,新切片的长度会增加,并且新切片的容量是新数组的容量。

示例:

goCopy code
s := make([]int, 0, 1) // 初始容量为1
s = append(s, 1)       // 容量足够,直接追加
s = append(s, 2)       // 容量不足,自动扩容,新容量通常为2

在这个例子中,当第二次调用 append 时,切片的容量不足以容纳新元素,因此Go自动创建一个新的底层数组,新数组的容量是原来的两倍。

注意事项:

  • 扩容可能是一个昂贵的操作,因为它涉及到内存分配和元素复制。
  • 扩容的具体行为(如增长因子)可能依赖于Go的版本和具体实现。
  • 当原切片的长度接近其容量时,考虑提前扩容可以避免多次扩容带来的性能损耗。

切片的这种动态扩容特性使得它非常灵活,适合用于实现动态增长的序

作业

实现切片的特定下标的删除操作 简单实现--->高性能实现--->泛型实现--->支持缩容,设计缩容机制

转载自:https://juejin.cn/post/7344186375610220563
评论
请登录