2026年3月13日 星期五

V8引擎運作原理與垃圾回收機制

 

一樣是今天面是遇到的問題,沒答好,紀錄一下

一、 V8 引擎運作流程:從原始碼到機器碼

V8 引擎(Chrome 與 Node.js 的核心)並非單純的解釋器,它是一個複雜的 JIT (Just-In-Time) 編譯器

  1. 解析 (Parsing):將 JavaScript 原始碼轉化為 AST (抽象語法樹)

  2. 解釋 (Ignition):解釋器將 AST 轉為較低階的 Bytecode (位元組碼) 並立即執行。

  3. 編譯優化 (TurboFan):監控程式碼運行(Profiling),如果某段函式被頻繁呼叫(熱點程式碼),編譯器會將其轉換為優化過的 機器碼 (Machine Code),大幅提升速度。

  4. 去優化 (Deoptimization):如果輸入的資料型別突然改變(例如從 Int 變成 String),V8 會拋棄機器碼,退回到位元組碼執行,這就是效能損耗的來源。


二、 V8 垃圾回收機制 (Garbage Collection, GC)

V8 將記憶體分為 新生代 (Young Generation)老生代 (Old Generation),並採用不同的回收策略:

1. 新生代:Scavenge 演算法

  • 特性:存活時間短、頻繁回收。

  • 機制:將空間平分為 FromTo。新物件存入 From,回收時將存活物件複製到 To,然後清空 From 並角色對調。

  • 晉升 (Promotion):經歷兩次回收仍存活的物件,會被移動到老生代。

2. 老生代:Mark-Sweep & Mark-Compact

  • 特性:存活時間長、空間大。

  • 機制

    • 標記 (Marking):從根節點(如 windowglobal)出發,標記所有可達(Reachable)的物件。

    • 清除 (Sweeping):回收未被標記的記憶體空間。

    • 整理 (Compacting):將分散的物件移動到一塊連續空間,減少「記憶體碎片」。


三、 如何偵測與處理記憶體洩漏 (Memory Leak)

當物件不再被需要,但仍被「根節點」間接引用,導致 GC 無法回收時,就會發生記憶體洩漏。

1. 常見的洩漏場景

  • 全域變數:不小心宣告了 window.data = [...]

  • 未清除的計時器setInterval 內引用了外部變數,但從未調用 clearInterval

  • 閉包 (Closures):內部函式引用了外部大型變數,導致外部變數無法被回收。

  • 脫離文件的 DOM 節點:在 JS 中保留了對 DOM 的引用,即使該 DOM 已從頁面移除。

2. 實戰處理步驟

當你懷疑高併發頁面出現記憶體異常時:

  1. 使用 Chrome DevTools (Performance 面板)

    • 勾選「Memory」並錄製一段操作。

    • 觀察 Heap (堆疊) 曲線,若呈現「階梯式上升」且不會回落,即代表洩漏。

  2. 堆疊快照 (Heap Snapshot)

    • 進入 Memory 面板,點擊 Take snapshot

    • 執行可疑操作後,再拍一張快照。

    • 使用 "Comparison" (比較) 模式,找出在兩次快照之間「新增且未消失」的物件。

  3. 三點定位法

    • 拍下快照 A(初始狀態)。

    • 執行操作並拍下快照 B。

    • 再執行操作並拍下快照 C。

    • 檢查 B 與 C 之間持續增長的物件,這通常就是洩漏點。


四、 程式碼優化建議

  • 弱引用 (WeakMap / WeakSet):如果你需要建立物件與資料的關聯,但不希望影響 GC 回收,請優先使用 WeakMap

  • 解構賦值後手動置空:大型物件處理完後,設定 obj = null 斷開引用鏈。

  • 生命週期管理:在 React 的 useEffect 或 Vue 的 onUnmounted 中,務必移除 Event Listener 或清除 Timer。


前端高併發情境範例

今天面是被問一題,高併發你怎麼處理,我講完後對方不滿意,是後回想才知道對方需要我設立情境,啊我也沒什麼遇過,所以當下完全想像不到,我想說高併發不是後端要處理的嗎,問完AI後才發現前端也有其相對情境,雖然真正遇到時一樣半小時查一下就知道該怎麼寫,但這些公司就是很愛問,所以紀錄一下

前端高併發情境設計指南

在高併發情境下,前端的核心目標不再只是「呈現數據」,而是**「流量削峰」「壓力隔離」**。透過將壓力擋在前端或邊緣端,可以有效防止後端資料庫因瞬間湧入的請求而崩潰。

核心設計原則

  • 動靜分離:將不常變動的 HTML/JS/CSS 與靜態素材(圖片、影片)佈署於 CDN,減少對應用伺服器的直接請求。

  • 請求削峰:利用緩衝、延遲或合併請求的方式,平滑化流量尖峰。

  • 客戶端降級:當系統負載過高時,主動關閉非核心功能(如:預測搜尋、即時進度更新)。


代表性情境範例

1. 電商秒殺/搶購系統 (Rush Sale)

這是最典型的高併發情境,數百萬用戶在同一秒點擊按鈕。

  • 前端應對策略

    • 按鈕防抖與置灰:點擊後立即禁用按鈕,防止重複送出請求。

    • 靜態資源預熱:在活動開始前,利用 Service Worker 預先下載活動頁面所需的圖片與腳本。

    • 排隊等待機制:前端發送請求後進入輪詢或 WebSocket 狀態,顯示「排隊中」而非直接報錯,提升用戶心理預期。

    • 驗證碼與異步化:透過驗證碼強制拉長用戶的操作路徑,分散瞬間請求壓力。

2. 大數據流與無限捲動列表 (Infinite Scroll)

在社交平台(如 Threads、Instagram)或新聞資訊流,快速滑動會產生大量 API 請求。

  • 前端應對策略

    • 虛擬列表 (Virtual List):只渲染當前視窗(Viewport)可見的 DOM 節點。即使有 10 萬筆資料,記憶體中也只維持 10-20 個節點,避免瀏覽器崩潰。

    • 分頁請求與預加載:設定滑動閾值,在用戶滑到剩下 20% 時才發送下一頁請求。

    • 請求取消 (AbortController):當用戶快速切換分類或滑動過快時,主動取消尚未完成的舊請求。

3. 即時互動與直播間彈幕 (Live Streaming Chat)

直播間數萬人同時發言時,頻繁的 DOM 更新與網路通訊是效能瓶頸。

  • 前端應對策略

    • 彈幕池緩衝:不要收到一則彈幕就渲染一次。建立緩衝池(Buffer),每隔 500ms 批量渲染一次。

    • 頻率限制與過濾:前端根據等級或關鍵字過濾次要訊息,或在本地限制用戶的發言頻率。

    • Canvas 渲染:針對海量彈幕,放棄使用 DOM 節點,改用 Canvas 繪製以獲得更高的渲染幀數。


總結技術清單

技術手段解決的問題應用範例
CDN / Edge減少伺服器頻寬壓力全域靜態檔案分發
Service Worker離線緩存與預加載秒殺頁面預熱
Debounce / Throttle過度頻繁的觸發搜尋框、視窗縮放
Request Batching減少 HTTP 請求數量點讚數合併計算
所以這樣看起來前端要做的就是防止伺服器端爆炸減少需求發生.....早說嗎,我想說高併發關前端啥事==

2026年3月12日 星期四

Golang, gRPC(protobuf)學習筆記

這似乎也是一們必修的課,不然json傳來傳去不香嗎


首先要建立資料型態 p5_grpc/simple/pb/simple.proto,為了辨識這副檔名,我安裝了vscode-proto3套件

Greeter名字可以自取,但在使用時例如在之後的server: pb.Unimplemented[自取姓名]Server , pb.Register[自取姓名]Server就會變成這樣

剩下就像interface定義輸入輸出模型

// 指定使用最新的 protobuf 第三版語法
syntax = "proto3";

// 定義 protobuf 內部的包裝名稱,避免跟別人寫的 message 命名衝突
package simple;

// 關鍵設定:告訴編譯器,生成的 Go 程式碼要放在哪一個 Go package 中
// 這裡我們指定生成到專案中 p5_grpc/simple/pb 這個 package
option go_package = "p5_grpc/simple/pb";

// 定義一個名為 Greeter 的「服務」(Service),就像是定一個 API 介面
service Greeter {
// 定義一個名為 SayHello 的 API 方法
// 它的輸入是 HelloRequest,輸出是 HelloReply
rpc SayHello (HelloRequest) returns (HelloReply) {}
}

// 定義客戶端傳給伺服器的資料結構 (請求)
message HelloRequest {
// 第一個欄位是字串型態,名稱為 name,數字 "1" 是它在二進位傳輸時的順序標籤
string name = 1;
}

// 定義伺服器回傳給客戶端的資料結構 (回應)
message HelloReply {
// 同樣地,定義一個字串欄位 message,順序標籤為 1
string message = 1;
}

在proto設定完成後還要輸入一段指令好產生simple.pb.go和simple_grpc.pb.go,千萬別傻傻的自己打,然後protoc指令不知道為什麼電腦理本來就有,不用特別裝什麼,改天沒有在研究吧


# 確保你已經有安裝 protoc 與 go 的 protoc 外掛
# 實際生成指令如下:
protoc --go_out=. --go_opt=paths=source_relative \
--go-grpc_out=. --go-grpc_opt=paths=source_relative \
simple/pb/simple.proto

接著要來建立server: p5_grpc/simple/server/main.go

package main

import (
"context"
"log"
"net"

// 匯入上一步工具幫我們自動產生的 pb package
pb "p5_grpc/simple/pb"

"google.golang.org/grpc"
)

// 定義一個我們自己的 sever 結構體
type server struct {
// 必須嵌入這個,這是 gRPC 為了向前相容的強制規定
pb.UnimplementedGreeterServer
}

// 正式實作 proto 中定義的 SayHello 方法!
// 注意:函數名稱和傳入傳出的參數型別,都必須跟自動生成的 pb 程式碼一致
func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
// in.GetName() 可以安全地拿到客戶端傳來的 name 欄位
log.Printf("收到客戶端的請求名稱: %v", in.GetName())

// 把 "Hello " 加上客戶端傳來的名字,包裝成 HelloReply 回傳給客戶端
return &pb.HelloReply{Message: "Hello " + in.GetName()}, nil
}

func main() {
// 1. 在本地端的 50052 port 開啟一個 TCP 監聽器
lis, err := net.Listen("tcp", ":50052")
if err != nil {
log.Fatalf("無法開啟監聽: %v", err)
}

// 2. 建立一台全新的 gRPC 伺服器
s := grpc.NewServer()

// 3. 把我們自己寫的 server 邏輯結構體,註冊到 gRPC 伺服器上
pb.RegisterGreeterServer(s, &server{})
log.Printf("簡單版 gRPC Server 正在監聽: %v", lis.Addr())

// 4. 開始伺服器以接受客戶端連線 (程式會卡在這裡持續運行)
if err := s.Serve(lis); err != nil {
log.Fatalf("伺服器啟動失敗: %v", err)
}
}

最後再來個client就搞定: p5_grpc/simple/client/main.go

package main

import (
"context"
"log"
"time"

// 同樣地,匯入自動產生的 pb package
pb "p5_grpc/simple/pb"

"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
)

func main() {
// 1. 設定並連接到目標伺服器的位置 (localhost:50052)
// grpc.WithTransportCredentials(insecure.NewCredentials()) 代表我們不使用 SSL/TLS 加密連線 (僅供本地測試用)
conn, err := grpc.Dial("localhost:50052", grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
log.Fatalf("無法連線至伺服器: %v", err)
}
// 確保程式結束前會關閉連線釋放資源
defer conn.Close()

// 2. 透過連線建立一個存取 Greeter 服務的客戶端實例
c := pb.NewGreeterClient(conn)

// 3. 建立一個有 1 秒超時限制的 Context
// 這是為了避免連線卡住,如果在 1 秒內沒完成就會自動取消
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()

log.Println("準備呼叫 SayHello 方法...")

// 4. 透過客戶端實例呼叫遠端的 SayHello 方法,並把自己的名字 (Gopher) 當作參數傳過去
r, err := c.SayHello(ctx, &pb.HelloRequest{Name: "Gopher"})
if err != nil {
log.Fatalf("呼叫失敗: %v", err)
}

// 5. 成功收到回應後,印出伺服器回傳的內容 (Message 欄位)
log.Printf("收到伺服器回傳: %s", r.GetMessage())
}

最後就是相繼把server, client啟動,就會看到Hello Gopher。大概就是這樣吧,明天再來研究最後一堂課MQ




2026年3月11日 星期三

Golang, Goroutine學習筆記

自己學習起來感覺跟之前碰Thread有點像,然後去詢問了一下差別,最大差距應該是系統佔用程度,另外就是goroutine(協程)是由go runtime(這兩個單字超像)內建排程去調度,必非是由os執行緒,我們來看AI怎麼說

Goroutine 與傳統 OS 的多執行緒 (Multi-threading / OS Thread) 的差別,主要在於對「系統資源的消耗」與「排程機制」的不同。以下為您整理幾個最核心的差異:

1. 資源消耗大小 (Memory Consumption)

  • Goroutine(協程):非常輕量化。初始建立時通常只需要約 2KB 的堆疊 (Stack) 記憶體空間,並且會根據需求自動擴充或縮小。
  • OS 執行緒 (OS Thread):傳統作業系統的執行緒,建立時通常會分配固定的記憶體大小(約為 1MB 到 8MB 不等)。因此,如果開啟一萬個 Thread 會吃掉 GB 級的記憶體甚至崩潰,但開啟一萬個 Goroutine 對 Go 來說卻非常輕鬆。

2. 建立與銷毀的成本 (Creation/Destruction Cost)

  • Goroutine(協程):由 Go 語言自己的 Runtime(執行階段)在使用者空間 (User Space) 來管理,建立和銷毀的成本極低,就相當於程式語言中一般變數的記憶體分配與回收。
  • OS 執行緒 (OS Thread):必須向作業系統核心發出請求 (System Call) 才能建立或銷毀。這個過程牽涉到切換至核心空間 (Kernel Space),開銷相當昂貴。

3. 排程者與上下文切換 (Context Switch & Scheduler)

  • Goroutine(協程):由 Go Runtime 內建的排程器調度 (稱為 GMP 模型) 處理。你可以把它想像成「程式語言自己內部的執行緒」。當某個 Goroutine 遇到阻塞(例如等待 Channel、網路 I/O 或 Sleep)時,Go 會將它掛起,然後把同一個底層的 OS 執行緒讓給其他準備好的 Goroutine 使用(這稱為 M:N 排程)。切換過程只需保存和恢復極少數的暫存器資料,速度極快 (約幾十奈秒)。
  • OS 執行緒 (OS Thread):完完全全由作業系統的排程器 (OS Scheduler) 負責調度。執行緒的切換需要保存大量的狀態(包含硬體暫存器、程式計數器、記憶體分頁表等),且必須切換到作業系統底層(Kernel Mode),速度相對較慢許多 (約幾微秒,相差百倍以上)。

總結

Goroutine 就是站在傳統 OS 執行緒的肩膀上,用更聰明、更輕量的方式來達到高並發 (High Concurrency) 的效果。因為它的成本極低,所以我們在 Go 裡面非常鼓勵**「不要透過共用記憶體來通訊,而是透過通訊 (Channel) 來共用記憶體」**。

以下就是作過的幾個練習

第一個練習goruntine(channel)+select

package main

import (
"log"
"time"
)

func main() {
// 不是在執行緒了的size
ch := make(chan int)
go func() {
time.Sleep(3 * time.Second)

ch <- 2
ch <- 3
}()
select {
// 第一次觸發
case v, ok := <-ch:
if ok {
log.Printf("I got %v", v) // 第一次觸發 I got 2 // end
}
}

針對chan取資料最後有完整補充


練習二,多個worker,想像就是多個工人開工,執行速度會比較快

make(chan int )第二個參數是緩衝區,簡單說可以預先設定能放幾比資料進去,不設定就是0,那就會排隊,資料被取走了下一筆資料才能進來,最後有個worker_pool會練習到

package main

import (
"fmt"
"time"
)

func writeIntData(intChan chan int) {
for i := 1; i <= 3; i++ {
fmt.Printf("WriteIntData = %v\n", i)
intChan <- i
}
close(intChan)
}

func writeSecData(secChan chan int) {
for i := 1; i <= 3; i++ {
fmt.Printf("WriteSecData ==> %v\n", i)
secChan <- i
}

close(secChan)
}

func main() {
intChan := make(chan int, 3)
secChan := make(chan int, 10)
go writeIntData(intChan)
go writeSecData(secChan)

//設定逾時
timeout := time.After(5 * time.Second)
Loop:
for {
select {
case v, ok := <-intChan:
if ok {
fmt.Printf("ReadIntData = %v\n", v)
}
case n, ok := <-secChan:
if ok {
fmt.Printf("ReadSecData ==> %v\n", n)
}
case <-timeout:
fmt.Println("timeout")
break Loop
default:
//在這裡可決定是否結束
fmt.Printf("Nothine input\n")
}
}
fmt.Println("End")
}


第三題,worker_pool,多工協作,這裡有三個worker,五個任務,所以假如任務內容相同,應該兩run就可以完成,range jobs就是一種取資料的方式,前提是chan要cloase

一樣來看一下AI怎麼說

  • 為何不用 range:如果要用 range results,前提是 results channel 必須被 close() 關閉。但在目前的設計中,是由 3 個不同的 worker 並行寫入 results。如果要關閉它,必須引入 sync.WaitGroup 來等待所有 worker 完成後才進行 close(results)。既然這裡已經知道只收 5 個,用 for 迴圈直接收是最簡潔的做法。
  • 為何不用 selectselect 通常用於「同時監聽多個 Channel」或者「需要設定 Timeout 機制」。在這裡只有單一一個 results channel 需要讀取,且我們願意阻塞直到拿滿 5 個結果為止,因此不需要使用 select

特別注意,如果這邊不給緩衝區,五筆資料無法一次塞入,就不會跑到下方開始監聽results ,但這裡設計worker做完任務需要回報results ,沒有人把results收走,worker就無法進行下一輪工作,如此就會產生死鎖!

package main

import (
"fmt"
"time"
)

// worker 負責從 jobs channel 接收任務並將結果放入 results channel
func worker(id int, jobs <-chan int, results chan<- int) {
for j := range jobs {
fmt.Printf("Worker %d 開始處理任務 %d\n", id, j)
time.Sleep(time.Second) // 模擬工作耗時
fmt.Printf("Worker %d 完成任務 %d\n", id, j)
results <- j * 2 // 假設工作結果是將數值乘以 2
}
}

func main() {
start := time.Now() // 記錄開始時間
const numJobs = 5
jobs := make(chan int, numJobs) // 建立一個有緩衝的 Channel ,一次可以塞五個任務,因為worker要把資料塞回results channel,但一直等不到人來接(Line37),就會死鎖
results := make(chan int, numJobs)

// 啟動 3 個 Worker Goroutine
for w := 1; w <= 3; w++ {
go worker(w, jobs, results)
}

// 派發 5 個任務
for j := 1; j <= numJobs; j++ {
jobs <- j
}
close(jobs) // 所有任務都已派發,關閉 jobs channel

// 從結果 channel 中收集 5 個結果
for a := 1; a <= numJobs; a++ {
res := <-results
fmt.Printf("收到任務結果: %d\n", res)
}

elapsed := time.Since(start) // 計算總耗時
fmt.Printf("總執行時間: %s\n", elapsed)
}


第四題,AI取名goroutine_pipe,你在不同任務需要多工,但又需要串連,就可以這樣寫

package main

import "fmt"

// 第一階段:產生數字
func generateNumbers(nums ...int) <-chan int {
out := make(chan int)
go func() {
for _, n := range nums {
out <- n
}
close(out) // 發送完畢關閉 channel
}()
return out // 1 -> 2 -> 3 -> 4 -> 5 -> 6
}

// 第二階段:將收到的數字進行平方
func squareNumbers(in <-chan int) <-chan int {
out := make(chan int)
go func() {
for n := range in {
out <- n * n
}
close(out)
}()
return out // 1 -> 4 -> 9 -> 16 -> 25 -> 36
}

// 第三階段:過濾偶數
func filterEvenNumbers(in <-chan int) <-chan int {
out := make(chan int)
go func() {
for n := range in {
if n%2 == 0 {
out <- n
}
}
close(out)
}()
return out // 4 -> 16 -> 36
}

func main() {
// 建立 Pipeline: 產生 -> 平方 -> 過濾偶數
nums := generateNumbers(1, 2, 3, 4, 5, 6)
squares := squareNumbers(nums)
evens := filterEvenNumbers(squares)

// 印出最終結果
for n := range evens {
fmt.Println("Pipeline 輸出結果:", n)
}
}


第五題,典型生產者與消費者,如果上面都看懂了這題就沒啥好講的

package main

import (
"fmt"
"time"
)

func producer(ch chan<- int) { // chan<- int 只能寫入,進到channel
for i := 1; i <= 5; i++ {
fmt.Printf("生產者產生資料: %d\n", i)
ch <- i
time.Sleep(500 * time.Millisecond) // 模擬生產時間
}
close(ch) // 生產完畢後關閉 Channel,通知消費者不會再有資料
}

func consumer(ch <-chan int) { // <-chan int 只能讀取,從channel出來
// 使用 for range 不斷接收資料,直到 channel 被關閉
for data := range ch {
fmt.Printf("消費者收到資料: %d\n", data)
}
fmt.Println("消費者完成接收")
}

func main() {
ch := make(chan int) // 建立一個無緩衝的 Channel
// 啟動生產者 Goroutine
go producer(ch)
// 主力 Goroutine 當作消費者
consumer(ch)
}


第六題,讀寫鎖增加效能,使用WaitGroup來進行任務等待

package main

import (
"log"
"math/rand"
"sync"
"time"
)

// Bank 結構定義了一個具備併發安全機制的帳戶
type Bank struct {
balance int
mux sync.RWMutex // 使用讀寫鎖 (RWMutex) 以優化讀取效能
}

// Deposit 存款方法,使用寫入鎖 (Lock)
// 寫入鎖是互斥的,同一時間只能有一個 goroutine 執行存款
func (b *Bank) Deposit(amount int) {
b.mux.Lock() // 取得寫入鎖,其他 Read/Write 都會被阻塞
time.Sleep(time.Second) // 模擬耗時的處理 (1秒鐘)
b.balance += amount
b.mux.Unlock() // 釋放鎖
}

// Balance 查詢餘額方法,使用讀取鎖 (RLock)
// 讀取鎖允許多個 goroutine 同時讀取,但會被寫入鎖阻塞
func (b *Bank) Balance() (balance int) {
b.mux.RLock() // 取得讀取鎖,不影響其他查詢者
balance = b.balance
b.mux.RUnlock() // 釋放鎖
return
}

func main() {
var wg sync.WaitGroup
b := &Bank{}

n := 10 // 修改為 10 來測試

// 寫入操作:啟動 n 個 goroutine 同時存款
wg.Add(n)
for i := 1; i <= n; i++ {
go func() {
b.Deposit(1000)
log.Printf("Write: deposit amount: %v", 1000)
wg.Done()
}()
}

// 讀取操作:啟動 n 個 goroutine 來讀取餘額
wg.Add(n)
for i := 1; i <= n; i++ {
go func() {
// 生成隨機等待時間。注意:此處 rand.Intn(1000) 最長可能睡 999 秒!
// 如果 n 較小,很可能所有 goroutine 都隨機到了較大的秒數。
sleepDuration := time.Duration(rand.Intn(n)) * time.Second
time.Sleep(sleepDuration)

log.Printf("Read : balance: %v (After sleeping %v)", b.Balance(), sleepDuration)
wg.Done()
}()
}

wg.Wait() // 等待所有任務完成
}

深入淺出:Golang sync.RWMutex

sync.RWMutex 是 Go 語言標準庫提供的「讀寫互斥鎖」(Read-Write Mutex)。它的核心理念是基於一個常見的開發情境:資料通常是「讀多寫少」的。
核心機制

sync.RWMutex 允許同時有多個讀取者,但在寫入時會進入排他狀態。其互斥規則如下:

讀鎖(RLock):當沒有任何「寫鎖」時,多個 goroutine 可以同時持有讀鎖。這能大幅提升讀取效能。

寫鎖(Lock):是排他的(Exclusive)。當一個 goroutine 持有寫鎖時,其他任何讀鎖或寫鎖都必須等待。

RWMutex vs. Mutex 的差異

不要過度優化:如果讀寫比例差不多,直接用 sync.Mutex 即可,因為 RWMutex 內部維護狀態的開銷比 Mutex 稍微複雜一點。

避免死鎖:千萬不要在持有 RLock 的同一個 goroutine 裡去嘗試獲取 Lock,這會導致程式永久卡死。

鎖的粒度:儘量縮小鎖定的範圍(臨界區),只鎖定必要的資料處理邏輯。


補充針對chan取資料方式

AI回答,基本上就三種,select是針對多channel做的,所以第一題就是脫褲子放屁,直接取就好

Channel 取資料的三種常見方式

從 Channel 中取資料 (Receive) 基本上有這三種常見寫法,會根據情境而有所不同:

1. 基礎取值 (搭配 ok idiom 判斷是否關閉)

最基本的讀取方式,可以順便檢查 channel 是否已經被關閉且內部沒有資料了。

go
v, ok := <-ch
if !ok {
// ok == false 代表 channel 已經被 close(),且裡面沒東西了
fmt.Println("Channel closed")
} else {
fmt.Println("Got:", v)
}
  • 適用情境:單次讀取,需嚴格確認拿出來的是有效資料,還是因為 Channel 被關閉而拿到的「預設零值」。

2. 結合 for range 迴圈 (最推薦的走訪方式)

Go 語言提供了對 Channel 的 range 語法,它會不斷且阻塞地從 Channel 取值,直到該 Channel 被 close() 關閉為止,迴圈才會自動結束。

go
// 另一端有人負責送資料,送完後會 close(ch)
for v := range ch {
fmt.Println("Got:", v)
}
// 當 ch 被 close 時,會自動跳出這個迴圈
  • 適用情境資料數量未知,發送方(Sender)承諾在送完所有資料後會呼叫 close()。像你 worker_pool 裡的 for j := range jobs 就是最好的例子。

3. 結合 select 多路復用 (Multiplexing)

當你有「兩個以上」的 Channel 要同時監聽,或者需要實現「非阻塞讀取 (Default)」、「逾時機制 (Timeout)」,就必須使用 select

go
select {
case v1 := <-ch1:
fmt.Println("從 ch1 收到:", v1)
case v2 := <-ch2:
fmt.Println("從 ch2 收到:", v2)
case <-time.After(3 * time.Second):
fmt.Println("3 秒內都沒有任何 Channel 送資料過來,超時了!")
// default:
// 如果這裡有 default,那上面都沒有資料時,就不會阻塞,立刻執行這裡 (非阻塞讀取)
}
  • 適用情境:需要監聽多個 Channel、需要設定 Timeout、或是需要非阻塞地嘗試讀取時。

總結來說:

  • 已知數量:直接用 for i := 0; i < N; i++ { <-ch }
  • 未知數量,但有人幫忙 close:用 for v := range ch { ... }
  • 多重 Channel 或防卡死 (Timeout):用 select { case <-ch: ... }