init函数是什么时候执行的?
init的函数的作用是什么?
通常作为程序执行前包的初始化,例如mysql redis 等中间件的初始化
init函数的执行顺序是怎样的?
分不同情况来回答:
- 在同一个go文件里面如果有多个init方法,它们会按照代码依次执行。
- 同一个package文件里面,它们会按照文件名的顺序执行。
- 不同的package且不是相互依赖的情况下,按照Import导入的顺序执行。
- 不同的package是相互依赖的情况,会优先执行最后被依赖的init函数。
go文件的初始化顺序是怎样的?
- 执行引入的包。
- 当前包里面的常量变量。
- 执行init函数。
- 执行main函数。
注意点:
- 包相互依赖避免循环导入问题。
- 所有文件的init函数都是在一个goroutine内执行的。
- 如果一个包在不同的地方被引入多次,但是它的Init函数只会执行一次。
go如何获取项目根目录
可以通过os go内置函数库来获取 os.Getwd()
os.Args[0]
os.Executable
也可以通过环境变量自定义根路径
go中new和make有什么区别?
go和new的区别
meke分配内存的同时会初始化,new只会分配零值填充。
make主要用来给slice,map,channel来初始化,new万能没有限制。
make返回的是原始类型,new返回的是类型的指针*T。
new 申请的值均为零值,对创建映射和切片没有意义。
new可以为任何类型的值开辟内存并返回此值的指针。
数组和切片的区别?
数组和切片的相同点
数组和切片 全部元素类型都必须相同。
数组和切片 所有元素都是连续存储在一块内存中,并且是紧挨着的。
数组和切片的不同点
数组的零值是每个元素类型的零值。
切片的零值为nil。
指针类型的数组和切片直接用类型声明后是nil,不能直接使用。
slice和map有什么区别
map中的元素所在内存不一定是连续的
访问元素的时间复杂度都是O1,但相对于slice来说map更慢。
map的优势
map的key值的类型是任何可以比较的类型。
对于大多数元素为零值的情况,map可以节省大量内存。
切片的底层数据结构是什么?有什么特性?
它的底层主要有三个部分
- array 是一个指向底层数组的指针
- len 切片的长度,是指当前切片包含元素数量
- cap 切片的容量,是指底层数组能够容纳的元素数量
切片的动态特性
当切片的len到达cap时,切片需要扩容,在这个扩容的过程中,go内存机制会分配一个新的更大的底层数组,并且将原数组的内容复制到新数组里面,这个过程是runtime包的growlice函数实现的。
切片是如何扩容的?
1.7和1.8后截然不同
切片是否并发安全
需要手动管理并发安全可以用sync.Mutex来确保切片在追加元素的时候避免并发问题。
如何判断两个切片是否相等
在Go语言中,判断两个切片是否相等,需要考虑切片的元素值是否相等以及顺序是否一致。可以使用reflect.DeepEqual函数来进行比较,它能够深度比较两个值是否相等
go
package main
import (
"reflect"
"fmt"
)
func main() {
slice1 := []int{1, 2, 3}
slice2 := []int{1, 2, 3}
slice3 := []int{3, 2, 1}
// 使用reflect.DeepEqual比较两个切片
if reflect.DeepEqual(slice1, slice2) {
fmt.Println("slice1 and slice2 are equal")
} else {
fmt.Println("slice1 and slice2 are not equal")
}
// 使用reflect.DeepEqual比较两个切片
if reflect.DeepEqual(slice1, slice3) {
fmt.Println("slice1 and slice3 are equal")
} else {
fmt.Println("slice1 and slice3 are not equal")
}
}
go
package main
import (
"fmt"
)
func main() {
slice1 := []int{1, 2, 3}
slice2 := []int{1, 2, 3}
slice3 := []int{3, 2, 1}
// 比较元素值是否相等
equal := len(slice1) == len(slice2) && len(slice1) == len(slice3)
if equal {
for i := range slice1 {
if slice1[i] != slice2[i] {
equal = false
break
}
}
}
if equal {
fmt.Println("slice1 and slice2 have the same elements")
} else {
fmt.Println("slice1 and slice2 do not have the same elements")
}
equal = len(slice1) == len(slice3) && len(slice1) == len(slice3)
if equal {
for i := range slice1 {
if slice1[i] != slice3[i] {
equal = false
break
}
}
}
if equal {
fmt.Println("slice1 and slice3 have the same elements")
} else {
fmt.Println("slice1 and slice3 do not have the same elements")
}
}
slice作为参数传递是传值还是传指针
,切片(slice)作为参数传递时,实际上是传递了切片的副本,而不是原始切片的指针。
切片作为参数传递时,传递的是切片的副本。
对切片副本的修改不会影响原始切片,除非通过函数返回值获取新的切片。
切片副本和原始切片共享底层数组,直到副本进行了导致底层数组扩容的操作。
go
package main
import "fmt"
func modifySlice(s []int) {
s[0] = 100 // 修改切片副本的第一个元素
}
func modifyAndAppend(s []int) []int {
s = append(s, 4) // 追加元素,如果容量不够,会重新分配底层数组
return s
}
func main() {
original := []int{1, 2, 3}
modifySlice(original)
fmt.Println("After modifySlice:", original) // 输出: After modifySlice: [1 2 3]
newSlice := modifyAndAppend(original)
fmt.Println("After modifyAndAppend:", original, newSlice) // 输出: After modifyAndAppend: [1 2 3] [1 2 3 4]
}
切片的优化技巧
- 预先分配足够的容量
- 避免在循环中使用append
- 使用copy来复制切片
- 注意切片的并发还用 用锁机制保证它的并发安全
strings.TrimRight和strings.TrimSuffix有什么区别
go
strings.TrimRight(s string, cutset string) string
s := "!!!Hello, World!!!"
result := strings.TrimRight(s, "!")
fmt.Println(result) // 输出 "!!!Hello, World"
go
strings.TrimSuffix(s string, suffix string) string
s := "Hello, World!"
result := strings.TrimSuffix(s, "!")
fmt.Println(result) // 输出 "Hello, World"
区别:
TrimRight关注的是去除尾部的特定字符集,而TrimSuffix关注的是去除尾部的特定后缀字符串。
TrimRight需要两个参数:原始字符串和要去除的字符集;TrimSuffix只需要两个参数:原始字符串和要去除的后缀。
TrimRight会去除尾部所有指定的字符,直到遇到不在cutset中的字符为止;TrimSuffix则只会去除尾部的特定后缀字符串,如果字符串不是以这个后缀结尾的,那么原始字符串不会被修改。
go语言值溢出会发生什么?
在 Go 语言中,数值类型溢出是指当数值超出类型所能表示的范围时,结果会被截断,从而产生不正确的值。Go 语言中的整型(如 int, int8, int16, int32, int64, uint8, uint16, uint32, uint64)都会受到这种限制。
go
var x uint8 = 255 // uint8 的最大值是 255
x += 1
fmt.Println(x) // 输出 0
避免值溢出
使用适当的数据类型:确保选择的数值类型可以容纳你所需的数值范围。例如,如果你需要存储大整数,应使用 int64 或者 big.Int 而不是较小的整型。
手动检查值是否溢出:在执行操作前检查数值是否接近类型的最大值或最小值。如果快要溢出,可以采取相应的措施。
go
var x uint8 = 255
if x == math.MaxUint8 {
fmt.Println("溢出警告")
} else {
x += 1
}
发生溢出后解决方案
检测到错误并抛出异常或警告:通过条件语句来捕捉溢出场景,并采取相应措施。
选择更大范围的类型:在代码中可以考虑将类型更改为 int64、uint64,或者对特别大的数值使用 math/big 包中的 big.Int 类型,这个类型没有固定的大小限制。
go
import "math/big"
a := big.NewInt(1)
b := big.NewInt(1)
result := new(big.Int).Mul(a, b) // 执行大数乘法
fmt.Println(result)
go语言中每个值 在内存中只分布在一个内存块上的类型有哪些?
总结来说,Go 语言中每个值只分布在一个内存块上的类型主要包括基本类型(如整型、浮点型、布尔型等)、数组和结构体。
有一些类型的值在内存中并不只占据一个连续的内存块,它们涉及到指向其他内存区域的指针或引用:
切片(slice):切片本身是一个描述符,包含指向底层数组的指针、长度和容量,切片描述符存储在一块内存中,但底层数组的元素可能分布在不同的内存块上。
映射(map):映射是一种引用类型,键值对并不会存储在一个连续的内存块中。
字符串(string):字符串是一个包含指向底层字节数组的指针和长度的结构,字符串的底层数据可能不在连续的内存块中。
接口(interface):接口是一个复杂的类型,包含两个部分:类型信息和数据指针。这两部分的内存布局通常不是连续的。
go语言中哪些类型可以使用cap和和len?
len 可用于:array、slice、string、map、channel。
cap 可用于:slice、array、channel。
go语言的指针有哪些限制?
- go指针不支持直接进行算术运算。
- 一个指针类型的值不能随意转换为另一个指针类型。
- 一个指针的值不能随意跟其他类型指针的值进行比较的。
- 一个指针的值不能随意被赋值给其它任意类型的指针值。
指针比较需要满足两个条件
- 这两个指针类型相同。
- 这两个指针之间可以隐式转换。
go语言中哪些类型的零值可以用nil表示
指针、切片、映射、通道、接口类型、函数都可以用nil表示
不同数据类型的nil值的尺寸是不同的。
nil值不一定是可以相互比较的,主要取决于该类型是否可以比较。
可以比较的两个Nil值不一定相等。
go调用函数传入结构体时,是传值还是传指针?
函数的传递参数只有值传递,且传递的实参都是原始数据的一份拷贝
在go语言中赋值操作和函数调用传参都是将原始值的直接部分赋值给了目标值
如何判断两个对象是否完全相同
- 基本类型的比较
对于基本类型(如整型、布尔型、浮点型、字符、字符串等),你可以直接使用 == 运算符比较两个对象的值是否相同。
go
a := 10
b := 10
fmt.Println(a == b) // true
str1 := "hello"
str2 := "hello"
fmt.Println(str1 == str2) // true
- 数组的比较
Go 语言中的数组支持使用 == 直接比较,前提是数组的元素类型必须是可比较的(如基本类型)。
go
arr1 := [3]int{1, 2, 3}
arr2 := [3]int{1, 2, 3}
fmt.Println(arr1 == arr2) // true
- 结构体的比较
结构体可以使用 == 进行比较,但前提是结构体的所有字段都是可比较的类型。如果结构体包含了不可比较的字段(如切片、映射、函数等),直接比较会引发编译错误。
go
type Person struct {
Name string
Age int
}
p1 := Person{Name: "Alice", Age: 25}
p2 := Person{Name: "Alice", Age: 25}
fmt.Println(p1 == p2) // true
- 切片(slice)、映射(map)和通道(channel)的比较
切片、映射、通道 不能直接使用 == 进行比较,除非与 nil 进行比较。要比较两个切片或映射是否完全相同,可以手动遍历每个元素或使用第三方库(如 reflect.DeepEqual)。
go
var s1 []int = nil
var s2 []int = nil
fmt.Println(s1 == s2) // true, 因为都为 nil
s1 = []int{1, 2, 3}
s2 = []int{1, 2, 3}
// fmt.Println(s1 == s2) // 编译错误,切片不能直接比较
- 接口的比较
接口可以用 == 进行比较,只有当两个接口的动态类型和动态值都相同的时候,才会被认为是相同的。
go
var i1 interface{} = 123
var i2 interface{} = 123
fmt.Println(i1 == i2) // true
- 使用 reflect.DeepEqual 进行深度比较
对于无法直接用 == 比较的复杂类型(如切片、映射等),你可以使用 Go 标准库中的 reflect.DeepEqual 进行深度比较。它会递归地比较对象的每个字段和元素,适用于结构体、切片、映射等复杂数据结构。
go
import (
"fmt"
"reflect"
)
s1 := []int{1, 2, 3}
s2 := []int{1, 2, 3}
fmt.Println(reflect.DeepEqual(s1, s2)) // true
m1 := map[string]int{"a": 1, "b": 2}
m2 := map[string]int{"a": 1, "b": 2}
fmt.Println(reflect.DeepEqual(m1, m2)) // true
总结
基本类型、数组、结构体(可比较字段)可以使用 == 直接比较。
切片、映射、通道等引用类型不能直接比较,需要使用 reflect.DeepEqual 或手动比较。
接口类型可以使用 == 比较,前提是其动态类型和动态值相同
用两种方法判断一个对象是否拥有某个方法
- 利用类型断言(Type Assertion)判断对象是否实现某个接口
Go 是静态类型语言,但它通过接口提供了动态的特性。通过定义一个接口,并使用类型断言,能够判断某个对象是否实现了该接口(即是否拥有某个方法)。
示例代码:
假设要判断某个对象是否有 Speak() 方法:
go
type Speaker interface {
Speak()
}
type Person struct{}
func (p Person) Speak() {
fmt.Println("I can speak!")
}
func main() {
var obj interface{} = Person{}
if speaker, ok := obj.(Speaker); ok {
fmt.Println("This object has the Speak method.")
speaker.Speak() // 调用该方法
} else {
fmt.Println("This object does not have the Speak method.")
}
}
- 使用 reflect 包进行反射判断
Go 的 reflect 包允许在运行时检查和操作对象的类型和方法。通过反射可以检查一个对象是否拥有某个方法。
go
import (
"fmt"
"reflect"
)
type Person struct{}
func (p Person) Speak() {
fmt.Println("I can speak!")
}
func main() {
var obj = Person{}
method := reflect.ValueOf(obj).MethodByName("Speak")
if method.IsValid() {
fmt.Println("This object has the Speak method.")
method.Call(nil) // 调用该方法
} else {
fmt.Println("This object does not have the Speak method.")
}
}
总结:
类型断言:通过接口类型断言来判断对象是否实现某个接口(从而拥有对应的方法)。
反射(reflect):利用反射在运行时动态检查对象是否具有指定名称的方法。