2026年3月11日 星期三

Golang, Goroutine學習筆記

自己學習起來感覺跟之前碰java 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: ... }


2026年2月24日 星期二

HD鬼故事N+6最終章

看標題應該想得到,只是沒想到來得這麼快。


就在過完年的第一天,小弟我就被火了。這故事怎麼開始的呢?就讓我娓娓道來。


這次故事接著第五集的後面開始。那位新進(要取代我的)工程師(後面就叫他新主管吧),在大概 review 完當時的專案之後,向公司提出他需要一個半月來改善目前的狀況。


我得知這個消息時,心中還暗自嘲笑:以公司高層那種急躁的程度,應該不會接受這一個半月的空窗期吧?但沒想到小丑竟然是我自己。上面不但答應了,而且這一個半月真的沒有任何動作,就安心地讓他們慢慢看、慢慢修。這期間幾乎看不到有任何程式碼上版,整個專案就這樣放空了一個半月。


在這個同時,我也沒有閒著,我因為長時間處裡這新專案,導致舊專案累積了大量的工作,另外兩位負責的同事加我協助處裡。我同時還要到處支援其他專案(沒錯,我們公司還有很多其他的小專案,也就是從那位逛街被 fire 的學姊那繼承下來的)


至於這個重構專案,就交由那兩位新進工程師主要負責,看著他們就這樣爽過了一個半月。而且最聰明的一點是:那位新主管,在這一個半月當中就會剛好通過試用期。這幾乎是保證他能通過試用的條款。


這一個半月基本上很快就過去了,也沒見他們提交什麼成果。我想他們做的最大貢獻,就是把整個專案進行細小功能的拆分上trello。這一個半月幾乎沒什麼建樹,UI一樣被開一大堆,BUG 一樣被開一大堆啊......


最後還是回歸到之前的開發模式:

1. 工程師負責修理

2. 進行測試、測試、再測試

3. 產生並返回 bug

4. 工程師再繼續修復

......


一樣啊,只是因為卡片變小了,所以不會有為什麼被開這麼多bug的錯覺,但這問題應該要算PM頭上啊!!!!!然後這個新主管通過試用期時,公司竟然完全沒有詢問過我這個現任主管的意見!!!!!唉~算了,反正這就是他們想要的結果。

這個事情一直到大概一月中的時候,我那邊的專案忙到一個程度了。這時候發生了一件事情:


這個新主管突然生了一場大病。其實我們身為工程師,看卡片(Task card)和程式碼大概都感覺得出來,他到後期有點疲倦,甚至開始擺爛。我是怎麼看出端倪的呢?他禮拜三請病假,禮拜四沒請假,禮拜五又請病假,說他真的撐不下去了。


我想說如果你禮拜五嚴重到沒辦法工作,那禮拜四肯定也沒辦法,那你禮拜四不是白嫖嗎;再回去看他上傳的內容(Commit log),那個禮拜他沒上任何 Code,等於擺爛了一週,白嫖一週薪水。我想,專案不能順利完成總不能怪到我頭上吧?再者是他擺爛成這樣,公司都看不到嗎?


在這個時間點,因為原本一月底專案要趕著上線,PM 終於意識到進度真的不行,就請我回來支援重構專案(啊人員是你們親自調度的,這也應該算PM的鍋啊)。其實早已在一月初,我手邊的兩位同仁已經有足夠能力應付舊專案的功能開發,當時我反而沒事做。


而我一直去詢問那位新進主管,問他重構專案有沒有需要幫忙的地方。一連問了兩個禮拜,他都回答「沒有」,所以我那段時間等於放空了兩個禮拜。


之後發生了一件更搞笑的事情,那位 PM 到處去問人知不知道我在幹嘛。他問遍了所有人,甚至問到財務都來問我底下的工程師。但問來問去就是不來問我本人。


我組員也反問他:「為什麼不直接去問本人?」


結果那位 PM 回覆:「這是我個人想要了解他在做什麼,好安排進度,不是上面的意思,我怕直接問他,他會多想。」


你知道嗎,我此刻信以為真了。


總之一月初,我被要求要開始支援專案,我跟另外兩位同仁把舊專案的任務全部完成,就投入這一個專案中。記得當時有新增 10 個任務,我們五個人,想說一人一張2run,肯定可以在月底就跑完,但最後結果是我一個人就完成了 6 張。而且 bug 超少。我想說這應該也是我的加分項。論規模,致少也是其他人的三倍


接著,事情來到了二月初。二月初的時候,這位新進的工程師打電話給我。他跟我說,其實早在一月中時,PM 就曾問他:「要讓他(指我)留到什麼時候?」


我聽了覺得超級傻眼。這位新主管現在明顯在擺爛,難道你們都看不出來嗎?竟然還反過來問他要讓我留到什麼時候。


當時那位新主管跟我說,這幾個月運作下來,他也知道這間公司很有問題,所以他不太想接主管。我聽完後表示理解,但基於保護團隊的立場,我建議他要跟 PM 講清楚,一方面也是為了保護我自己,你大不了是一走,但一次走兩個人對團隊很傷。


我的私心是:如果他真的不想接,或許我就可以留下來繼續接手。(但我事後驗證他根本沒有說,但也沒有差了,後面會講),接著事情就發生了。


   因為他當時也不想善後,所以希望讓我留得越久越好。當時開出的日期是一月底,但他跟我提這件事時已經是二月初了,我就想說這一關應該是過了,公司應該有看到我的努力。


   接著開心過尾牙,然後接著過年。那過完年,最大的轉折就開始了。開工第一天,早上我還在處理自己的私事,正中午想說要開始辦正事了,就接到財務的訊息說要通電話,時間還挑得真好。我當下就警覺到似乎不太妙,果然是收到要資遣的消息。


對方還詢問我知不知道被資遣的原因。我當然不能說是因為我知道上面很討厭我,所以只回說不知道。他接著說:「你去年出了很多包巴拉巴拉,所以公司找了人要來取代你。公司當然也想過要不要給你機會轉為一般工程師,但你自己想想,公司給了你多少次機會,你都沒有好好把握。」


我心裡的 OS 是:我一個人單刷六張卡。先不說主管到底要不要寫 code,光是效率,我就不知道你們到底有什麼好說嘴的,而且我一直有去要工作啊,是他(新主管)不給,幹又算我的了。但,既然木已成舟,我覺得也沒必要跟財務爭辯,畢竟他也只是傳話的,並非決策層,就想說算了、接受吧。


理所應當我就把手上的所有任務和資料全部交接出去,想說應該就沒我的事了。


但我想到我的假還剩下一大堆,而且他通知我只做到月底。要知道那天已經是 2 月 23 號中午了,這個月只剩下四個上班日,等於我只剩下三天半的時間可以找工作,不然就會進入待業狀態。


我覺得這件事真的很扯。他說老闆是為了不破壞我過年的心情所以才現在講,但當年那個台灣主管被 fire 時,至少還給了一個月的時間交接並讓他找工作。現在他媽的只給我三天半,我真的不知道能幹嘛。我想說好聚好散,接下來三天也不打算跟公司請好請滿,就意思意思請了一天假。趁著這一天,我跟老婆約好去看電影。順帶額外推薦一下,《陽光女子合唱團》真的大推,絕對能治好乾眼症。


回到主線,在我去看電影的時候,手機訊息就開始狂跳。在關靜音之前,我看到他們決定在這個時間點讓專案上線。


.....本來想說點什麼,但算了,反正他們也沒打算尊重我。我身為這個專案最初的創作者與主管,負責整個前端維運的部分,他們卻完全不不在意專案的穩定度了。既然如此,我也沒什麼好說的,就讓他們自己去處理。


我把通訊軟體全部設為靜音,過了一整天舒爽的日子。直到 2 月 25 號,也就是今天,通訊軟體裡的訊息依樣狂跳。我仔細看了一下,原來不知道是怎麼上版,舊專案與新專案竟然同時並存。老闆一開始開到舊專案,還在群組裡罵人,質疑為什麼跟測試機不一樣,開始標罵。


這是維運的部分,前端工程師的他們都沒處裡過。我想說畢竟還是多年戰友,就小小幫了他們一下。接著更讓我傻眼的是,線上被開了多達 18 個 bug。我想說這件事情你們測試測了這麼多天,營運也測了這麼多天都測不出來,一定要等到上線之後才噴出 18 個 bug?我覺得這件事真的他媽的有夠掉詭。


然後那位不想接主管的新主管呢?繼續神隱,沒有大咖CUE他就不干他的事~


我前一天晚上還跟同樣被 fire 的一個 PM 聊天,他就說在這間公司發生這種事一點都不意外。


在這樣一間營運至上的公司裡面,他們根本不了解技術的價值,甚至連維持專案穩定度都不在乎。如果覺得薪水發太多,你 fire 新人也就算了,居然連主管都 fire。只能說這一切的發生都不意外,也確實反映了這間公司那些大佬的主觀感受,反正在這間公司:


1. 軟體操作問題:

   當他們正在操作某個軟體,只要出問題了,肯定都是系統創作者,也就是前端要負責。

   (a) 網站突然掛了,是前端要負責

   (b) 資料跑不出來,是前端要負責

   (c) 跟UI團隊溝通不良或溝通激烈或照著做出事,只要有前端在,就是前端的錯


2. 測試與品質問題:

   如果測試人員自己測太慢、測不完,也會說是前端丟出來的程式碼品質不好、沒有經過自測,最後還是前端的錯。

3. 開發速度與討論參與:

   (a) 如果程式寫太快,肯定是因為功能給得太簡單,這被認為是應該且正常的。

   (b) 參與討論時,如果你給太多建議,就你意見最多、都是你的毛;但如果你不講話全盤皆收,又會覺得你絕對都沒在做事情。


總而言之,千錯萬錯都是前端的錯。我覺得也差不多夠了。

這些事情之後都與我無關了。「HD 鬼故事」也不會再連載,因為我也畢業了。感謝大家長時間以來的支持,HD 的故事就在此落幕,謝謝大家。


補充一件事,我原本以為他們會忘記我的電腦是公司提供的,我就可以繼續爽用。想當初在要電腦時我還跟PM開玩笑說可不可以兩年後電腦送我,他也回答應該可以吧,殊不知.....


在今天下午財務理所當然他詢問了我要收回電腦的事情,問說什麼時候可以來跟我拿電腦。因為這台電腦平時也是我的日常機在使用,我就想問她說能不能再緩個幾天讓我備份一下資料。結果他就開始訓話模式說:「為什麼公司的電腦裡會有你的資料?你知不知道公司電腦是不能私用的......」;啊是說後來CEO也沒啥意見,他就沒說什麼了


關於這位財務,記然講到這份上了,就補充完整吧

就在去年員工旅遊時,我突然覺得他對我特別冷淡。原本還想說是不是我的錯覺,但我發現公司所有帶小孩的家庭中,他是會去主動關心小孩,跟小孩玩的,但唯獨不會跟我們家小孩主動互動,通常都是不小心同桌或對到的時候,才會有那麼一點點必要性互動。

而且在平常與公司的相處中,只要我們受到公司好處,例如聚餐,發薪水,他就會私底下提醒我們要謝恩、要感謝公司。這導致我們公司產生了一個很獨特的文化:收到薪水後,要在群組裡面發「感謝公司」文。

他真的是一個非常會做人的人。可能這就是他得寵,人緣好到成為情報集中站,且跟大老走這麼進還可以活到現在的原因吧。或許去年員工旅遊時,他早就接到不少關於我的消息,知道我已經被公司討厭了,所以對我的態度才變得冷淡吧。

當然這純屬我的個人猜測。我自認應該沒有得罪他什麼,剛進公司時,常跟他抱怨,但也不是真的有意要吐槽他,我吐槽的是公司制度。可能是我第一次員工旅遊時,因為他沒搭上飛機,最後請他幫忙改機票、改行程,然後他幫我買伴手禮(お土産),主動補貼我費用,我竟然都真的接受了,且沒有任何表示,因此得罪他吧?不知道。

先說我沒有不喜歡他,反倒很感謝他的照顧,不管是出於義務或是自願,我都領受他不少照顧。

.......總之就是⋯⋯

讀者以後找工作如果公司讀音是HD開頭的,特別你應徵的是前端,請三思~


DLC

就在我歸還 Mac 的時候,詢問了一下我原本的組員,問他有沒有在公司聽到什麼傳聞。他說沒有,只是暫時沒有前端主管。


我接著問說,那原本要取代我的那位前端主管呢?他說對方似乎前一陣子被投訴了,公司應該也不會再讓他擔任主管。

恩.....果然是這樣,就算沒有要升他,但也絕對沒打算要留我。


我之前有去聯繫一位剛過試用期就被資遣的前員工,詢問他有沒有領到資遣費。他說有,領了兩天。

今天我也領到薪水了,然後確實比平常多出一些金額,算了一下,也大概是我兩天左右的薪水。

重點是:
1. 他才剛過試用期,就能領到這個數字。
2. 我在這邊可是做了快三年啊。

暨給三天半找工作期限後,這個資遣費真的是廢到讓我笑出來。

話說這兩天我開前公司專案看到新的 commit,發現前同事還在三更半夜上 code。

我突然想到,當初在我接主管的時候,有一個口頭條款:我跟我的組員不進行非必要的加班。

也確實這段時間加班情況銳減,也或許就是因為這一條,讓他們最終決定要把我砍掉。

恩,沒啥幫助,就是想講講~

DLC2-遣散費的真相

一週之後,前同事來關心我找工作狀況,我順便吃了點瓜,接著講到遣散費,然後他給了我一個驚人的事實

所以多出來的280U,是發薪當天匯率剛好突破0.145,所以平常薪水乘以0.14,這個月變成乘以0.15.....就這樣,所以根本不是遣散,是人人都有的匯率紅利啊......好吧,所以要砍我的原因又多了一個,成本上升了!!

後來想想,大家平常對這間公司幹得要死,但偏偏流動率又超低,這算不算是一種有毒的關係呢....

2026年2月5日 星期四

動態單位dvh, svh, lvh

我知道我孤陋寡聞,這東西都出來多久了,現在才在追,沒錯,要不是瀏覽器遇到奇怪的問題,我大概一被子也不會碰到這東西,原本以為傳統vh, %已經夠用了,沒想到有推出這麼好用的東西,廢話不多說,直接開始

1. svh (Small Viewport Height)

  • 含義小視窗高度

  • 定義:當瀏覽器的 工具列完全顯示(展開狀態)時,所剩餘的可視區域高度。

  • 特性:它是三者中最小的高度。

  • 場景:當你希望元件在任何情況下都不會被工具列遮擋時使用。

2. lvh (Large Viewport Height)

  • 含義大視窗高度

  • 定義:當瀏覽器的 工具列完全隱藏(收起狀態)時,所能達到的最大可視區域高度。

  • 特性:它的數值與傳統的 100vh 基本一致,但在某些裝置上更穩定。

  • 場景:當你追求極致的全螢幕視覺效果,且不介意底部內容暫時被工具列覆蓋時使用。

3. dvh (Dynamic Viewport Height)

  • 含義動態視窗高度

  • 定義:根據 工具列目前的顯示狀態自動調整 的高度。

  • 特性:當工具列縮小時,100dvh 會變大;當工具列展開時,它會縮小。

  • 場景:這是目前最推薦的單位,適合用來製作「真正的全螢幕」滿版背景或中間定位的彈窗(Modal),它能確保元件始終在當前的畫面中央。


快速對比表

單位全寫工具列狀態高度大小適用情況
svhSmall Viewport Height展開 (Max UI)最小確保內容不被遮擋
lvhLarge Viewport Height隱藏 (Min UI)最大追求最大視覺空間
dvhDynamic Viewport Height自動變動隨動最推薦,自動適應當前畫面
那100dvh跟100%差別在哪呢,主要就是不會受到父元素影響吧,上圖表
特性height: 100%height: 100dvh
參照對象父元素 的高度。視窗 (Viewport) 的動態高度。
依賴性必須「層層向上」定義高度。獨立存在,不論父元素高度為何。
動態縮放隨父元素變動。隨瀏覽器工具列(網址列)縮放變動。
預設行為若父元素沒設高度,此設定無效。即使放在深層結構,也能直接抓到螢幕高度。
所以傳統做法需要先定意html, body的高度,讓container可以做到高度100%
html, body, .container {
    height: 100%;
}
之後就不用這麼麻煩了,直接
.container {
    height: 100dvh;
}
搞定收工,歡呼聲!

2025年12月8日 星期一

下載txt小說在mac卻不能看,試試看iconv

 其實說穿就是一行指令,請參考

iconv -c -f GB2312 -t UTF-8 [小說檔名含副檔名] >> [轉存後的檔名]