Go 面試題一(基礎語法)
來源:geektutu
01 = 和 := 的差別?
=:賦值給「已定義」的變數。:=:宣告並賦值,會在當前作用域中「定義新變數」。
var a int
a = 1 // 使用 = 給已存在變數賦值
b := 2 // 使用 := 定義並賦值新變數
02 指標(pointer)的作用
指標是「存放變數記憶體位址」的變數。在 32 / 64 位系統上,指標本身分別固定佔用 4 / 8 個位元組。
主要用途:
- 獲取變數的值(透過解參考)
- 修改外部變數的值(模擬「傳址呼叫」)
- 作為方法接收器,避免大量拷貝,並可修改接收者狀態
package main
import "fmt"
func main() {
a := 1
p := &a // 取址:&
fmt.Printf("%d\n", *p) // 取值:*
}
指標用於修改外部變數:
// 交換函式
func swap(a, b *int) {
*a, *b = *b, *a
}
指標作為方法接收器:
type A struct{}
func (a *A) Fun() {
// ...
}
03 Go 允許多個回傳值嗎?
允許。Go 函式可以回傳多個值,常見模式是「回傳結果 + error」:
func doSomething() (int, error) {
// ...
return 0, nil
}
04 Go 有「異常」型別嗎?
Go 沒有傳統 try...catch,而是以 error 介面搭配顯式錯誤處理。
val, err := funcDemo()
if err != nil {
fmt.Println(err)
return
}
可以使用 errors.New 建立自訂錯誤,只要實作 Error() string 方法即可:
type errorString struct {
s string
}
func (e *errorString) Error() string {
return e.s
}
// 建構函式
func New(text string) error {
return &errorString{text}
}
05 什麼是協程(Goroutine)?
Goroutine 是使用者態的輕量級執行緒,是 Go 調度的基本單位。
- 透過在函式前加上
go關鍵字啟動:
go someFunc()
- Goroutine 以非常小的堆疊(例如 2KB 或 4KB)啟動,並會隨需求自動伸縮。
- 可輕易同時啟動數十萬個 Goroutine。
06 如何高效地拼接字串?
07 什麼是 rune 型別?
- ASCII:只需 7 bit,能表示 128 個字元(主要為英文)。
- Unicode:為世界各種文字系統定義統一編碼,每個字元稱為一個「碼點(Code Point)」。
- Go 中的
rune是int32的別名,用來表示一個 Unicode 碼點。
Go 字串的底層是 []byte(8 bit),不是 []rune。
sample := "我愛GO"
runeSamp := []rune(sample)
runeSamp[0] = '你'
fmt.Println(string(runeSamp)) // "你愛GO"
fmt.Println(len(runeSamp)) // 4
08 如何判斷 map 中是否包含某個 key?
使用「逗號 ok」寫法:
var sample map[int]int
if _, ok := sample[10]; ok {
// key 10 存在
} else {
// key 10 不存在
}
09 Go 支援預設參數或可選參數嗎?
不支援 C++ / Python 那種形式的「預設參數」與「具名可選參數」。
常見替代方式:
- 使用 結構體參數,搭配建構函式或選項模式。
- 使用 可變參數
...T接受一組值。
// 可以傳入任意數量的整數參數
func sum(nums ...int) {
total := 0
for _, num := range nums {
total += num
}
fmt.Println(total)
}
info
func sum(nums []int) { ... } 的特性比較
| 特性 | nums ...int (可變參數) | nums []int (切片參數) |
|---|---|---|
| 傳入多個值 | sum(1, 2, 3) (直觀、簡潔) | 不允許 |
| 傳入一個切片 | sum(s...) (需要展開) | sum(s) (直接傳入) |
| 不傳任何參數 | sum() (合法,nums 為 nil) | 不允許,必須傳 nil 或 []int |
10 defer 的執行順序
defer的呼叫會在「當前函式 return 之後、真正返回呼叫者之前」執行。- 多個
defer以「後進先出(LIFO)」順序執行。 - 對於「有名回傳值」,
defer內可以修改回傳值。
範例一(無名回傳值,不會被 defer 修改):
func test() int {
i := 0
defer func() {
fmt.Println("defer1")
}()
defer func() {
i += 1
fmt.Println("defer2")
}()
return i
}
func main() {
fmt.Println("return", test())
}
// 輸出:
// defer2
// defer1
// return 0
範例二(有名回傳值,可被 defer 修改):
func test() (i int) {
i = 0
defer func() {
i += 1
fmt.Println("defer2")
}()
return i
}
func main() {
fmt.Println("return", test())
}
// 輸出:
// defer2
// return 1
11 如何交換兩個變數的值?
- 一般變數:
a, b = b, a - 指標變數:
*a, *b = *b, *a
// 一般變數
a, b := 1, 2
a, b = b, a
// 指標
pa, pb := &a, &b
*pa, *pb = *pb, *pa
12 Go 語言的 tag 有什麼用途?
tag 是結構體欄位上的額外標註資訊(字串),常用於:
json:序列化 / 反序列化時對應的欄位名稱db:ORM(例如sqlx)對應資料庫欄位名form:Web 框架(例如gin)對應表單欄位binding:搭配form使用,如binding:"required"表示為必填
type User struct {
Name string `json:"name" db:"name" form:"name" binding:"required"`
Age int `json:"age" db:"age" form:"age"`
}
13 如何取得結構體所有欄位的 tag?
可透過 反射(reflect) 取得:
package main
import (
"fmt"
"reflect"
)
type Author struct {
Name int `json:"Name"`
Publications []string `json:"Publication,omitempty"`
}
func main() {
t := reflect.TypeOf(Author{})
for i := 0; i < t.NumField(); i++ {
f := t.Field(i)
fmt.Println(f.Name, f.Tag)
}
}
14 如何判斷兩個字串切片([]string)是否相等?
可以使用 reflect.DeepEqual:
import "reflect"
func equalStrings(a, b []string) bool {
return reflect.DeepEqual(a, b)
}
但 反射開銷較大,高效能場景建議自行迭代比較長度與每個元素。
15 結構體列印時 %v、%+v、%#v 的差別
%v:只列印欄位值。%+v:列印欄位名與值。%#v:列印型別名稱、欄位名與值(Go 語法表示)。
type User struct {
Name string
Age int
}
u := User{Name: "Tom", Age: 18}
fmt.Printf("%v\n", u) // {Tom 18}
fmt.Printf("%+v\n", u) // {Name:Tom Age:18}
fmt.Printf("%#v\n", u) // main.User{Name:"Tom", Age:18}
16 Go 語言中如何表示列舉值(enum)?
透過常數搭配 iota:
const (
B = 1 << (10 * iota)
KiB
MiB
GiB
TiB
PiB
EiB
)
iota 會在每個 const 宣告區塊內,自 0 開始自動遞增。
17 空結構體 struct{} 的用途
空結構體本身 不佔用任何記憶體空間,常見用途:
1. 使用 map 模擬集合(Set)
type Set map[string]struct{}
func main() {
set := make(Set)
for _, item := range []string{"A", "A", "B", "C"} {
set[item] = struct{}{}
}
fmt.Println(len(set)) // 3
if _, ok := set["A"]; ok {
fmt.Println("A exists") // A exists
}
}
2. 作為 chan 的訊號類型
func main() {
ch := make(chan struct{}, 1)
go func() {
<-ch
// 做一些事情
}()
// 只表示「事件發生」,不需要任何數據
ch <- struct{}{}
}
3. 僅有方法的結構體
type Lamp struct{}
func (l *Lamp) On() {}
func (l *Lamp) Off() {}
18 Go 裡的 int 和 int32 是同一個概念嗎?
不是。
int的尺寸與平台位元數相關:- 32 位系統上通常是 4 bytes
- 64 位系統上通常是 8 bytes
uint也同樣依賴作業系統位元數。int8/int16/int32/int64則是 固定大小:int8:1 byteint16:2 bytesint32:4 bytesint64:8 bytes
在需要 明確位寬 或與外部系統(例如資料庫、網路協定)對接時,應優先使用 int32、int64 等固定長度型別,而不是平台相關的 int。