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 包,同時需要格外小心,確保程序的正確性和安全性。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章