我們對帳對了一整天,就為了三分錢——金額為什麼絕不能用浮點數
Backend Engineering·2026年4月28日·5 分鐘閱讀

我們對帳對了一整天,就為了三分錢——金額為什麼絕不能用浮點數

L
Leo Wu

後端工程師,做過交易所與金流系統,曾經為了三分錢的帳差查了一整個下午。

那天帳差了三分錢

我們對帳對了一整天,就為了三分錢——金額為什麼絕不能用浮點數:本文架構

那是月結對帳,系統跑完自動比對,跳出一行紅字:平台入帳總額,跟金流商回報的總額,差了新台幣 0.03 元。

三分錢。你可能會想,三分錢是能有多嚴重?但做金流的都知道,帳只要不平,不管差多少,你都不能簽名放行。差三分錢跟差三百萬,在稽核眼裡是同一件事:你的帳是錯的,而且你不知道錯在哪。這代表可能有一整類的計算邏輯出了問題,這次剛好抵銷到只剩三分,下次可能就是三十萬。

我跟同事對著兩邊的明細,一筆一筆比,比到下午才找到兇手。不是誰貪汙,也不是金流商算錯。是我們某段拆帳的程式碼,把手續費用浮點數(float)在那邊加加減減,累積了微小的誤差,幾十萬筆訂單累加下來,就浮出了那三分錢。

浮點誤差累積曲線:單筆誤差極小,但隨訂單量單調累積,幾十萬筆後浮出約 0.03 元
浮點誤差累積曲線:單筆誤差極小,但隨訂單量單調累積,幾十萬筆後浮出約 0.03 元

問題的根源:二進位存不下 0.1

這件事的底層原因,其實是電腦科學的老問題:浮點數是二進位的,而很多十進位小數,二進位根本存不下來

最經典的例子,你打開任何一個語言的直譯器,算 0.1 加 0.2,得到的不是 0.3,是 0.30000000000000004。因為 0.1 在二進位裡是一個無限循環小數,電腦只能存一個很接近、但不精確的近似值。單獨看不痛不癢,可是金額計算是會累加的——你把幾十萬個「接近但不精確」加在一起,誤差就攢出來了。

平常寫寫一般業務,這點誤差你可能一輩子遇不到。但金額不行。金額的每一分錢都要對得起使用者的錢包,也對得起稽核。只要牽涉到錢,float 就是一個定時炸彈。

解法一:用整數存最小單位

我後來把所有金額相關的計算,全部改掉。第一種、也是我最推薦的做法:用整數存最小貨幣單位

台幣就存「分」,一塊錢存成 100。日圓沒有小數,直接存。加密貨幣更要小心,很多幣種最小單位到小數點後八位甚至十八位,那就存那個最小單位的整數(像比特幣的「聰」)。整數的加減乘除是精確的,不會有任何浮點誤差,這是它最大的好處。

顯示的時候再除回來、補上小數點就好。存的是整數,算的是整數,只有「給人看」的那一刻才轉成有小數的字串。

解法二:用 Decimal 型別

第二種做法,是用語言或資料庫提供的 Decimal(定點數)型別。它內部就是為了精確表示十進位小數而設計的,不會有二進位近似的問題。

資料庫這邊,金額欄位就該用 DECIMAL/NUMERIC,明確指定精度跟小數位數,絕對不要用 FLOAT 或 DOUBLE。這點我後來當成鐵律,code review 看到有人金額欄位開 float,一律打回。

Decimal 的代價是運算比整數慢一點、用起來稍微囉唆,但在金額場景,正確性遠比那一點效能重要。我通常的原則是:內部核心計算用整數分,跟資料庫、跟外部系統的邊界用 Decimal,就是不碰 float。

真正難防的:邊界上的精度洩漏

改完內部計算,我以為就沒事了。結果又踩到第二個坑,而且這個更陰險——精度是在「邊界」上漏掉的

什麼意思?你內部算得再精確,資料一旦要跨出去,就有風險:

  • 序列化成 JSON:很多語言把數字轉成 JSON 時,會用浮點數表示。你一個精確的 Decimal,序列化成 JSON 再被對方解析,就可能變回帶誤差的浮點數。我後來的做法是金額在 JSON 裡一律用字串傳,例如 "100.00",兩邊都約定好用字串解析,不讓它經過浮點數這一關。
  • 第三方 API:對接金流商、銀行的介面,一定要看清楚他們金額欄位的型別跟精度,不要自己算完直接塞。
  • 前端顯示:前端的數字型別同樣是浮點,金額到了前端也要小心處理。

那次三分錢的事故,最後補的其實不只是把 float 換掉,還包括把所有對外邊界的金額格式全部盤過一遍。

小結

「金額不要用浮點數」這句話你一定聽過,網路上三秒就查得到。但我想讓你記得的,是那個對帳對到下午、對著兩份明細找三分錢的下午。教條是別人幫你把痛濃縮成一句話,代價是你很難真的怕它。等你自己被三分錢搞到查一整天,你就會跟我一樣,看到金額欄位是 float 就渾身不對勁。內部用整數分、邊界用字串或 Decimal、資料庫用 NUMERIC——這三條,是我用一個下午換來的。

#Go#金額#Decimal#精度#金流

留言討論

有想法、有不同經驗、或想糾正我?歡迎在下面留言,免註冊,填個暱稱就能留。

相關文章