首页 > 其他分享 >go语言面试

go语言面试

时间:2023-02-19 20:33:54浏览次数:29  
标签:Println 语言 fmt 面试 func go Go main 函数

go面试

基础

= 和 := 的区别?

**Go语言中,= 操作符用于赋值,而 := 操作符可以用于声明及赋值。 Go 语言支持短变量声明(针对局部变量),以 := 为标志,这里要注意的是,Go 语言中会优先选择 :=,而不是 =,但在赋值的情况下,两者的效果是相同的。 **

Go语言中, = 和 := 之间的主要区别在于使用 := 将变量声明时,它会自动分配类型,而 = 不会

指针的作用

Go 语言中的指针是一种特殊的变量类型,用于存储变量的内存地址。通过指针,可以间接地访问和修改存储在内存中的变量,这在某些情况下非常有用。以下是指针在 Go 语言中的主要作用:

  1. 传递变量的内存地址:当需要将一个变量传递给函数时,如果直接传递变量的值,那么函数内部对变量的修改并不会影响原来的变量。但如果将变量的地址传递给函数,那么函数就可以通过指针来访问和修改变量,从而实现对原来变量的修改。
  2. 动态分配内存:通过指针,可以在程序运行时动态地分配内存,这在一些需要动态管理内存的应用程序中非常有用。例如,可以使用 new 函数来创建一个新的变量,并返回它的地址。
  3. 优化内存和性能:通过指针,可以直接访问和修改存储在内存中的变量,而不需要进行复制和传递值,这可以在一些对内存和性能要求较高的应用程序中提高程序的效率。

在 Go 语言中,指针可以通过 & 运算符来取得变量的地址,通过 * 运算符来访问指针指向的变量。

go语言允许多返回值吗?

是的,Go 语言允许函数返回多个值,这是 Go 语言的一项非常有用的特性。通过返回多个值,可以让函数在不同的情况下返回不同的值,这使得代码更加简洁和易读,并且减少了程序员的工作量。

go语言有异常类型吗?

Go 语言没有像 Java 和 C# 中那样的异常类型,而是采用了另一种错误处理机制,即错误类型。在 Go 中,错误是一个普通的接口类型,定义如下:

type error interface {
    Error() string
}

这个接口只有一个方法 Error(),该方法返回一个字符串,表示错误的详细信息。如果一个函数遇到了错误,可以返回一个实现了 error 接口的对象,用来表示错误的类型和信息。

在调用一个可能会返回错误的函数时,通常需要使用条件语句来判断是否发生了错误。例如,下面的代码演示了如何调用 os.Open 函数来打开一个文件,并处理可能出现的错误:

f, err := os.Open("filename.txt")
if err != nil {
    fmt.Println("Failed to open file:", err)
    return
}
defer f.Close()

// 从文件中读取数据

在上面的代码中,os.Open 函数用于打开一个文件,并返回一个 *os.File 类型的指针和一个 error 类型的错误对象。如果 err 不为 nil,则表示打开文件出现了错误,需要进行相应的错误处理。否则,文件打开成功,可以使用 defer 语句来确保文件在函数结束前关闭。

需要注意的是,虽然 Go 中没有异常类型,但错误处理机制可以达到类似的效果,使得程序可以更加灵活地处理各种异常情况。同时,Go 的错误处理机制也可以使得程序更加简洁和可读,避免了 Java 和 C# 中异常滥用的问题。

go语言什么是协程(Goroutine)

在 Go 语言中,协程(Goroutine)是一种轻量级的线程,由 Go 语言运行时环境(runtime)管理。与传统的线程相比,协程的创建和销毁开销较小,且协程之间的切换开销也较小,因此可以轻松地创建数以千计的协程。

协程可以看作是一种并发的设计模式,可以用来处理大量的任务或者事件。在 Go 语言中,协程可以通过 go 关键字来创建,例如:

func main() {
    go foo()
    go bar()
}

func foo() {
    // 执行某些任务
}

func bar() {
    // 执行另外一些任务
}

在上面的例子中,foobar 函数都是使用 go 关键字创建的协程。这两个协程将会并发地执行,互不干扰。同时,main 函数本身也是一个协程,可以与其他协程并发执行。

协程通常比线程更加轻量级,因为它们不需要独立的内存空间和操作系统级别的线程调度器。Go 语言的运行时环境会在多个协程之间进行自动的调度,使得程序可以高效地利用多核 CPU,并实现高并发的执行模型。

需要注意的是,协程与线程不同的是,它们的执行顺序是不确定的。因此,在编写使用协程的程序时,需要考虑如何进行同步和互斥,以避免数据竞争和其他并发问题。在 Go 语言中,可以使用通道(channel)等同步机制来实现协程之间的通信和协作。

如何高效的拼接字符串

在 Go 语言中,字符串是不可变的,即一旦创建之后就不能被修改。因此,在需要频繁拼接字符串的情况下,使用简单的字符串拼接操作可能会导致性能问题,因为每次拼接字符串都需要创建一个新的字符串对象。为了高效地拼接字符串,可以使用以下几种方法:

  1. 使用 strings.Builder 类型。strings.Builder 是一个可变字符串类型,提供了多种方法来高效地拼接字符串,例如 WriteStringWriteByteWriteRune 等。使用 strings.Builder 类型时,可以减少因为频繁创建字符串对象而导致的内存分配和拷贝操作,从而提高程序的性能。例如:
var builder strings.Builder
builder.WriteString("Hello, ")
builder.WriteString("world!")
result := builder.String()
  1. 使用 bytes.Buffer 类型。bytes.Buffer 类型是一个可变的字节数组类型,同样提供了多种方法来高效地拼接字符串。与 strings.Builder 类型类似,使用 bytes.Buffer 时可以减少内存分配和拷贝操作,提高程序性能。例如:
var buffer bytes.Buffer
buffer.WriteString("Hello, ")
buffer.WriteString("world!")
result := buffer.String()
  1. 使用 fmt.Sprintf 函数。fmt.Sprintf 函数可以格式化字符串并返回一个字符串结果。该函数支持多种格式化选项,例如 %d%s 等。虽然使用 fmt.Sprintf 可能会产生额外的字符串拷贝操作,但在大多数情况下,这种操作的影响很小,而且代码更加简洁易懂。例如:
result := fmt.Sprintf("%s%s", "Hello, ", "world!")

需要注意的是,使用以上方法时,应该尽量避免在循环中频繁拼接字符串,因为这样可能会导致内存分配和拷贝操作过多,从而影响程序的性能。如果需要拼接大量字符串时,建议使用 strings.Builderbytes.Buffer 等可变类型,以便高效地拼接字符串。

go语言什么是 rune 类型

在 Go 语言中,rune 类型是一个 32 位的 Unicode 字符,用于表示 Unicode 码点。rune 类型是一个别名类型,本质上等价于 int32 类型,但在语义上表示 Unicode 字符。

由于 rune 类型可以表示任意一个 Unicode 码点,因此它可以用来处理多语言和国际化应用中的字符数据。在 Go 语言中,可以使用 string 类型来表示字符串,而每个字符都可以表示为一个 rune 类型的值。例如:

str := "Hello, 世界"
for _, r := range str {
    fmt.Printf("%c", r)
}

在上面的例子中,str 是一个包含英文字符和中文字符的字符串,可以通过 range 关键字遍历字符串中的每个字符,并使用 %c 格式化选项输出字符。

**需要注意的是,虽然 rune 类型本质上等价于 int32 类型,但在语义上它表示一个 Unicode 字符,因此不应该将其与普通的整数类型混用。如果需要处理整数数据,应该使用 int 或其他适当的整数类型。

小兔

ASCII 码只需要 7 bit 就可以完整地表示,但只能表示英文字母在内的128个字符,为了表示世界上大部分的文字系统,发明了 Unicode, 它是ASCII的超集,包含世界上书写系统中存在的所有字符,并为每个代码分配一个标准编号(称为Unicode CodePoint),在 Go 语言中称之为 rune,是 int32 类型的别名。

Go 语言中,字符串的底层表示是 byte (8 bit) 序列,而非 rune (32 bit) 序列。例如下面的例子中 使用 UTF-8 编码后各占 3 个 byte,因此 len("Go语言") 等于 8,当然我们也可以将字符串转换为 rune 序列。

fmt.Println(len("Go语言")) // 8
fmt.Println(len([]rune("Go语言"))) // 4

go语言如何判断 map 中是否包含某个 key ?

在 Go 语言中,可以使用以下两种方法判断 map 中是否包含某个 key

  1. 使用 if 语句和 ok 值:可以使用 if 语句和 ok 值来判断 map 中是否包含某个 key。具体地,当 map 中包含指定的 key 时,ok 值为 true,否则为 false。例如:
m := make(map[string]int)
m["foo"] = 1

if value, ok := m["foo"]; ok {
    fmt.Println("m[\"foo\"]=", value)
} else {
    fmt.Println("m does not contain key \"foo\"")
}

在上面的例子中,当 map m 中包含 key"foo" 时,if 语句中的条件表达式为 true,因此执行 if 语句中的语句块。在语句块中,value 变量被赋值为 m["foo"] 的值,即 1

2.使用 _, ok := m[key] 语句:可以使用 _, ok := m[key] 语句来判断 map 中是否包含某个 key。具体地,当 map 中包含指定的 key 时,ok 值为 true,否则为 false。例如:

m := make(map[string]int)
m["foo"] = 1

if _, ok := m["foo"]; ok {
    fmt.Println("m contains key \"foo\"")
} else {
    fmt.Println("m does not contain key \"foo\"")
}

在上面的例子中,_, ok := m["foo"] 语句判断 map m 中是否包含 key"foo" 的键值对。由于 map 中确实包含这个 key,因此 ok 值为 trueif 语句中的条件表达式为 true,因此执行 if 语句中的第一条语句。

Go 支持默认参数或可选参数吗?

Go 语言不支持默认参数或可选参数。在函数定义中,必须指定所有参数的类型和名称,调用函数时也必须按照定义的顺序传递参数。

这是因为 Go 语言的设计哲学之一是尽可能简单和直接。在语言设计中,避免复杂性是一个重要的目标。默认参数和可选参数等功能虽然可以减少代码量,但同时也会增加语言的复杂性和不确定性。因此,Go 语言选择了不支持这些功能。

在实际使用中,如果需要定义一个函数,它可以接受不同数量的参数或不同类型的参数,可以使用不定参数列表的语法,即使用 ... 语法来定义一个参数的不定数量的列表。例如:

func myFunc(args ...string) {
    for _, arg := range args {
        fmt.Println(arg)
    }
}

在上面的例子中,myFunc 函数使用不定参数列表来接受不同数量的字符串参数。在函数内部,可以通过 args 参数来访问参数列表中的所有元素。这样,调用方可以传递任意数量的参数给函数,并且函数也可以接受不同数量的参数。但是,需要注意的是,这些参数在函数内部都被视为同一类型,这意味着在函数内部需要进行类型检查和转换,以确保参数类型的正确性。

go语言defer 的执行顺序

在 Go 语言中,defer 语句用于延迟函数或方法的执行,以便在函数或方法返回之前执行一些清理或收尾工作。在一个函数或方法中,可以使用多个 defer 语句来延迟多个函数或方法的执行。defer 语句的执行顺序如下:

  1. 当执行到 defer 语句时,将 defer 语句后面的函数或方法压入一个栈中,并记录函数的参数值。
  2. 在函数或方法返回之前,依次执行栈中的所有函数或方法,即后进先出(LIFO)的顺序执行。

下面是一个例子,演示了 defer 语句的执行顺序:

func main() {
    defer fmt.Println("1st defer")
    defer fmt.Println("2nd defer")
    defer fmt.Println("3rd defer")

    fmt.Println("Hello, world!")
}

在上面的代码中,我们使用了三个 defer 语句来延迟三个 fmt.Println 函数的执行。运行上面的代码,输出结果如下:

Hello, world!
3rd defer
2nd defer
1st defer

从输出结果可以看出,Hello, world! 语句先被执行,然后依次执行了栈中的三个 defer 语句,即 3rd defer2nd defer1st defer。因此,defer 语句的执行顺序是后进先出的。

需要注意的是,defer 语句中记录的函数参数在 defer 语句执行时就已经确定,因此如果在 defer 语句后面修改参数值,对 defer 语句中的函数没有影响。因此,建议在 defer 语句中不要修改参数值。

go语言实现变量交换

在 Go 语言中,交换两个变量的值可以通过中间变量或使用多重赋值的方式来实现。下面是两种实现方式的示例:

  1. 中间变量实现:
func swap(a, b int) (int, int) {
    tmp := a
    a = b
    b = tmp
    return a, b
}

func main() {
    x, y := 1, 2
    x, y = swap(x, y)
    fmt.Println(x, y)
}

在上面的代码中,我们定义了一个 swap 函数来交换两个整数类型的变量。在 swap 函数中,我们定义了一个中间变量 tmp,然后通过中间变量交换 ab 的值。在 main 函数中,我们通过多重赋值的方式来交换 xy 的值,并打印结果。

  1. 多重赋值实现:
func main() {
    x, y := 1, 2
    x, y = y, x
    fmt.Println(x, y)
}

在上面的代码中,我们使用多重赋值的方式来交换 xy 的值。通过 x, y = y, x 的方式,先将 y 的值赋给 x,然后将 x 的值赋给 y,从而交换了 xy 的值。

无论使用中间变量还是多重赋值的方式,都可以很容易地实现两个变量的值交换。需要注意的是,在使用多重赋值的方式时,两个变量的类型必须相同。

go语言中tag的作用

在 Go 语言中,结构体(struct)类型的字段可以使用 tag(标签)来指定一些额外的信息,这些信息通常用于反射(reflection)或序列化(serialization)等场景。tag 是一个字符串,写在结构体字段的后面,用反引号(`)括起来。tag 的格式如下:

`key1:"value1" key2:"value2" ...`

其中,每个键值对之间使用空格分隔,键和值之间使用冒号(:)分隔。tag 中的键必须是非空字符串,值可以是任意字符串。

Go 语言中内置的 reflect 包可以使用 tag 来获取结构体字段的额外信息。例如,可以使用 reflect 包中的 TypeField 函数来获取结构体类型和字段的信息。下面是一个使用 tag 的示例:

type User struct {
    Name    string `json:"name" xml:"name"`
    Age     int    `json:"age" xml:"age"`
    Address string `json:"address" xml:"address"`
}

func main() {
    user := User{
        Name:    "Alice",
        Age:     30,
        Address: "New York",
    }
    b, err := json.Marshal(user)
    if err != nil {
        panic(err)
    }
    fmt.Println(string(b))
}

在上面的代码中,我们定义了一个名为 User 的结构体类型,其中每个字段都使用了 jsonxml 两个 tag。在 main 函数中,我们创建了一个 User 类型的实例,并使用 json 包中的 Marshal 函数将其序列化为 JSON 格式的字符串。在序列化过程中,json 包会自动识别 User 结构体的 tag,将其转换为 JSON 字段名。因此,最终输出的 JSON 字符串中的字段名为 nameageaddress

除了序列化之外,tag 还可以用于其他场景,例如验证、ORM 等。通过使用 tag,可以为结构体字段添加额外的元数据,使其在不同的场景下发挥更大的作用。

如何判断 2 个字符串切片(slice) 是相等的?

在 Go 语言中,判断两个字符串切片(slice)是否相等,需要先判断它们的长度是否相等,然后再依次比较它们的每个元素是否相等。

可以使用 reflect.DeepEqual 函数来比较两个字符串切片是否相等,该函数可以比较任意类型的值是否相等,但是效率相对较低,因此不推荐在大型数据集上使用。示例如下:

package main

import (
    "fmt"
    "reflect"
)

func main() {
    s1 := []string{"foo", "bar", "baz"}
    s2 := []string{"foo", "bar", "baz"}

    if reflect.DeepEqual(s1, s2) {
        fmt.Println("s1 and s2 are equal")
    } else {
        fmt.Println("s1 and s2 are not equal")
    }
}

输出结果为:

s1 and s2 are equal

另外,如果字符串切片中的元素是可比较类型(例如 stringint 等),则可以使用循环来逐个比较每个元素是否相等。示例如下:

goCopy code
package main

import "fmt"

func main() {
    s1 := []string{"foo", "bar", "baz"}
    s2 := []string{"foo", "bar", "baz"}

    if len(s1) != len(s2) {
        fmt.Println("s1 and s2 are not equal")
        return
    }

    for i := 0; i < len(s1); i++ {
        if s1[i] != s2[i] {
            fmt.Println("s1 and s2 are not equal")
            return
        }
    }

    fmt.Println("s1 and s2 are equal")
}

输出结果与上面的示例相同,为:

s1 and s2 are equal

字符串打印时,%v 和 %+v 的区别

在 Go 语言中,%v%+v 都是用于格式化字符串的占位符,用于打印结构体等数据类型。

%v 表示按照默认格式输出,对于结构体类型,将会递归地输出其所有的字段和值。示例如下:

type Person struct {
    Name string
    Age  int
}

func main() {
    p := Person{"Alice", 30}
    fmt.Printf("%v\n", p)
}

输出结果为:

{Alice 30}

%+v 表示输出更详细的信息,对于结构体类型,除了递归地输出其所有的字段和值之外,还会输出字段的名称。示例如下:

type Person struct {
    Name string
    Age  int
}

func main() {
    p := Person{"Alice", 30}
    fmt.Printf("%+v\n", p)
}

输出结果为:

{Name:Alice Age:30}

因此,可以根据需要选择 %v%+v 来格式化打印字符串。

Go 语言中如何表示枚举值(enums)?

Go 语言中没有枚举类型(enum),但是可以使用常量来实现类似枚举的功能。常量是一种恒定不变的值,可以是数值、布尔值或字符串等类型。

常量的值可以是数值、字符、字符串或布尔值等类型,如果多个常量具有相同的类型和值,则它们可以被分组在一起定义。示例如下:

package main

import "fmt"

const (
    Sunday = iota // 0
    Monday        // 1
    Tuesday       // 2
    Wednesday     // 3
    Thursday      // 4
    Friday        // 5
    Saturday      // 6
)

func main() {
    fmt.Println(Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday)
}

输出结果为:

0 1 2 3 4 5 6

在上面的示例中,使用 const 关键字定义了一组常量,并使用 iota 表示自动递增的枚举值。在这个示例中,Sunday 的值为 0,后面的常量值自动递增。

在 Go 语言中,使用常量定义枚举值具有较高的可读性,同时也保证了常量值的不可变性。

go语言空 struct{} 的用途

在 Go 语言中,空的 struct{} 类型也被称为“空结构体”或“零宽度结构体”,它没有任何字段,也不占用任何内存空间。

空结构体的主要用途是在实现某些算法或数据结构时,作为占位符或标记使用。例如,可以将空结构体作为 map 中的值类型,用于表示该 map 只关心键的存在与否,而不关心键的值是什么。示例如下:

goCopy code
// 用空结构体作为 map 的值类型
var m map[string]struct{}

// 添加元素
m["key1"] = struct{}{}
m["key2"] = struct{}{}
m["key3"] = struct{}{}

// 判断元素是否存在
if _, ok := m["key1"]; ok {
    fmt.Println("key1 exists")
}

在上面的示例中,使用空结构体作为 map 的值类型,将 map 定义为 map[string]struct{},并使用 struct{}{} 添加元素。在判断元素是否存在时,使用 _ 忽略掉返回值,只判断 ok 值是否为真即可。

由于空结构体不占用任何内存空间,因此使用它作为 map 的值类型时,可以大大减少内存占用,提高程序的性能。

小兔

使用空结构体 struct{} 可以节省内存,一般作为占位符使用,表明这里并不需要一个值。

fmt.Println(unsafe.Sizeof(struct{}{})) // 0

比如使用 map 表示集合时,只关注 key,value 可以使用 struct{} 作为占位符。如果使用其他类型作为占位符,例如 int,bool,不仅浪费了内存,而且容易引起歧义。

type Set map[string]struct{}

func main() {
	set := make(Set)

	for _, item := range []string{"A", "A", "B", "C"} {
		set[item] = struct{}{}
	}
	fmt.Println(len(set)) // 3
	if _, ok := set["A"]; ok {
		fmt.Println("A exists") // A exists
	}
}

再比如,使用信道(channel)控制并发时,我们只是需要一个信号,但并不需要传递值,这个时候,也可以使用 struct{} 代替。

func main() {
	ch := make(chan struct{}, 1)
	go func() {
		<-ch
		// do something
	}()
	ch <- struct{}{}
	// ...
}

再比如,声明只包含方法的结构体。

type Lamp struct{}

func (l Lamp) On() {
        println("On")

}
func (l Lamp) Off() {
        println("Off")
}

实现原理

init() 函数是什么时候执行的?

在 Go 语言中,每个包都可以包含一个或多个 init() 函数。init() 函数用于在程序运行之前执行一些必要的初始化操作,例如初始化全局变量、检查环境变量、注册驱动程序等等。

init() 函数是在程序运行之前自动执行的,没有任何参数和返回值,也不能手动调用。在一个包中,init() 函数的执行顺序是从上到下的,也就是说,先定义的 init() 函数会先执行。

在一个程序中,如果一个包被引用多次,那么它的 init() 函数只会被执行一次。如果一个程序中引用了多个包,它们的 init() 函数的执行顺序是按照包导入的依赖关系来确定的,也就是说,被依赖的包的 init() 函数会先被执行。

以下是一个示例程序,其中包含一个 init() 函数:

package main

import "fmt"

func init() {
    fmt.Println("Initialization...")
}

func main() {
    fmt.Println("Hello, world!")
}

在上面的示例中,定义了一个 init() 函数,在程序运行之前会被自动调用,输出 "Initialization..." 字符串。在 main() 函数执行之前,init() 函数已经完成了初始化操作。执行上述代码将输出如下结果:

Initialization...
Hello, world!

小兔

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

每个包首先初始化包作用域的常量和变量(常量优先于变量),然后执行包的 init() 函数。同一个包,甚至是同一个源文件可以有多个 init() 函数。init() 函数没有入参和返回值,不能被其他函数调用,同一个包内多个 init() 函数的执行顺序不作保证。

一句话总结: import –> const –> var –> init() –> main()

示例:

package main

import "fmt"

func init()  {
	fmt.Println("init1:", a)
}

func init()  {
	fmt.Println("init2:", a)
}

var a = 10
const b = 100

func main() {
	fmt.Println("main:", a)
}
// 执行结果
// init1: 10
// init2: 10
// main: 10

Go 语言的局部变量分配在栈上还是堆上?

Go 语言中的局部变量(例如在函数内部定义的变量)的分配位置既可以是栈上,也可以是堆上,具体取决于该变量的类型和其生命周期。

一般来说,对于较小的局部变量(例如 int、float 等基本类型),Go 编译器会将它们分配在栈上。因为这些变量的生命周期较短,不需要在堆上分配内存。栈的分配和释放非常快,不需要进行垃圾回收,因此可以提高程序的性能。

对于较大的局部变量(例如数组、结构体等复合类型),Go 编译器会将它们分配在堆上。因为这些变量的大小不确定,分配在栈上可能导致栈溢出等问题。此外,这些变量的生命周期可能比较长,需要在函数返回后继续存在,因此需要在堆上分配内存,并由垃圾回收器来管理内存的释放。

需要注意的是,在 Go 语言中,无论变量是在栈上还是堆上分配,都可以使用指针来访问它们的值,而且在使用时无需考虑变量的分配位置。这一点与 C/C++ 等语言有所不同,可以减少程序员的负担。

小兔

由编译器决定。Go 语言编译器会自动决定把一个变量放在栈还是放在堆,编译器会做逃逸分析(escape analysis),当发现变量的作用域没有超出函数范围,就可以在栈上,反之则必须分配在堆上。

func foo() *int {
	v := 11
	return &v
}

func main() {
	m := foo()
	println(*m) // 11
}

foo() 函数中,如果 v 分配在栈上,foo 函数返回时,&v 就不存在了,但是这段函数是能够正常运行的。Go 编译器发现 v 的引用脱离了 foo 的作用域,会将其分配在堆上。因此,main 函数中仍能够正常访问该值

2 个 interface 可以比较吗 ?

Go 语言中,两个接口的比较并不是直接可行的,因为接口是动态类型,比较接口需要比较接口变量的动态值,这是不可靠的。但是,如果两个接口的类型和值都相同,那么这两个接口就是相等的。这可以通过类型断言来实现,例如:

a := SomeInterface{...}
b := SomeOtherInterface{...}

if reflect.TypeOf(a) == reflect.TypeOf(b) {
    if reflect.ValueOf(a).Interface() == reflect.ValueOf(b).Interface() {
        // a 和 b 相等
    }
}

上面的代码使用了反射库 reflect 来比较两个接口变量的类型和动态值,如果它们都相同,则认为这两个接口相等。

需要注意的是,使用反射来比较接口会导致性能问题,并且需要谨慎使用。如果可能的话,最好避免比较接口,而是使用其他方式来实现需求。

小兔

Go 语言中,interface 的内部实现包含了 2 个字段,类型 T 和 值 V,interface 可以使用 ==!= 比较。2 个 interface 相等有以下 2 种情况

  1. 两个 interface 均等于 nil(此时 V 和 T 都处于 unset 状态)
  2. 类型 T 相同,且对应的值 V 相等。

看下面的例子:

type Stu struct {
	Name string
}

type StuInt interface{}

func main() {
	var stu1, stu2 StuInt = &Stu{"Tom"}, &Stu{"Tom"}
	var stu3, stu4 StuInt = Stu{"Tom"}, Stu{"Tom"}
	fmt.Println(stu1 == stu2) // false
	fmt.Println(stu3 == stu4) // true
}

stu1stu2 对应的类型是 *Stu,值是 Stu 结构体的地址,两个地址不同,因此结果为 false。
stu3stu4 对应的类型是 Stu,值是 Stu 结构体,且各字段相等,因此结果为 true。

2 个 nil 可能不相等吗?

在 Go 语言中,通常情况下,两个 nil 是相等的,无论它们是哪种类型的 nil。例如,两个空的切片或映射的 nil 是相等的,它们可以使用 == 运算符进行比较。

但是,如果一个值的类型是接口类型,并且它的动态值为 nil,则该值不等于 nil。这是因为接口类型的值包括类型和值两个部分,即使值为 nil,类型也不为空。因此,两个接口类型的值即使都是 nil,它们的类型可能不同,因此不相等。例如:

var a io.Reader
var b *bytes.Buffer

if a == nil && b == nil {
    fmt.Println("a and b are equal") // 不会执行
}

a = b
if a == nil && b == nil {
    fmt.Println("a and b are equal") // 执行
}

在上面的示例中,a 是一个空接口类型,b 是一个指向 bytes.Buffer 的空指针。在将 b 赋值给 a 后,ab 都是 nil,但它们的类型不同,因此第一个比较结果为 false,第二个比较结果为 true。

需要注意的是,在 Go 语言中,nil 不是关键字,而是预定义的常量,可以用于表示空指针或空引用。因此,在使用 nil 进行比较时,必须使用 == 运算符,而不是 = 运算符,后者用于赋值操作。

小兔

可能。

接口(interface) 是对非接口值(例如指针,struct等)的封装,内部实现包含 2 个字段,类型 T 和 值 V。一个接口等于 nil,当且仅当 T 和 V 处于 unset 状态(T=nil,V is unset)。

  • 两个接口值比较时,会先比较 T,再比较 V。
  • 接口值与非接口值比较时,会先将非接口值尝试转换为接口值,再比较。
func main() {
	var p *int = nil
	var i interface{} = p
	fmt.Println(i == p) // true
	fmt.Println(p == nil) // true
	fmt.Println(i == nil) // false
}

上面这个例子中,将一个 nil 非接口值 p 赋值给接口 i,此时,i 的内部字段为(T=*int, V=nil),i 与 p 作比较时,将 p 转换为接口后再比较,因此 i == p,p 与 nil 比较,直接比较值,所以 p == nil

但是当 i 与 nil 比较时,会将 nil 转换为接口 (T=nil, V=nil),与i (T=*int, V=nil) 不相等,因此 i != nil。因此 V 为 nil ,但 T 不为 nil 的接口不等于 nil。

简述 Go 语言GC(垃圾回收)的工作原理

最常见的垃圾回收算法有标记清除(Mark-Sweep) 和引用计数(Reference Count),Go 语言采用的是标记清除算法。并在此基础上使用了三色标记法和写屏障技术,提高了效率。

标记清除收集器是跟踪式垃圾收集器,其执行过程可以分成标记(Mark)和清除(Sweep)两个阶段:

  • 标记阶段 — 从根对象出发查找并标记堆中所有存活的对象;
  • 清除阶段 — 遍历堆中的全部对象,回收未被标记的垃圾对象并将回收的内存加入空闲链表。

标记清除算法的一大问题是在标记期间,需要暂停程序(Stop the world,STW),标记结束之后,用户程序才可以继续执行。为了能够异步执行,减少 STW 的时间,Go 语言采用了三色标记法。

三色标记算法将程序中的对象分成白色、黑色和灰色三类。

  • 白色:不确定对象。
  • 灰色:存活对象,子对象待处理。
  • 黑色:存活对象。

标记开始时,所有对象加入白色集合(这一步需 STW )。首先将根对象标记为灰色,加入灰色集合,垃圾搜集器取出一个灰色对象,将其标记为黑色,并将其指向的对象标记为灰色,加入灰色集合。重复这个过程,直到灰色集合为空为止,标记阶段结束。那么白色对象即可需要清理的对象,而黑色对象均为根可达的对象,不能被清理。

三色标记法因为多了一个白色的状态来存放不确定对象,所以后续的标记阶段可以并发地执行。当然并发执行的代价是可能会造成一些遗漏,因为那些早先被标记为黑色的对象可能目前已经是不可达的了。所以三色标记法是一个 false negative(假阴性)的算法。

三色标记法并发执行仍存在一个问题,即在 GC 过程中,对象指针发生了改变。比如下面的例子:

A (黑) -> B (灰) -> C (白) -> D (白)

正常情况下,D 对象最终会被标记为黑色,不应被回收。但在标记和用户程序并发执行过程中,用户程序删除了 C 对 D 的引用,而 A 获得了 D 的引用。标记继续进行,D 就没有机会被标记为黑色了(A 已经处理过,这一轮不会再被处理)。

A (黑) -> B (灰) -> C (白) 
  ↓
 D (白)

为了解决这个问题,Go 使用了内存屏障技术,它是在用户程序读取对象、创建新对象以及更新对象指针时执行的一段代码,类似于一个钩子。垃圾收集器使用了写屏障(Write Barrier)技术,当对象新增或更新时,会将其着色为灰色。这样即使与用户程序并发执行,对象的引用发生改变时,垃圾收集器也能正确处理了。

一次完整的 GC 分为四个阶段:

  • 1)标记准备(Mark Setup,需 STW),打开写屏障(Write Barrier)
  • 2)使用三色标记法标记(Marking, 并发)
  • 3)标记结束(Mark Termination,需 STW),关闭写屏障。
  • 4)清理(Sweeping, 并发)

函数返回局部变量的指针是否安全?

这在 Go 中是安全的,Go 编译器将会对每个局部变量进行逃逸分析。如果发现局部变量的作用域超出该函数,则不会将内存分配在栈上,而是分配在堆上。

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

  • 一个T类型的值可以调用为*T类型声明的方法,但是仅当此T的值是可寻址(addressable) 的情况下。编译器在调用指针属主方法前,会自动取此T值的地址。因为不是任何T值都是可寻址的,所以并非任何T值都能够调用为类型*T声明的方法。
  • 反过来,一个*T类型的值可以调用为类型T声明的方法,这是因为解引用指针总是合法的。事实上,你可以认为对于每一个为类型 T 声明的方法,编译器都会为类型*T自动隐式声明一个同名和同签名的方法。

哪些值是不可寻址的呢?

  • 字符串中的字节;
  • map 对象中的元素(slice 对象中的元素是可寻址的,slice的底层是数组);
  • 常量;
  • 包级别的函数等。

举一个例子,定义类型 T,并为类型 *T 声明一个方法 hello(),变量 t1 可以调用该方法,但是常量 t2 调用该方法时,会产生编译错误。

type T string

func (t *T) hello() {
	fmt.Println("hello")
}

func main() {
	var t1 T = "ABC"
	t1.hello() // hello
	const t2 T = "ABC"
	t2.hello() // error: cannot call pointer method on t
} 

并发编程

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

对于无缓冲的 channel,发送方将阻塞该信道,直到接收方从该信道接收到数据为止,而接收方也将阻塞该信道,直到发送方将数据发送到该信道中为止。

对于有缓存的 channel,发送方在没有空插槽(缓冲区使用完)的情况下阻塞,而接收方在信道为空的情况下阻塞。

例如:

func main() {
	st := time.Now()
	ch := make(chan bool)
	go func ()  {
		time.Sleep(time.Second * 2)
		<-ch
	}()
	ch <- true  // 无缓冲,发送方阻塞直到接收方接收到数据。
	fmt.Printf("cost %.1f s\n", time.Now().Sub(st).Seconds())
	time.Sleep(time.Second * 5)
}
func main() {
	st := time.Now()
	ch := make(chan bool, 2)
	go func ()  {
		time.Sleep(time.Second * 2)
		<-ch
	}()
	ch <- true
	ch <- true // 缓冲区为 2,发送方不阻塞,继续往下执行
	fmt.Printf("cost %.1f s\n", time.Now().Sub(st).Seconds()) // cost 0.0 s
	ch <- true // 缓冲区使用完,发送方阻塞,2s 后接收方接收到数据,释放一个插槽,继续往下执行
	fmt.Printf("cost %.1f s\n", time.Now().Sub(st).Seconds()) // cost 2.0 s
	time.Sleep(time.Second * 5)
}

什么是协程泄露(Goroutine Leak)?

协程泄露是指协程创建后,长时间得不到释放,并且还在不断地创建新的协程,最终导致内存耗尽,程序崩溃。常见的导致协程泄露的场景有以下几种:

  • 缺少接收器,导致发送阻塞

这个例子中,每执行一次 query,则启动1000个协程向信道 ch 发送数字 0,但只接收了一次,导致 999 个协程被阻塞,不能退出。

func query() int {
	ch := make(chan int)
	for i := 0; i < 1000; i++ {
		go func() { ch <- 0 }()
	}
	return <-ch
}

func main() {
	for i := 0; i < 4; i++ {
		query()
		fmt.Printf("goroutines: %d\n", runtime.NumGoroutine())
	}
}
// goroutines: 1001
// goroutines: 2000
// goroutines: 2999
// goroutines: 3998
  • 缺少发送器,导致接收阻塞

那同样的,如果启动 1000 个协程接收信道的信息,但信道并不会发送那么多次的信息,也会导致接收协程被阻塞,不能退出。

  • 死锁(dead lock)

两个或两个以上的协程在执行过程中,由于竞争资源或者由于彼此通信而造成阻塞,这种情况下,也会导致协程被阻塞,不能退出。

  • 无限循环(infinite loops)

这个例子中,为了避免网络等问题,采用了无限重试的方式,发送 HTTP 请求,直到获取到数据。那如果 HTTP 服务宕机,永远不可达,导致协程不能退出,发生泄漏。

func request(url string, wg *sync.WaitGroup) {
	i := 0
	for {
		if _, err := http.Get(url); err == nil {
			// write to db
			break
		}
		i++
		if i >= 3 {
			break
		}
		time.Sleep(time.Second)
	}
	wg.Done()
}

func main() {
	var wg sync.WaitGroup
	for i := 0; i < 1000; i++ {
		wg.Add(1)
		go request(fmt.Sprintf("https://127.0.0.1:8080/%d", i), &wg)
	}
	wg.Wait()
}

Go 可以限制运行时操作系统线程的数量吗?

The GOMAXPROCS variable limits the number of operating system threads that can execute user-level Go code simultaneously. There is no limit to the number of threads that can be blocked in system calls on behalf of Go code; those do not count against the GOMAXPROCS limit.

可以使用环境变量 GOMAXPROCSruntime.GOMAXPROCS(num int) 设置,例如:

runtime.GOMAXPROCS(1) // 限制同时执行Go代码的操作系统线程数为 1

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

代码

常量与变量

下列代码的输出是:

func main() {
	const (
		a, b = "golang", 100
		d, e
		f bool = true
		g
	)
	fmt.Println(d, e, g)
}

golang 100 true

在同一个 const group 中,如果常量定义与前一行的定义一致,则可以省略类型和值。编译时,会按照前一行的定义自动补全。即等价于

func main() {
	const (
		a, b = "golang", 100
		d, e = "golang", 100
		f bool = true
		g bool = true
	)
	fmt.Println(d, e, g)
}

下列代码的输出是:

func main() {
	const N = 100
	var x int = N

	const M int32 = 100
	var y int = M
	fmt.Println(x, y)
}

编译失败:cannot use M (type int32) as type int in assignment

Go 语言中,常量分为无类型常量和有类型常量两种,const N = 100,属于无类型常量,赋值给其他变量时,如果字面量能够转换为对应类型的变量,则赋值成功,例如,var x int = N。但是对于有类型的常量 const M int32 = 100,赋值给其他变量时,需要类型匹配才能成功,所以显示地类型转换:

var y int = int(M)

下列代码的输出是:

func main() {
	var a int8 = -1
	var b int8 = -128 / a
	fmt.Println(b)
}

-128

int8 能表示的数字的范围是 [-2^7, 2^7-1],即 [-128, 127]。-128 是无类型常量,转换为 int8,再除以变量 -1,结果为 128,常量除以变量,结果是一个变量。变量转换时允许溢出,符号位变为1,转为补码后恰好等于 -128。

对于有符号整型,最高位是是符号位,计算机用补码表示负数。补码 = 原码取反加一。

例如:

-1 :  11111111
00000001(原码)    11111110(取反)    11111111(加一)
-128:    
10000000(原码)    01111111(取反)    10000000(加一)

-1 + 1 = 0
11111111 + 00000001 = 00000000(最高位溢出省略)
-128 + 127 = -1
10000000 + 01111111 = 11111111

下列代码的输出是:

func main() {
	const a int8 = -1
	var b int8 = -128 / a
	fmt.Println(b)
}

编译失败:constant 128 overflows int8

-128 和 a 都是常量,在编译时求值,-128 / a = 128,两个常量相除,结果也是一个常量,常量类型转换时不允许溢出,因而编译失败

作用域

下列代码的输出是:

func main() {
	var err error
	if err == nil {
		err := fmt.Errorf("err")
		fmt.Println(1, err)
	}
	if err != nil {
		fmt.Println(2, err)
	}
}

1 err

:= 表示声明并赋值,= 表示仅赋值。

变量的作用域是大括号,因此在第一个 if 语句 if err == nil 内部重新声明且赋值了与外部变量同名的局部变量 err。对该局部变量的赋值不会影响到外部的 err。因此第二个 if 语句 if err != nil 不成立。所以只打印了 1 err

defer 延迟调用

下列代码的输出是:

type T struct{}

func (t T) f(n int) T {
	fmt.Print(n)
	return t
}

func main() {
	var t T
	defer t.f(1).f(2)
	fmt.Print(3)
}

132

defer 延迟调用时,需要保存函数指针和参数,因此链式调用的情况下,除了最后一个函数/方法外的函数/方法都会在调用时直接执行。也就是说 t.f(1) 直接执行,然后执行 fmt.Print(3),最后函数返回时再执行 .f(2),因此输出是 132。

下列代码的输出是:

func f(n int) {
	defer fmt.Println(n)
	n += 100
}

func main() {
	f(1)
}

1

打印 1 而不是 101。defer 语句执行时,会将需要延迟调用的函数和参数保存起来,也就是说,执行到 defer 时,参数 n(此时等于1) 已经被保存了。因此后面对 n 的改动并不会影响延迟函数调用的结果。

下列代码的输出是:

func main() {
	n := 1
	defer func() {
		fmt.Println(n)
	}()
	n += 100
}

101

匿名函数没有通过传参的方式将 n 传入,因此匿名函数内的 n 和函数外部的 n 是同一个,延迟执行时,已经被改变为 101。

下列代码的输出是:

func main() {
	n := 1
	if n == 1 {
		defer fmt.Println(n)
		n += 100
	}
	fmt.Println(n)
}
101
1

先打印 101,再打印 1。defer 的作用域是函数,而不是代码块,因此 if 语句退出时,defer 不会执行,而是等 101 打印后,整个函数返回时,才会执行。

标签:Println,语言,fmt,面试,func,go,Go,main,函数
From: https://www.cnblogs.com/HJZ114152/p/17135516.html

相关文章

  • Go语言sync.Map(在并发环境中使用的map)
    Go语言中的map在并发情况下,只读是线程安全的,同时读写是线程不安全的。 下面来看下并发情况下读写map时会出现的问题,代码如下://创建一个int到int的映射m:=make(......
  • The Number of Good Subsets
    TheNumberofGoodSubsetsYouaregivenanintegerarray nums .Wecallasubsetof nums good ifitsproductcanberepresentedasaproductofoneormo......
  • C语言经典习题(二)
    打印7层杨辉三角形打印7层杨辉三角形图案如下:这个题我再前几天的刷题中也写过,但是很多人私信说上次写的太简陋了,那我这次就写完整。通过图,可以看出。无论它是多少层的......
  • django中使用celery,模拟商品秒杀。
    Celery是Python开发的简单、灵活可靠的、处理大量消息的分布式任务调度模块 安装:pipinstallcelery#安装celery库pipinstallredis#celery依赖于......
  • Elasticsearch面试题
    Elasticsearch面试题1、elasticsearch了解多少,说说你们公司es的集群架构,索引数据大小,分片有多少,以及一些调优手段。面试官:想了解应聘者之前公司接触的ES使用场景......
  • Kafka面试题
    Kafka面试题1、Kafka是什么1.broker:Kafka服务器,负责消息存储和转发2.topic:消息类别,Kafka按照topic来分类消息3.partition:topic的分区,一个topic可以包......
  • 微服务面试题
    微服务面试题微服务,又称微服务架构,是一种架构风格,它将应用程序构建为以业务领域为模型的小型自治服务集合。通俗地说,你必须看到蜜蜂如何通过对齐六角形蜡细胞来构建它......
  • MyBatis面试题
    MyBatis面试题1、什么是Mybatis?1、Mybatis是一个半ORM(对象关系映射)框架,它内部封装了JDBC,开发时只需要关注SQL语句本身,不需要花费精力去处理加载驱动、创建连接......
  • ZooKeeper面试题
    ZooKeeper面试题1、什么是Zookeeper?ZooKeeper是一个开放源码的分布式协调服务,它是集群的管理者,监视着集群中各个节点的状态根据节点提交的反馈进行下一步合理操作。......
  • 数据结构面试题
    数据结构面试题1、栈(stack)栈(stack)是限制插入和删除只能在一个位置上进行的表,该位置是表的末端,叫做栈顶(top)。它是后进先出(LIFO)的。对栈的基本操作只有push(进栈)和pop(出......