Golang 知识点

  1. make 和 new 的区别:
make 用于创建切片、映射和通道,返回的是创建类型的引用。
new 用于创建任意类型的指针,返回的是该类型的指针。

// make 用法示例
slice := make([]int, 5)  // 创建一个包含5个元素的整型切片

// new 用法示例
var pointer *int
pointer = new(int)  // 创建一个整型指针,指向默认值 0

  1. 数组和切片的区别
数组的长度是固定的,在创建时必须指定长度;切片长度不固定,可以动态扩展。
数组是值类型,复制操作会复制整个数组;切片是引用类型,复制操作只会复制引用,不会复制底层数据。
数组和切片的索引方式不同,数组索引从0开始,切片索引可以从任意位置开始。

// 数组的创建和赋值示例
var arr [3]int  // 创建一个包含3个元素的整型数组
arr[0] = 1
arr[1] = 2
arr[2] = 3

// 切片的创建和赋值示例
slice := []int{1, 2, 3}  // 创建一个包含3个元素的整型切片
  1. for range 循环中地址是否会发生变化:
在 for range 循环中,如果循环的是切片或数组,那么每次循环中的元素都是原始数据的一个拷贝,地址不会发生变化。如果循环的是映射,那么每次循环中的元素是一个键值对,地址也不会发生变化。

// 切片的 for range 循环示例
slice := []int{1, 2, 3}
for i, v := range slice {
    fmt.Printf("slice[%d] = %d, addr: %p\n", i, v, &v)
}

// 输出结果:
// slice[0] = 1, addr: 0xc000014160
// slice[1] = 2, addr: 0xc000014160
// slice[2] = 3, addr: 0xc000014160

// 映射的 for range 循环示例
m := map[string]int{"a": 1, "b": 2}
for k, v := range m {
    fmt.Printf("m[%s] = %d, addr: %p\n", k, v, &v)
}

// 输出结果:
// m[a] = 1, addr: 0xc000014168
// m[b] = 2, addr: 0xc000014168

  1. defer 的工作方式和执行顺序:
defer 用于在函数返回之前执行一些操作,通常用于释放资源或记录日志。
多个 defer 语句的执行顺序是先进后出(类似于栈),即最后一个 defer 语句会最先执行,最先定义的 defer 语句会最后执行。

在执行 defer 语句时,函数的返回值已经确定,但是可以通过修改具名返回值来修改函数的返回值。


func demo() (result int) {
    defer fmt.Println("defer 1, result:", result)
    defer fmt.Println("defer 2, result:", result)

    result = 1
    fmt.Println("set result to 1")
    return result + 1
}

// 输出结果:
// set result to 1
// defer 2, result: 2
// defer 1, result: 2
  1. uint 类型溢出
在 Go 语言中,整数类型分为有符号整数类型和无符号整数类型。其中,uint 类型是无符号整数类型,表示非负整数。uint 类型会发生溢出,当一个无符号整数类型的值大于它能够表示的最大值时,它会发生溢出,导致值重新变为 0,然后继续增加。

var x uint8 = 255
fmt.Println(x, x+1, x+2) // 输出:255 0 1

  1. rune 类型介绍
rune 类型是 Go 语言中的内置类型,表示一个 Unicode 码点,也就是一个字符。在 Go 语言中,一个 rune 类型的值通常使用单引号来表示,例如 'a'。rune 类型的底层实现是 int32 类型。

var r rune = '好'
fmt.Printf("%c\n", r) // 输出:好

在上面的示例代码中,定义了一个 rune 类型的变量 r,并将其初始化为 '好',即一个汉字的 Unicode 码点。然后使用 %c 格式化占位符来输出 r 的值,可以看到,输出结果为 好。

  1. Go 中解析 tag 的实现方式和反射原理
Go 中的 tag 是用于给结构体中的字段添加元数据的一种方式。tag 通常用于在运行时解析结构体中的信息,例如在 ORM 库中,tag 可以用于指定数据库中表的名称和字段名。Go 语言提供了反射(reflection)机制,可以在运行时动态地获取类型信息和访问对象的成员和方法,可以通过反射机制实现 tag 的解析。

在 Go 语言中,反射的主要实现方式是使用 reflect 包。 reflect 包提供了两个重要的类型:Type 和 Value。Type 表示类型的元信息,包括名称、大小、方法等;Value 表示值的元信息,包括类型、值、字段、方法等。

当使用反射机制解析结构体时,可以使用 reflect 包中的 TypeOf 和 ValueOf 函数来获取类型和值的元信息。然后,可以通过 TypeOf 函数获取结构体的类型信息,通过 ValueOf 函数获取结构体的值信息。接着,可以使用 Type 和 Value 类型的方法来获取结构体的字段和方法等信息。

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

func main() {
    u := User{Name: "Alice", Age: 18}
    t := reflect.TypeOf(u)
    v := reflect.ValueOf(u)

    for i := 0; i < t.NumField(); i++ {
        field := t.Field(i)
        value := v.Field(i)
        fmt.Printf("%s: %v\n", field.Tag.Get("json"), value.Interface())
    }
}
在上面的示例代码中,定义了一个名为 User 的结构体,其中包含了两个字段:Name 和 Age,并在它们上面添加了 json tag。然后,在 main 函数中,创建了一个 User 类型的变量 u,并使用 reflect.TypeOf 和 reflect.ValueOf 函数获取 u 的类型和值信息。接着,使用 t.NumField 方法获取 User 结构体中的字段数量,并使用 t.Field 方法和 v.Field 方法获取每个字段的类型信息和值信息。最后,使用 field.Tag.Get("json") 方法获取每个字段上的 json tag 的值,并使用 value.Interface() 方法获取每个字段的值。可以看到,输出结果是:

name: Alice
age: 18


t := reflect.TypeOf(User{})
for i := 0; i < t.NumField(); i++ {
    field := t.Field(i)
    jsonTag := field.Tag.Get("json")
    bsonTag := field.Tag.Get("bson")
    fmt.Printf("%s: %s, %s\n", field.Name, jsonTag, bsonTag)
}

  1. 调用函数传入结构体时,应该传值还是指针?
在 Go 语言中,调用函数时传递结构体,应该根据实际情况考虑传值还是传指针。如果结构体较小,传值比较方便,而且可以避免指针的额外开销。但是,如果结构体较大,传指针可以避免结构体的复制,提高程序的性能。

需要注意的是,在 Go 语言中,所有的参数传递都是值传递,包括指针类型。因此,即使传递一个指针类型的参数,函数内部也会复制该指针的值,而不是指针指向的值。

type User struct {
    Name string
    Age  int
}

func modifyUser1(u User) {
    u.Name = "Bob"
    u.Age = 20
}

func modifyUser2(u *User) {
    u.Name = "Bob"
    u.Age = 20
}

func main() {
    u := User{Name: "Alice", Age: 18}

    // 传值
    modifyUser1(u)
    fmt.Println(u) // 输出:{Alice 18}

    // 传指针
    modifyUser2(&u)
    fmt.Println(u) // 输出:{Bob 20}
}

在上面的示例代码中,定义了一个 User 结构体,然后分别定义了两个函数 modifyUser1 和 modifyUser2,分别接收值类型和指针类型的参数。然后定义了一个 User 类型的变量 u,并将其初始化为 {Alice 18}。接着,分别调用了 modifyUser1 和 modifyUser2 函数,并将 u 作为参数传递进去。可以看到,当传值类型的参数时,函数内部对 u 的修改并没有影响到原来的 u 变量。而当传指针类型的参数时,函数内部对 u 的修改可以直接反映到原来的 u 变量上。
  1. 关于 context:

context 结构是怎样的?
context 的使用场景和用途是什么?

在 Go 语言中,context 是一个标准库中的包,用于在不同的 goroutine 之间传递请求范围的数据、取消信号以及截止时间等信息。在并发编程中,context 是非常重要的一部分,能够帮助更好地控制和管理 goroutine 的行为。

context 包中定义了一个 Context 接口,该接口定义了四个方法:

Deadline() (deadline time.Time, ok bool):返回当前 context 的截止时间。
Done() <-chan struct{}:返回一个只读的 channel,当 context 被取消或者超时时,该 channel 会被关闭。
Err() error:返回 context 取消的原因。
Value(key interface{}) interface{}:返回指定 key 对应的值。
Context 接口还有一个实现:context.Background(),用于创建一个空的 context。此外,还有一个 context.WithCancel(parent Context) (ctx Context, cancelFunc func()) 方法,用于创建一个派生的 context 和取消函数,可以通过调用取消函数来取消 context 的操作。

context 的使用场景和用途是多种多样的,例如:

在 HTTP 服务中使用 context 可以传递请求范围的数据,例如用户的认证信息、请求 ID 等,这些数据可以在后续的处理中使用。
在数据库查询操作中使用 context 可以设置查询的超时时间,如果查询时间超过了指定的时间,就会被取消,避免查询操作一直阻塞。
在服务调用中使用 context 可以传递调用链信息,例如追踪 ID、父子调用关系等,可以帮助进行分布式跟踪和日志记录。
在测试中使用 context 可以设置测试的超时时间,如果测试时间超过了指定的时间,就会被取消,避免测试无限制地运行。
  1. unsafe 介绍

unsafe 是 Go 语言中一个特殊的包,其中包含了一些不安全的操作,这些操作涉及指针运算、类型转换等操作,可以绕过 Go 语言中的类型安全检查,直接操作内存,可能会引起一些未定义的行为。

在 Go 语言中,通常不会直接使用 unsafe 包,因为这会导致程序变得不稳定、不可移植,容易出现难以调试和排查的问题。但是,在某些特殊的场景下,可能需要使用 unsafe 包来实现一些高性能的操作,例如:

访问和修改结构体的私有字段
绕过数组边界检查,实现更高效的内存操作
将任意类型的指针转换成 uintptr 类型的指针,进行指针运算
突破 Go 语言的类型系统,实现类型之间的相互转换
在使用 unsafe 包时,需要格外小心,遵循一些基本的规则,例如:

不要对指针进行未经检查的偏移或解引用操作
不要将指针转换成不同类型的指针,除非可以保证类型安全
不要将指针转换成 uintptr 类型的指针,除非必须进行指针运算
总之,在使用 unsafe 包时,需要慎重考虑,并且确保操作的正确性和安全性。如果可能,应该尽量避免使用 unsafe 包,使用 Go 语言的安全机制来保证程序的正确性和稳定性。

package main

import (
	"fmt"
	"unsafe"
)

type Person struct {
	name string
	age  int
}

func main() {
	p := Person{name: "Alice", age: 18}

	// 访问私有字段 name
	namePtr := (*string)(unsafe.Pointer(&p))
	fmt.Printf("name: %v\n", *namePtr)

	// 修改私有字段 name
	*namePtr = "Bob"
	fmt.Printf("name: %v\n", p.name)
}

在这个例子中,定义了一个 Person 结构体,其中包含了两个字段 name 和 age,其中 name 字段是私有的,外部无法直接访问。然后,使用 unsafe 包中的 Pointer 和 unsafe.Pointer 函数,将 Person 结构体的地址转换成一个 unsafe.Pointer 类型的指针,再将这个指针转换成一个 *string 类型的指针,即可访问结构体中的私有字段 name。最后,通过修改指针所指向的值,实现了对私有字段 name 的修改。

需要注意的是,这种方式实现了对私有字段的访问和修改,但同时也绕过了 Go 语言中的类型安全检查,可能会引起一些未定义的行为。因此,建议只在极少数情况下使用 unsafe 包,同时需要格外小心,确保程序的正确性和安全性。

posted @ 2023-04-19 10:14  紫系流月  阅读(83)  评论(0编辑  收藏  举报