前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >【Go】四舍五入在go语言中为何如此困难

【Go】四舍五入在go语言中为何如此困难

作者头像
thinkeridea
发布于 2021-01-04 01:43:25
发布于 2021-01-04 01:43:25
1.4K00
代码可运行
举报
运行总次数:0
代码可运行

四舍五入是一个非常常见的功能,在流行语言标准库中往往存在 Round 的功能,它最少支持常用的 Round half up 算法。

而在 Go 语言中这似乎成为了难题,在 stackoverflow 上搜索 [go] Round 会存在大量相关提问,Go 1.10 开始才出现 math.Round 的身影,本以为 Round 的疑问就此结束,但是一看函数注释 Round returns the nearest integer, rounding half away from zero ,这是并不常用的 Round half away from zero 实现呀,说白了就是我们理解的 Round 阉割版,精度为 0 的 Round half up 实现,Round half away from zero 的存在是为了提供一种高效的通过二进制方法得结果,可以作为 Round 精度为 0 时的高效实现分支。

带着对 Round 的‘敬畏’,我在 stackoverflow 翻阅大量关于 Round 问题,开启寻求最佳的答案,本文整理我认为有用的实现,简单分析它们的优缺点,对于不想逐步了解,想直接看结果的小伙伴,可以直接看文末的最佳实现,或者跳转 exmath.Round 直接看源码和使用吧!

Round 第一弹

stackoverflow 问题中的最佳答案首先获得我的关注,它在 mathx.Round 被开源,以下是代码实现:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
//source: https://github.com/icza/gox/blob/master/mathx/mathx.go
package mathx

import "math"

// Round returns x rounded to the given unit.
// Tip: x is "arbitrary", maybe greater than 1.
// For example:
//     Round(0.363636, 0.001) // 0.364
//     Round(0.363636, 0.01)  // 0.36
//     Round(0.363636, 0.1)   // 0.4
//     Round(0.363636, 0.05)  // 0.35
//     Round(3.2, 1)          // 3
//     Round(32, 5)           // 30
//     Round(33, 5)           // 35
//     Round(32, 10)          // 30
//
// For details, see https://stackoverflow.com/a/39544897/1705598
func Round(x, unit float64) float64 {
	return math.Round(x/unit) * unit
}

这个实现非常的简洁,借用了 math.Round,由此看来 math.Round 还是很有价值的,大致测试了它的性能一次运算大概 0.4ns,这非常的快。

但是我也很快发现了它的问题,就是精度问题,这个是问题中一个回答的解释让我有了警觉,并开始了实验。他认为使用浮点数确定精度(mathx.Round的第二个参数)是不恰当的,因为浮点数本身并不精确,例如 0.05 在64位IEEE浮点数中,可能会将其存储为0.05000000000000000277555756156289135105907917022705078125

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
//source: https://play.golang.org/p/0uN1kEG30kI
package main

import (
	"fmt"
	"math"
)

func main() {
	f := 12.15807659924030304
	fmt.Println(Round(f, 0.0001)) // 12.158100000000001

	f = 0.15807659924030304
	fmt.Println(Round(f, 0.0001)) // 0.15810000000000002
}

func Round(x, unit float64) float64 {
	return math.Round(x/unit) * unit
}

以上代码可以在 Go Playground 上运行,得到结果并非如期望那般,这个问题主要出现在 math.Round(x/unit)unit 运算时,math.Round 运算后一定会是一个精确的整数,但是 0.0001 的精度存在误差,所以导致最终得到的结果精度出现了偏差。

格式化与反解析

在这个问题中也有人提出了先用 fmt.Sprintf 对结果进行格式化,然后再采用 strconv.ParseFloat 反向解析,Go Playground 代码在这个里。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
source: https://play.golang.org/p/jxILFBYBEF
package main

import (
	"fmt"
	"strconv"
)

func main() {
	fmt.Println(Round(0.363636, 0.05)) // 0.35
	fmt.Println(Round(3.232, 0.05))    // 3.25
	fmt.Println(Round(0.4888, 0.05))   // 0.5
}

func Round(x, unit float64) float64 {
	var rounded float64
	if x > 0 {
		rounded = float64(int64(x/unit+0.5)) * unit
	} else {
		rounded = float64(int64(x/unit-0.5)) * unit
	}
	formatted, err := strconv.ParseFloat(fmt.Sprintf("%.2f", rounded), 64)
	if err != nil {
		return rounded
	}
	return formatted
}

这段代码中有点问题,第一是结果不对,和我们理解的存在差异,后来一看第二个参数传错了,应该是 0.01,我想试着调整调整精度吧,我改成了 0.0001 之后发现一直都是保持小数点后两位,我细细研究了下这段代码的逻辑,发现 fmt.Sprintf("%.2f", rounded) 中写死了保留的位数,所以它并不通用,我尝试如下简单调整一下使其生效。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
package main

import (
	"fmt"
	"strconv"
)

func main() {
	f := 12.15807659924030304
	fmt.Println(Round(f, 0.0001)) // 12.1581

	f = 0.15807659924030304
	fmt.Println(Round(f, 0.0001)) // 0.1581

	fmt.Println(Round(0.363636, 0.0001)) // 0.3636
	fmt.Println(Round(3.232, 0.0001))    // 3.232
	fmt.Println(Round(0.4888, 0.0001))   // 0.4888
}

func Round(x, unit float64) float64 {
	var rounded float64
	if x > 0 {
		rounded = float64(int64(x/unit+0.5)) * unit
	} else {
		rounded = float64(int64(x/unit-0.5)) * unit
	}

	var precision int
	for unit < 1 {
		precision++
		unit *= 10
	}

	formatted, err := strconv.ParseFloat(fmt.Sprintf("%."+strconv.Itoa(precision)+"f", rounded), 64)
	if err != nil {
		return rounded
	}
	return formatted
}

确实获得了满意的精准度,但是其性能也非常客观,达到了 215ns/op,暂时看来如果追求精度,这个算法目前是比较完美的。

大道至简

很快我发现了另一个极简的算法,它的精度和速度都非常的高,实现还特别精简:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
package main

import (
	"fmt"

	"github.com/thinkeridea/go-extend/exmath"
)

func main() {
    f := 0.15807659924030304
    fmt.Println(float64(int64(f*10000+0.5)) / 10000) // 0.1581
}

这并不通用,除非像以下这么包装:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
func Round(x, unit float64) float64 {
	return float64(int64(x*unit+0.5)) / unit
}

unit 参数和之前的概念不同了,保留一位小数 uint =10,只是整数 uint=1, 想对整数部分进行精度控制 uint=0.01 例如: Round(1555.15807659924030304, 0.01) = 1600Round(1555.15807659924030304, 1) = 1555Round(1555.15807659924030304, 10000) = 1555.1581

这似乎就是终极答案了吧,等等……

终极方案

上面的方法够简单,也够高效,但是 api 不太友好,第二个参数不够直观,带了一定的心智负担,其它语言都是传递保留多少位小数,例如 Round(1555.15807659924030304, 0) = 1555Round(1555.15807659924030304, 2) = 1555.16Round(1555.15807659924030304, -2) = 1600,这样的交互才符合人性啊。

别急我在 go-extend 开源了 exmath.Round,其算法符合通用语言 Round 实现,且遵循 Round half up 算法要求,其性能方面在 3.50ns/op, 具体可以参看调优exmath.Round算法, 具体代码如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
//source: https://github.com/thinkeridea/go-extend/blob/main/exmath/round.go

package exmath

import (
	"math"
)

// Round 四舍五入,ROUND_HALF_UP 模式实现
// 返回将 val 根据指定精度 precision(十进制小数点后数字的数目)进行四舍五入的结果。precision 也可以是负数或零。
func Round(val float64, precision int) float64 {
	p := math.Pow10(precision)
	return math.Floor(val*p+0.5) / p
}

总结

Round 功能虽简单,但是受到 float 精度影响,仍然有很多人在四处寻找稳定高效的算法,参阅了大多数资料后精简出 exmath.Round 方法,期望对其他开发者有所帮助,至于其精度使用了大量的测试用例,没有超过 float 精度范围时并没有出现精度问题,未知问题等待社区检验,具体测试用例参见 round_test

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2021-01-02,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
Go 语言字符串使用方式与技巧
关于 Go 语言字符串的使用,我们需要了解标准库 strconv 和标准库 strings 的使用方式,它们分别用于字符串类型转换和字符串操作。
frank.
2023/12/14
2460
Go 语言字符串使用方式与技巧
Go语言学习系列——基础语法——【坚果派-红目香薰】
红目香薰
2025/03/06
1010
Go语言学习系列——基础语法——【坚果派-红目香薰】
一篇文章带你了解Go语言基础之数据类型
上篇文章中我们学习了Go语言基础中的变量,一篇文章带你了解Go语言基础之变量,这篇文章我们继续介绍Go语言基础知识,今天跟大家分享的是基础数据类型,一起来学习下吧~
Go进阶者
2021/01/22
2810
[日常] Go语言圣经--浮点数习题
练习 3.1: 如果f函数返回的是无限制的float64值,那么SVG文件可能输出无效的多边形元素(虽然许多SVG渲染器会妥善处理这类问题)。修改程序跳过无效的多边形。
唯一Chat
2019/09/10
9710
[日常] Go语言圣经--浮点数习题
go语言学习-类型转换
1.字符串到整形(string to int):ParseInt 返回的是 int64
solate
2019/07/19
1K0
Google资深工程师深度讲解Go语言-基础语法(二)「建议收藏」
发布者:全栈程序员栈长,转载请注明出处:https://javaforall.cn/111735.html原文链接:https://javaforall.cn
全栈程序员站长
2022/07/12
3340
Google资深工程师深度讲解Go语言-基础语法(二)「建议收藏」
第七章 字符串
字符串可以使用 双引号(" ")或者 反引号(` `)来创建。双引号用来创建可解析的字符串,但不能用来引用多行,这也是大多数字符串的定义方式。
宇宙之一粟
2020/10/26
2810
第七章 字符串
Go语言中常用的基本数据类型
Go语言中以bool类型进行声明布尔型数据,布尔型数据只有true(真)和false(假)两个值。
极客运维圈
2020/03/23
1.3K0
go语言中的math库
Go语言的 math 包提供了许多数学函数和常量,涵盖了各种数学运算。以下是一些常用函数的介绍:
GeekLiHua
2025/01/21
1160
go语言中的math库
三分钟学 Go 语言——函数深度解析(下) 可变参数
小熊这两天因为个人种种令人难受的原因,没有能更新,也没有提前请假,给大家道歉歉了。
机智的程序员小熊
2020/05/12
6900
[日常] Go语言圣经-WEB服务与习题
2.main函数将所有发送到/路径下的请求和handler函数关联起来,/开头的请求其实就是所有发送到当前站点上的请求,服务监听8000端口
唯一Chat
2019/09/10
7220
[日常] Go语言圣经-WEB服务与习题
GoLang中基本数据类型之间的转换
转换的时候建议从低位转换成高位,高位转换成低位的时候如果转换不成功就会溢出,和我们想的结果不一样。
BUG弄潮儿
2024/01/24
3200
GoLang中基本数据类型之间的转换
[go 标准库] strconv
go strconv 包提供了基本数据类型与 string 类型相互转换常用的处理函数。提供了如下操作接口:
柳公子
2021/05/06
9520
Go语言知识查漏补缺|基本数据类型
学习Go半年之后,我决定重新开始阅读《The Go Programing Language》,对书中涉及重点进行全面讲解,这是Go语言知识查漏补缺系列的文章第二篇,前一篇文章则对应书中一二两章。
白泽z
2022/12/20
5280
Go语言知识查漏补缺|基本数据类型
Golang不同类型比较
在日常开发过程中难免会遇到各个类型的变量的比较以及运算操作,这里我们做了一些简单的汇总,希望能给各位同学在开发中带来帮助。
孤烟
2020/09/27
1.2K0
Go语言的浮点型比较大小、与字符串互转、精准舍入
我们在编程中经常需要对两个浮点型比较大小,下面我就来分享一段这样的代码,同时也展示了Go语言函数式编程的独特魅力:
henrylee2cn
2019/04/04
5.3K0
go语言中的数据类型
注意,在 Go 语言中有些类型是预定义的别名类型,如 byte 和 rune,它们 实际上是 uint8 和 int32 的别名。还有一种特殊的类型 nil 表示空值,可以表示指针、切片、映射、通道、函数和接口类型的零值。
GeekLiHua
2025/01/21
970
Golang之旅27-Golang知识点总结1
字符串是由一连串的字符连接起来的字符序列,使用的utf-8编码标识的unicode文本。
皮大大
2021/03/02
3960
Go | 基本数据类型的相互转换
n1为int32, n2为int16, 所以先把加数n1转换成int16, 再做加法
甜点cc
2022/11/02
3210
Go | 基本数据类型的相互转换
(七)golang--变量之基本数据类型(看这篇就够了)
(1)golang整数类型分为:有符号和无符号,int和uint的大小和系统有关;
西西嘛呦
2020/08/26
6450
相关推荐
Go 语言字符串使用方式与技巧
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验