Go语言基础十三连问(底部有资料免费送)

博主介绍:

我是了 凡 微信公众号【了凡银河系】期待你的关注。未来大家一起加油啊~


前言

前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家。点击跳转到网站: https://www.cbedai.net/lf

下面记录一些关于Golang基础问题一些常见的问题


1. 与其他语言相比,GO语言有什么优势?

  1. 并发编程支持:Go语言内置了轻量级线程(goroutine)和信道(channel),这使得编写并发程序变得容易,并且可以有效地利用多核CPU。
  2. 高效性:Go语言是一种编译型语言,编译后的程序执行速度非常快。此外,Go语言的垃圾回收器能够有效地管理内存,避免了程序员手动释放内存的繁琐工作。
  3. 简单易学:Go语言的语法非常简单,容易学习和使用。其代码风格强制执行,这有助于代码的可读性和维护性。
  4. 跨平台支持:Go语言可以在不同的平台上进行编译,并生成对应平台的可执行文件。这使得Go语言成为一个跨平台的语言,可以在不同的操作系统上运行。
  5. 强大的标准库:Go语言内置了许多强大的标准库,涵盖了各种领域,包括网络编程、加密、文件操作、文本处理等。这些库使得Go语言成为一个非常强大和高效的语言。

总之,Go语言的并发编程、高效性、简单易学、跨平台支持以及强大的标准库等优势,使得它成为了一种非常有吸引力的编程语言,越来越多的开发者正在选择它来开发自己的应用程序。

2. GO使用的数据类型有哪些?

  1. 布尔型(bool):布尔型只有两个值,true和false。
  2. 整型(int、int8、int16、int32、int64):表示整数值的类型,分别代表不同的位数和符号类型。
  3. 无符号整型(uint、uint8、uint16、uint32、uint64):表示不带符号的整数值,同样分别代表不同的位数。
  4. 浮点型(float32、float64):表示浮点数值的类型。
  5. 复数型(complex64、complex128):表示复数值的类型。
  6. 字符串型(string):表示字符串值的类型。
  7. 数组型(array):表示具有固定长度的同类型元素序列的类型。
  8. 切片型(slice):表示长度可变的同类型元素序列的类型。
  9. 映射型(map):表示一组键值对的无序集合的类型。
  10. 结构体型(struct):表示一组字段的集合,每个字段都有自己的类型和名称的类型。
  11. 接口型(interface):表示一组方法的集合的类型。

除此之外,Go语言还提供了一些其他的数据类型,如函数型、指针型、通道型等。在实际编程中,开发者可以根据需要选择合适的数据类型来处理不同的数据。

3. GO的类型转换和类型推断以及类型断言是什么?

Go语言提供了类型转换和类型推断功能,使得程序开发更加便捷和灵活。

3.1 类型转换

类型转换指将一个类型的值转换为另一个类型的值。在Go语言中,使用强制类型转换的方式进行类型转换。例如,可以使用以下语句将一个整数类型的值转换为浮点型的值:

var a int = 42
var b float64 = float64(a)

这里使用float64()将整数类型的值a转换为浮点型的值b。

3.2 类型推断

类型推断指根据变量的初始值推断变量的类型。在Go语言中,可以使用关键字var或:=来声明变量。如果使用var关键字声明变量,则需要显式指定变量的类型。但是,如果使用:=来声明变量,则可以根据变量的初始值推断变量的类型。例如:

var a int = 42
b := float64(a)

这里使用:=来声明变量b,并根据变量a的类型推断出b的类型为float64。
需要注意的是,类型转换和类型推断都可能会引起类型不匹配的问题,需要开发者在使用时注意类型的正确性。同时,在进行类型转换时也需要注意数据的精度和范围问题。

3.3 类型断言

接口之间在编译期间可以确定的情况下可以使用隐式类型转换,当然也可以用强制类型转换(不常用),所有情况下都可以使用类型断言。

type A interface {}
type B interface {Foo()}
// 编译时无法确定能不能转换,因此用断言
var a A
var b = a.(B)
// 编译时,可以确定
var c B
var d = A(c)
// or var d = c
// or d = c.(A)

3.4 总结

类型转换在编译期完成,包括强制转换和隐式转换
类型断言在运行时确定,包括安全类型断言和非安全类型断言

4. GO语言中Channel有什么特点。读写一个已关闭的Channel会发生什么?

在Go语言中,Channel是一种重要的并发编程机制,用于在不同的Goroutine之间传递数据。以下是Channel的一些特点:

  1. Channel是类型安全的:Channel只能传递一种类型的数据,确保了数据的类型安全。
  2. Channel是同步的:发送操作和接收操作在不同的Goroutine中时,会阻塞Goroutine直到另一个Goroutine完成对应的操作。
  3. Channel是有缓冲的:可以创建带缓冲的Channel,缓冲区中的元素数量大于0时,发送操作不会阻塞,直到缓冲区满为止。
  4. Channel是可关闭的:可以使用close()函数关闭Channel,一旦Channel被关闭,任何尝试向Channel发送数据的操作都会导致panic。

当读取一个已关闭的Channel时,读取操作不会阻塞,而是会立即返回一个零值(即该类型的默认值),并且该操作不会引起panic。例如,如果读取一个已关闭的字符串类型的Channel,会返回一个空字符串""。类似地,如果读取一个已关闭的整型类型的Channel,会返回整型的零值0。

当向一个已关闭的Channel发送数据时,发送操作会导致panic,因此在发送数据之前需要检查Channel是否已关闭。可以使用for-range语句来遍历一个Channel,如果Channel已关闭,for-range语句会自动退出循环。例如:

ch := make(chan int, 10)
// ...
close(ch)
// ...
for i := range ch {
    // 处理Channel中的元素
}

在for-range语句中遍历Channel,如果Channel已关闭,会自动退出循环。如果Channel未关闭,for-range语句会一直等待,直到接收到新的元素或者Channel被关闭为止。

以下是对channel不同状态下操作的结果。

5. GO语言中New和Make有什么区别?简单讲讲他们的用法。

在Go语言中,new和make都是用于创建变量的内置函数,但是它们有不同的用法和返回值类型。

  1. new函数
    new函数用于创建一个指向新分配的零值对象的指针。它的语法如下:

    ptr := new(Type)
    

    其中,Type表示要创建的对象的类型,ptr是指向该对象的指针。例如,创建一个字符串类型的变量,可以使用以下
    语句:

    str := new(string)
    

    注意,new函数只分配内存空间,并将分配的空间初始化为零值,不会进行任何其他的初始化操作。因此,在使用new函数创建变量时,需要注意变量的初始化操作。

  2. make函数
    make函数用于创建切片、映射和通道等引用类型的对象。它的语法如下:

    ref := make(Type, size)
    
    • 其中,Type表示要创建的对象的类型,size表示要创建的对象的大小。根据不同的类型,size的含义也不同:
    • 对于切片,size表示切片的长度,同时也会为切片分配内存空间。
    • 对于映射,size应该是0或者映射的初始容量,同时也会为映射分配内存空间。
    • 对于通道,size表示通道的缓冲区大小,同时也会为通道分配内存空间。

    例如,创建一个切片类型的变量,可以使用以下语句:

    slice := make([]int, 10)
    

    注意,make函数返回的是一个已经初始化并可以使用的对象,而不是对象的指针。因此,在使用
    make函数创建变量时,不需要进行额外的初始化操作。

总结: 在go语言中,make和new都是内存的分配(堆上),但是make只用于slice、map以及channel的初始化(非零值);而new用于类型的内存分配,并且内存置为零。

6. 如何理解GO语言中的值传递。“GO不存在指针或者引用传递”应该如何理解。

在Go语言中,函数参数传递采用的是值传递方式,即传递的是参数的副本而不是原始参数本身。这意味着,在函数内部修改参数的值并不会影响原始参数的值。因此,可以说Go语言不存在指针或引用传递。

但是,需要注意的是,在Go语言中可以使用指针类型的参数来实现类似于引用传递的效果。通过传递指针,可以在函数内部直接操作原始数据,从而改变原始数据的值。这种方式称为指针传递。例如:

func increment(x *int) {
    *x++
}

func main() {
    var n int = 10
    increment(&n)
    fmt.Println(n) // 输出11
}

在上面的例子中,increment函数接收一个指向int类型变量的指针,并通过指针来修改原始数据的值。在调用increment函数时,使用&n的方式将n的地址作为参数传递给函数,从而实现了类似于引用传递的效果。

因此,可以说Go语言不存在指针或引用传递,而是通过指针类型的参数来实现类似于引用传递的效果。这种方式既保证了函数参数的安全性,又能够高效地进行数据操作。

7. GO语言中数组和切片有什么区别?

在Go语言中,数组和切片都是用来存储一组有序的元素,但是它们有以下几个区别:

  1. 长度
    数组的长度是固定的,一旦创建就不能改变。而切片的长度是可以动态改变的,可以根据需要进行扩展或缩短。
  2. 内存分配
    在Go语言中,数组的长度是固定的,因此在创建数组时,需要分配足够的内存空间。而切片可以动态扩展或缩短,因此在创建切片时,只会分配初始大小的内存空间,随着切片的扩展,会自动分配更多的内存空间。
  3. 传递方式
    在Go语言中,数组是值类型,即当数组被传递给函数时,函数会复制整个数组。这意味着对复制的数组的任何更改都不会影响原始数组。而切片是引用类型,即当切片被传递给函数时,函数会复制切片的引用,而不是整个切片。这意味着对复制的切片所做的任何更改都会影响原始切片。
  4. 声明方式
    在Go语言中,数组的声明需要指定数组的长度。例如:
    var arr [5]int
    
    而切片的声明不需要指定长度,只需要指定元素的类型即可。例如:
    var slice []int
    
    或者使用make函数创建切片,例如:
    slice := make([]int, 5)
    

总的来说,数组和切片都是有序的元素集合,但是它们在长度、内存分配、传递方式和声明方式等方面有所不同。在实际使用时,需要根据具体的需求来选择合适的数据类型。

这里简单介绍一下切片的底层结构

type slice struct {
    array unsafe.Pointer
    len   int
    cap   int
}

这样一定就明白了,也可以说切片包含了数组。切片的底层实现结构里存在一个指向数组的指针。

8. 切片是如何扩容的。有哪些规则?

在Go语言中,切片的扩容是通过append函数实现的。当切片容量不足以容纳新元素时,系统会自动扩容,以容纳更多的元素。切片的扩容规则如下:

  1. 如果新元素的数量小于等于切片剩余的容量,系统会将新元素直接添加到切片末尾,不会进行扩容。
  2. 如果新元素的数量大于切片剩余的容量,系统会根据以下规则进行扩容:
    这里分为两个版本
    • 1.8版本之前
      • 如果切片的长度小于1024,每次扩容会将切片的容量翻倍,直到容量足够容纳新元素。
      • 如果切片的长度大于等于1024,每次扩容只会增加1/4的容量,直到容量足够容纳新元素。
      • 如果扩容后的容量仍然不足以容纳新元素,系统会继续按照上述规则进行扩容,直到容量足够为止。
    • 1.8 版本和1.18版本之后
      • 当原slice容量(oldcap)小于256的时候,新slice(newcap)容量为原来的2倍;原slice容量超过256,新slice容量newcap = oldcap+(oldcap+3*256)/4
    • 具体还是要看当前SDK内runtime内的slice包内一个叫做growslice函数内的具体扩容实现方式最准确

注意,扩容会重新分配内存空间,并将原来的元素复制到新的内存空间中。因此,在进行大量的切片操作时,可能会产生较大的内存开销,需要特别注意。
另外,切片的容量可以使用cap函数获取,可以使用cap函数查看当前切片的容量是否足够,从而决定是否需要扩容。例如:

slice := make([]int, 5, 10)
if cap(slice) < 10 {
    slice = append(slice, 1, 2, 3)
}

在上面的例子中,切片slice的容量为10,如果当前容量小于10,就需要扩容,将1、2、3添加到切片末尾。

以下为1.17.7版本的扩容实现方式

func growslice(et *_type, old slice, cap int) slice {
	// ...省略一部分
	newcap := old.cap
	doublecap := newcap + newcap
	if cap > doublecap {
		newcap = cap
	} else {
		if old.cap < 1024 {
			newcap = doublecap
		} else {
			// Check 0 < newcap to detect overflow
			// and prevent an infinite loop.
			for 0 < newcap && newcap < cap {
				newcap += newcap / 4
			}
			// Set newcap to the requested cap when
			// the newcap calculation overflowed.
			if newcap <= 0 {
				newcap = cap
			}
		}
	}
	// ...省略一部分
}

以下为1.18版本的扩容实现方式

func growslice(et *_type, old slice, cap int) slice {
	// ...省略一部分
	newcap := old.cap
	doublecap := newcap + newcap
	if cap > doublecap {
		newcap = cap
	} else {
		const threshold = 256
		if old.cap < threshold {
			newcap = doublecap
		} else {
			// Check 0 < newcap to detect overflow
			// and prevent an infinite loop.
			for 0 < newcap && newcap < cap {
				// Transition from growing 2x for small slices
				// to growing 1.25x for large slices. This formula
				// gives a smooth-ish transition between the two.
				newcap += (newcap + 3*threshold) / 4
			}
			// Set newcap to the requested cap when
			// the newcap calculation overflowed.
			if newcap <= 0 {
				newcap = cap
			}
		}
	}
	// ...省略一部分
}

以下为1.19版本的扩容实现方式

func growslice(et *_type, old slice, cap int) slice {
	// ...省略一部分
	newcap := old.cap
	doublecap := newcap + newcap
	if cap > doublecap {
		newcap = cap
	} else {
		const threshold = 256
		if old.cap < threshold {
			newcap = doublecap
		} else {
			// Check 0 < newcap to detect overflow
			// and prevent an infinite loop.
			for 0 < newcap && newcap < cap {
				// Transition from growing 2x for small slices
				// to growing 1.25x for large slices. This formula
				// gives a smooth-ish transition between the two.
				newcap += (newcap + 3*threshold) / 4
			}
			// Set newcap to the requested cap when
			// the newcap calculation overflowed.
			if newcap <= 0 {
				newcap = cap
			}
		}
	}
	// ...省略一部分
}

9. GO语言的异常处理是如何实现的。panic和recover的实现。

Go语言中的异常处理机制通过panic和recover两个关键字实现。当程序运行出现严重错误时,可以使用panic函数引发一个异常,该异常会向上冒泡,直到被最近的recover函数捕获或者程序退出。

panic函数可以接受一个任意类型的参数,通常用于表示错误信息。例如:

func foo() {
    panic("Something went wrong!")
}

当foo函数调用panic函数时,程序会抛出一个异常,并打印出错误信息。

recover函数用于捕获异常并进行处理。当一个函数中调用了recover函数时,如果当前函数是由panic函数引发的异常而导致的程序跳转,那么recover函数就会捕获该异常,并返回一个非空的错误值,否则返回nil。

recover函数只能在defer语句中调用。defer语句的作用是将一个函数推迟到当前函数返回之前执行。当在defer语句中调用recover函数时,可以捕获该函数所在的函数引发的异常。例如:

func foo() {
    defer func() {
        if err := recover(); err != nil {
            fmt.Println("Recovered:", err)
        }
    }()
    panic("Something went wrong!")
}

在上面的例子中,foo函数调用panic函数引发一个异常,但是由于在foo函数中使用了defer语句调用了recover函数,因此异常被捕获并打印出错误信息。

需要注意的是,recover函数只能在同一个Goroutine中捕获异常,不能在其他Goroutine中捕获异常。另外,建议尽量避免在程序中频繁使用panic和recover函数,以免影响程序的可读性和稳定性。

以下三个条件会让recover()返回nil也就是失效的情况:

  • panic时指定的参数为nil;(一般panic语句如panic(“xxx failed…”))
  • 当前协程没有发生panic;
  • recover没有被defer方法直接调用;

10. GO语言的错误处理。

Go语言中的错误处理是通过返回错误值来实现的。在Go语言中,函数通常会返回两个值,第一个值是函数执行的结果,第二个值是一个错误值。如果函数执行成功,错误值为nil;如果函数执行失败,错误值为一个非nil值,通常是一个实现了error接口的类型。

error接口只有一个方法Error(),返回一个字符串类型的错误信息。可以根据错误信息的内容来判断错误类型。例如:

func divide(a, b int) (int, error) {
    if b == 0 {
        return 0, errors.New("division by zero")
    }
    return a / b, nil
}

在上面的例子中,divide函数接受两个参数a和b,返回两个值,第一个值是a除以b的结果,第二个值是一个错误值。如果b等于0,就会返回一个错误值,表示除以0的错误。

在调用带有错误返回值的函数时,可以使用多重赋值来获取函数的结果和错误值。例如:

result, err := divide(10, 0)
if err != nil {
    fmt.Println("Error:", err)
} else {
    fmt.Println("Result:", result)
}

在上面的例子中,调用divide函数会得到两个返回值,分别赋值给result和err变量。如果err不为nil,则说明函数执行出错,打印出错误信息。否则,说明函数执行成功,打印出结果值。

除了使用errors.New函数创建一个新的错误值,还可以使用fmt.Errorf函数来创建一个带有格式化字符串的错误值,例如:

func divide(a, b int) (int, error) {
    if b == 0 {
        return 0, fmt.Errorf("division by zero: a=%d, b=%d", a, b)
    }
    return a / b, nil
}

在上面的例子中,使用fmt.Errorf函数创建一个带有格式化字符串的错误值,其中%d表示输出一个整数类型的值。

总之,在Go语言中,错误处理是一种非常重要的编程技巧,可以帮助我们编写出更加健壮和稳定的程序。需要养成良好的习惯,在函数中合理地使用错误返回值,并正确地处理错误。

11. Defer在使用中有哪些需要注意的地方?

defer语句用于在函数返回之前执行某些操作,例如关闭文件或释放资源等。defer语句在函数内部定义,但是可以在函数返回之前被执行,无论函数是通过正常返回还是通过panic异常返回,defer语句都会被执行。

在使用defer语句时,需要注意以下几点:

  1. defer语句的执行顺序是后进先出(LIFO),即最后一个defer语句最先执行,第一个defer语句最后执行。
  2. defer语句中的函数参数会在defer语句执行时被计算并保存,但是函数调用并不会在defer语句被定义时执行。这意味着,如果在defer语句中调用一个带有参数的函数,函数的参数会在defer语句被定义时就被计算出来,而不是在函数返回时被计算。
  3. defer语句中的变量在执行时会被保存,但是不会立即被计算。这意味着,如果在defer语句中使用一个变量,它的值会在defer语句执行时被保存,并在函数返回时被计算。
  4. 在使用defer语句时,应该避免在循环中使用defer,因为每次循环都会创建一个新的defer语句,这会导致内存泄漏或性能问题。

总之,在使用defer语句时,需要注意其执行顺序、函数参数、变量保存和循环使用等问题,以确保程序能够正确地执行,并且不会产生意料之外的行为。

12. GO语言的MAP的底层原理,它是如何实现扩容的。

Go语言的Map底层是基于哈希表实现的。哈希表是一种高效的数据结构,可以通过哈希函数将数据存储在数组中,以O(1)的时间复杂度进行查找、插入、删除操作。Go语言的Map底层使用哈希表实现了类似Python中的字典或Java中的Map的数据结构,提供了键值对的存储和快速的查找、插入、删除等操作。

当Map中的元素数量达到哈希表的容量时,Go语言会触发Map的扩容操作。扩容操作会分配一个新的更大的哈希表,并将所有原来的元素重新插入到新的哈希表中。在重新插入元素的过程中,Go语言会为每个元素重新计算哈希值和索引,以确保它们在新的哈希表中能够正确地被定位。

Map的扩容操作可以在运行时动态进行,这意味着Map的大小可以根据需要自动调整。但是,由于扩容操作需要重新插入所有元素,因此会导致一定的性能开销。为了避免频繁扩容,建议在使用Map时尽可能预估元素的数量,并在初始化Map时指定合适的初始容量。

另外,需要注意的是,Map中的元素是无序的,不能保证它们按照插入的顺序或其他特定的顺序进行访问。如果需要按照特定的顺序进行访问,可以考虑使用切片或其他数据结构来存储元素,并在需要时进行排序。

扩容的条件主要为两个:

  • 负载因子 > 6.5时,也即平均每个bucket存储的键值对达到6.5个。
  • overflow数量 > 2^15时,也即overflow数量超过32768时。

Go对于map的扩容主要有两种:

  1. 增量式扩容
    在当前 map 中新增一个桶,并将一部分元素从原有的桶中迁移到新桶中。增量扩容的好处是不需要一次性将所有元素都重新散列,可以降低扩容时的时间和空间复杂度。但是增量扩容也有缺点,就是容易导致多次扩容,增加空间的浪费。
  2. 等量式扩容
    当 map 中元素数量超过阈值时,一次性将当前 map 中的所有元素都重新散列,并重新分配到新的桶中。等量扩容的好处是可以将桶的数量控制在一个合理的范围内,减少空间浪费,但是其缺点就是需要一次性重新散列所有元素,时间复杂度较高。

无论是增量扩容还是等量扩容,map 扩容时都会重新计算元素的哈希值,重新分配到新的桶中,并且维护元素的查找和插入操作的正确性。

13. GO语言的逃逸分析是什么。哪些情况出现逃逸。

逃逸分析是Go语言的一种编译器技术,用于确定变量在运行时是在栈上分配还是在堆上分配。如果变量在函数返回后仍然需要使用,或者被多个函数共享,则需要在堆上分配。如果变量只在函数内使用,并且不需要在函数返回后继续存在,则可以在栈上分配,这样可以提高程序的性能。

在Go语言中,变量的逃逸分析是由编译器在编译时完成的。编译器会分析函数内部的变量的生命周期,并决定它们应该在栈上还是在堆上分配空间。如果变量可能逃逸到堆上,则编译器会在堆上分配空间,并在函数返回后将指针返回给调用方。

逃逸分析可以帮助我们优化程序的性能,因为在栈上分配空间比在堆上分配空间要快得多。但是,有一些情况会导致变量逃逸到堆上,例如:

  1. 将变量的指针传递给函数或方法,而且函数或方法在函数返回后仍然需要使用该指针。
  2. 在函数内部创建一个切片、Map、结构体等复杂类型,并将其返回或传递给其他函数或方法。
  3. 在函数内部使用闭包(函数内部定义函数)并将其返回或传递给其他函数或方法。
  4. 变量的生命周期跨越多个协程或多个函数。

在这些情况下,变量可能会逃逸到堆上,因为它们需要在函数返回后仍然存在。因此,在编写高性能的Go程序时,需要了解逃逸分析的相关知识,并尽可能避免出现逃逸情况,以提高程序的性能。


需要系统学习的,推荐以下学习资源,需要的发送序号领取下载:
000007:Go语言圣经
000008:Go语言实战
000009:Go专家编程
000010:Go并发编程实践
000011:Go语言编译器简介
000012:Golang修养之路

如图:
在这里插入图片描述


创作不易,点个赞吧!
如果需要后续再看点个收藏!
如果对我的文章有兴趣给个关注!
如果有问题,可以关注公众号【了凡银河系】点击联系我私聊。


  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

了 凡

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值