Skip to main content

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 如何高效地拼接字串?

參考:字串拼接效能及原理 | Go 語言高效能程式設計


07 什麼是 rune 型別?

  • ASCII:只需 7 bit,能表示 128 個字元(主要為英文)。
  • Unicode:為世界各種文字系統定義統一編碼,每個字元稱為一個「碼點(Code Point)」。
  • Go 中的 runeint32 的別名,用來表示一個 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 裡的 intint32 是同一個概念嗎?

不是。

  • int 的尺寸與平台位元數相關:
    • 32 位系統上通常是 4 bytes
    • 64 位系統上通常是 8 bytes
  • uint 也同樣依賴作業系統位元數。
  • int8 / int16 / int32 / int64 則是 固定大小
    • int8:1 byte
    • int16:2 bytes
    • int32:4 bytes
    • int64:8 bytes

在需要 明確位寬 或與外部系統(例如資料庫、網路協定)對接時,應優先使用 int32int64 等固定長度型別,而不是平台相關的 int