- make 和 new 的區別:
make 用於創建切片、映射和通道,返回的是創建類型的引用。
new 用於創建任意類型的指針,返回的是該類型的指針。
// make 用法示例
slice := make([]int, 5) // 創建一個包含5個元素的整型切片
// new 用法示例
var pointer *int
pointer = new(int) // 創建一個整型指針,指向默認值 0
- 數組和切片的區別
數組的長度是固定的,在創建時必須指定長度;切片長度不固定,可以動態擴展。
數組是值類型,複製操作會複製整個數組;切片是引用類型,複製操作只會複製引用,不會複製底層數據。
數組和切片的索引方式不同,數組索引從0開始,切片索引可以從任意位置開始。
// 數組的創建和賦值示例
var arr [3]int // 創建一個包含3個元素的整型數組
arr[0] = 1
arr[1] = 2
arr[2] = 3
// 切片的創建和賦值示例
slice := []int{1, 2, 3} // 創建一個包含3個元素的整型切片
- 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
- 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
- uint 類型溢出
在 Go 語言中,整數類型分爲有符號整數類型和無符號整數類型。其中,uint 類型是無符號整數類型,表示非負整數。uint 類型會發生溢出,當一個無符號整數類型的值大於它能夠表示的最大值時,它會發生溢出,導致值重新變爲 0,然後繼續增加。
var x uint8 = 255
fmt.Println(x, x+1, x+2) // 輸出:255 0 1
- rune 類型介紹
rune 類型是 Go 語言中的內置類型,表示一個 Unicode 碼點,也就是一個字符。在 Go 語言中,一個 rune 類型的值通常使用單引號來表示,例如 'a'。rune 類型的底層實現是 int32 類型。
var r rune = '好'
fmt.Printf("%c\n", r) // 輸出:好
在上面的示例代碼中,定義了一個 rune 類型的變量 r,並將其初始化爲 '好',即一個漢字的 Unicode 碼點。然後使用 %c 格式化佔位符來輸出 r 的值,可以看到,輸出結果爲 好。
- 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)
}
- 調用函數傳入結構體時,應該傳值還是指針?
在 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 變量上。
- 關於 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 可以設置測試的超時時間,如果測試時間超過了指定的時間,就會被取消,避免測試無限制地運行。
- 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 包,同時需要格外小心,確保程序的正確性和安全性。