登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  Golang >  Go问答

Go math/bits.Mul64 返回的 hi 和 lo 怎么还原完整乘积

来源:17golang原创

时间:2026-11-05 09:30:12 372浏览 收藏

bits.Mul64(x, y) 返回完整 128 位无符号乘积的两个部分:hi 是高 64 位,lo 是低 64 位。数学上完整乘积等于 hi × 2^64 + lo,但 Go 的 uint64 只能容纳其中一个字,不能在同一个 uint64 中用左移把两段重新变成 128 位整数。

需要完整保存时,保留 {Hi, Lo} 两个字段,或编码成固定16字节;只需要检查结果是否能装进 uint64,则看 hi != 0。下面用一次乘法记录的计算、保存和恢复说明这两种需求。

官方地址:https://pkg.go.dev/math/bits#Mul64

乘法结果看似变小,先确认结果宽度

设想某个计数工具用两个 uint64 相乘,输入看起来都合法,结果却比预期小。首先要区分“输入能装下”和“乘积能装下”。两个64位无符号输入的完整乘积最多需要128位,结果变量仍然是 uint64 时,普通乘法只保留低64位。

这不意味着运行时会自动报溢出错误。无符号整数运算有固定宽度规则,超出部分不会凭空存到别处。若业务必须知道是否截断,就要使用能够提供高字的信息,而不是等乘法之后再把结果转换成其他表示。

Mul64 直接给出 hi 和 lo,避免在普通乘法已经丢失高位后再猜测。本文只处理无符号高低字的表示与溢出判断,不扩展到其他大整数方案。

用两个 uint64 表示完整的 128 位无符号数

解释乘法接口与两个返回字
原创静态结构图:Mul64 接收两个 uint64,返回 Hi 与 Lo;两者共同组成完整乘积,不能再塞进单个 uint64。连线表示接口组成,不是运行步骤。

可以把两段理解为以 2^64 为基数的两个数位。hi 是高位数位,lo 是低位数位。它们共同表示一个数,而不是两个互不相关的结果;同时 hi 也不是只有0或1的进位标志,它本身可以占满64位。

常见错误是写 combined := hi。hi 的类型还是 uint64,左移不会把变量类型自动拓宽,结果仍然没有空间容纳原来的高位。这条表达式不能实现数学公式中的128位拼接。

另一个错误是把 hi 与 lo 的十进制字符串直接连接。基数是2的64次方,不是十进制文本长度;两段十进制字符串连起来,通常与完整乘积没有对应的数值关系。十六进制可以按固定宽度展示,字节也可以按固定布局保存,但都需要明确约定。

把高低字编码为固定 16 字节

解释大端16字节的字段布局
原创静态结构图:16字节大端记录分为高字区域和低字区域,每区八字节;恢复时分别读取为 Hi、Lo。图是字段布局说明,不是执行结果。

以下完整原创程序使用 product128 表示两个字,保存为大端16字节。前八字节存 Hi,后八字节存 Lo。这个布局是本文定义的应用格式,与本机内存的结构体布局无关。

package main

import (
    "encoding/binary"
    "fmt"
    "math/bits"
)

type product128 struct {
    Hi uint64
    Lo uint64
}

func multiply(x, y uint64) product128 {
    hi, lo := bits.Mul64(x, y)
    return product128{Hi: hi, Lo: lo}
}

func (p product128) bytesBE() [16]byte {
    var out [16]byte
    // 应用格式:前八字节是高字,后八字节是低字,段内均为大端。
    binary.BigEndian.PutUint64(out[:8], p.Hi)
    binary.BigEndian.PutUint64(out[8:], p.Lo)
    return out
}

func decodeBE(data []byte) (product128, error) {
    if len(data) != 16 {
        return product128{}, fmt.Errorf("乘积记录应为16字节,实际为%d", len(data))
    }
    return product128{
        Hi: binary.BigEndian.Uint64(data[:8]),
        Lo: binary.BigEndian.Uint64(data[8:]),
    }, nil
}

func main() {
    // 两个输入都能装入 uint64,完整乘积需要高字参与表示。
    x := uint64(1

输入选为 (2^40+3) 与 (2^30+5)。展开后为 2^70 + 5×2^40 + 3×2^30 + 15:高字应为64,低字应为后面三项的和。这个推导说明高字确实参与结果,不是依靠一个全为低位的小数样本来展示保存方法。

代码给出的是语义预期,没有在本地执行或虚构打印结果。实际接入时,可以用自己可独立计算的边界样本对照调用方逻辑,但保存格式本身不需要借助运行截图解释。

读取时按相同字节序恢复两段

decodeBE 先要求长度恰好为16,再分别读取两个八字节区域。先判断长度,是为了避免把截断数据直接交给 Uint64;拒绝多余字节,则是为了让“一个记录”拥有清楚边界,避免悄悄吞掉后续内容。

恢复后的 Hi 和 Lo 分别与原字段一致,才说明编码往返没有交换两个字或改变段内字节序。仅比较 Lo 不足以证明完整恢复;Hi 恰好为零时,也容易掩盖高低顺序写反的问题。

字节序包含两个需要约定的层次:整个16字节中哪个字在前,以及每个64位字内部的字节排列。本文采用完整大端表示,因此高字在前、低字在后,且每个字内部也是大端。不能只把两个 PutUint64 改成 LittleEndian,却仍把布局称作同一种完整数值编码。

日志展示使用 %016x%016x,每个字各占16个十六进制数字,总计32个。低字必须补足宽度,否则中间省略的零会改变高字与低字的分界。文本展示与字节保存是同一个值的不同表示,不会自动赋予 Go 一个内置 uint128 算术类型。

只接受 uint64 结果时检查 hi

如果调用方只接受 uint64,不必先编码16字节再检查。直接判断 hi 是否为零即可:高字非零,完整结果就超出64位;高字为零,lo 就是完整结果。

// 仅当完整乘积能装进 uint64 时返回结果;溢出不返回截断值。
func checkedMul64(x, y uint64) (uint64, error) {
    hi, lo := bits.Mul64(x, y)
    if hi != 0 {
        return 0, fmt.Errorf("乘积超出uint64范围")
    }
    return lo, nil
}

这个局部函数可放入前面同一个文件,使用已有导入。它在溢出时返回错误而不返回截断值,调用方就不会误把 Lo 当成成功结果。若项目采用布尔标志,务必清楚约定标志表示“成功”还是“溢出”,避免相反理解。

不能只看 Lo 是否变小、是否等于零来判断。Lo 为零既可能是零乘积,也可能是高字非零而低64位恰好全零;Lo 看起来很大,也不能证明高字不存在。溢出信息来自 Hi。

上述结论针对 uint64 的范围。把结果再转换成 int64 或 int 时,目标类型还有自己的边界;Hi 为零只证明能装进 uint64,不证明一定能装进任何其他整数类型。本文不把无符号检查冒充带符号乘法规则。

保存与调用边界的速查清单

目标保存或判断方式注意事项
保留完整乘积Hi 与 Lo 两字段不得遗漏任一字
写入固定长度记录16字节明确字节序恢复时检查长度与字段位置
十六进制展示每字固定16位,高字在前低字必须补零
只返回uint64Hi 为零时返回 LoHi 非零时按业务报告溢出

这份记录是普通值数据,不持有文件、连接或其他需要 Close 的资源。接入保存层时只需另外处理保存失败和记录边界,不必为两个整数设计额外的清理动作。更关键的是在调用接口、存储字段和展示格式中始终保持同一套高低字约定。

hi 非零意味着 Mul64 本身溢出了吗?

不是。Mul64 正在返回两个64位输入的完整128位乘积。hi 非零只说明结果不能缩减为单个 uint64。

为什么先算 x*y,再转成两字段不行?

如果 x、y 都是 uint64,普通乘法的高位可能已经丢失。后续转换不能恢复未被保存的信息,应直接使用 Mul64 获得两段。

两个字段能直接作为可比较的值使用吗?

本文的 product128 只有两个 uint64,因此可以做相等比较。这只判断两个表示是否相同,不会自动提供128位加减乘除或十进制格式化能力。

参考:math/bits.Mul64 官方接口、encoding/binary 字节序接口、Go 规范:整数溢出。示例与记录格式为本文原创。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>