关于golang:疑惑-Go-const-会导致程序结果错乱

61次阅读

共计 4028 个字符,预计需要花费 11 分钟才能阅读完成。

const 是 Go 外面咱们常常应用的关键字, 基本上很难玩出花来. 不过某些非凡状况下 const 会呈现你意想不到的后果

场景模仿

某公司某次营销流动中, 会依据用户 VIP 级别送用户一些优惠券, 最大面值 520. 某用户发现自己购买的 500 元钱的商品, 应用 520 的优惠券来领取, 实践上能 0 元购买的商品, 最初却须要领取一个天文数字.

这个场景是我本人轻易想的, 如果过于夸大, 请原谅我. ^^ 上面咱们用代码大略模仿下这个场景:

func main() {
    var totalPrice uint32 = 500
    const couponPrice = 550

    fmt.Println("用户须要领取金额:", totalPrice-couponPrice)
}

先别运行程序, 你感觉应该返回的后果是多少?

A. 程序无奈编译
B. -50
C. 50
D. 4294967246

后果是 D, 你会不会感觉很意外?

一些疑难:

  1. 500 – 550 的后果为什么不是 -50 ?
  2. 你是否留神过 const 的类型 ?
  3. 如果你留神过 const 类型, 为什么程序能失常编译 ?

500 – 550 的后果为什么不是 -50 ?

重写再写一段新的代码, 咱们把数值放大一点, 不便前面的论述

func main() {
    var totalPrice uint8 = 1
    var couponPrice uint8 = 2
    fmt.Println("用户须要领取金额:", totalPrice-couponPrice)
}

后果: 用户须要领取金额: 255

你是否据说过 原码 , 反码 , 补码 这三个概念? 如果不晓得的话, 请持续往下看马上就能揭开天文数字的假相.

原码

应用二进制如何标识 1 和 -1 呢?

 1  :  0000 0001
-1  :  1000 0001

咱们通过比照能很快发现第一位是符号位, 这其实是易于人来了解和计算的一种示意形式, 这个示意形式叫: 原码. 计算机本身就只辨认 0(低电位) 和 1(高电位), 说的再彻底点 0 和 1 其实就是 CPU 逻辑运算单元外面的二极管的导通和阻断. 这么看来, 如果让一个 1 来代表符号位, 能让计算机能辨认进去, 并且还让这一位不参加计算, 这根本是不事实的.

这怎么办呢? 有人已经说过: 计算机科学畛域的任何问题都能够通过减少一个间接的中间层来解决? 同样对于 原码 也能够转换成计算机可能辨认二进制编码.

反码

接下来咱们看另外一种示意形式, 应用 1 - 1 = 0 来解释. 如果咱们符号位也参加计算, 同时让 负数 的二进制放弃不变. 正数 的二进制符号位放弃不变, 其余位按位取反.

1 - 1 => 0000 0001 + 1111 1110 = 1111 1111  

这其实就是反码. 咱们将 1111 1111 转换成咱们能辨认的原码就是 1000 0000, 其实也就是 -0. 于是就呈现另外一个问题, 0000 0000 也代表 0. 于是就呈现了 1000 00000000 0000 的原码都代表 0. 这是不行的.

补码

终极奥义出场. 同样让符号位参加计算, 咱们让负数的二进制放弃不变, 正数的二进制的符号位放弃不变, 其余各位取反, 最初 +1, 其实正数的补码就是这个先找个数的反码, 而后反码加 1.

于是 1 – 1 就能够标识为:

1 - 1 => 0000 0001 + 1111 1110  =>  0000 0001 + 1111 1111  => 0000 0000(补码)
         ---------   ---------      ---------   ---------
            反码         反码           补码         补码

补码 0000 0000 其实也是原码, 也就是 0. 这下就没问题了.

1 – 2 的问题

咱们应用补码来解释下 1 - 2的后果:

1 - 2 => [0000 0001]反 + [1111 1101]反 => [0000 0001]补 + [1111 1110]补 = 1111 1111(补码)

将补码 1111 1111 转换成原码 1000 0001 => -1

看到这里你是不是又奇怪了? 后果明明是: 4294967295.

对于写 Java 同学来说, 他们绝大多数场景下 int 代表一个整数就足够了. 对于 Php 那就更放肆了, 不仅不关怀数字的大小, 有可能传给你一个 “2” 来当整数 2 用. 但你是否留神到 golang 外面分有符号和无符号类型的数, 如 int8 和 uint8 ?

所以, 咱们能看进去, 有符号数的减法根本在咱们认知范畴之内. 那无符号数为何就如何神奇呢?

无符号数

func main() {
    var totalPrice uint8 = 1
    var couponPrice uint8 = 2
    fmt.Println("用户须要领取金额:", totalPrice-couponPrice)
}

还是这段代码, 咱们看到 totalPrice, couponPrice 都是 uint32 类型的整数, 他们都是无符号类型的整数. 所以当看到程序用 uintx 来定义变量的, 这个变量就是无符号类型的.

为什么 Go 不像 Java 那样一个 int 类型吃遍天呢, 搞出无符号类型的目标何在?

  1. 有符号数是能够示意正数的. 如 int8 的范畴区间是[-128, 127]. 而有些场景下咱们只想要负数, 那么就能够用无符号数来示意, 同样 uint8 就能够代表 [0, 255]
  2. 其实我感觉更大的可能性是因为 Go 是那帮写 C 的人写的, 他们持续沿用了 C 外面这个传统的数值示意形式.

无符号的数的减法来说, 咱们要把 1 – 2 同样也看成 1 + (-2), 于是同样须要将 -2 转换成补码模式.

 1 - 2 => [0000 0001]反 + [1111 1101]反 => [0000 0001]补 + [1111 1110]补 = 1111 1111(补码)

因为无符号数的加减后果依然是无符号数, 那么 1111 1111 就是一个无符号的数, 所以最高位不是符号位, 负数的补码和原码一样, 于是 1 – 2 的后果就是 255.

咱们当初用的是 uint8 类型的数, 如果换成 uint16, uint32, uint64 会怎么样呢? 同样的 1-2 对于 uint64 后果就会变成 18446744073709551615. 是不是很夸大???

咱们下面都说的减法, 那加法会不会呈现这个状况呢? 这个我就不在开展了, 聪慧的你能够试试上面的代码, 而后思考下为啥?

func main() {
    var totalPrice uint8 = 255
    var couponPrice uint8 = 1
    fmt.Println("用户须要领取金额:", totalPrice+couponPrice)
}

终于说完了 Go 的无符号类型的计算过程, 咱们由此能够看出, Go 在做加减估算时要留神上面几点:

  1. 抉择不同的类型的变量时, 要特地留神该类型的取值范畴, 避免数值越界
  2. 节俭计算机资源. 申明同一个变量, 应用 int8 占一个字节, uint32 就占用 4 个字节
  3. 在应用 uint 家族的类型的变量做减法时, 肯定要判断变量的大小, 不然开篇我说的场景就是你们线上将要产生的事件.

你是否留神过 const 的类型 ?

func main() {
    var totalPrice uint8 = 1
    var couponPrice int8 = 2
    fmt.Println("用户须要领取金额:", totalPrice+couponPrice)
}

这段代码你感觉后果是什么?

A. 程序无奈编译
B. -1
C. 255
D. 4294967246

后果是 A, 你是不是又很意外. 其实你应该不意外才对, Go 不反对隐式类型转换, 不同种类型的变量之间不能间接做互相转换, 必须做类型的强转. 下面的代码是编译不过的.

其实类型间接做强转有的时候也是会有问题的. 我这里就不具体开展了, 不然就越扯越多了. 请自行执行上面这段代码的后果, 而后剖析一下吧, 起因还是还是下面说的, 肯定要留神类型的取值范畴.

func main() {
    var a int64 = 922372037
    fmt.Println("a:", a)
    
    var b int8 = int8(a)
    fmt.Println("b:", b)
}

后果:

a: 922372037
b: -59

持续回到 const 的类型下面, 我猜大部分人其实没关注过 const 的类型吧

func main() {
    const a = 10
    fmt.Println("type:", reflect.TypeOf(a))
}

后果: type: int

再回到开篇的那段代码, 为啥就能够失常编译过而且能输入一个不正确的后果呢?

func main() {
    var totalPrice uint32 = 500
    const couponPrice = 550

    fmt.Println("用户须要领取金额:", totalPrice-couponPrice)
}

const 依然和一般变量的默认类型保持一致. 整数常量的默认类型是 int, 浮点数常量的默认类型是 float64, 字符串常量的默认类型是 string

这里就要到说到 Go 的非凡法令:

Go 里没有默认类型是无符号类型的整数常量, 然而为了灵便, 能够应用 untyped const 类型的常量给无符号类型变量赋值(这是官网博客里的话). 这也就是意味着 untyped const 的变量突破了 Go 外面不同类型之间不能做隐式类型转换的规定. 于是 untyped const 变量能够给同族类型之间的变量做任意的加减乘除. 这就造成了一个 uint32 类型数减一个 unsigned const int 类型的数搞进去一个天文数字进去. 这里要留神的是: constant 分为 typed 和 untyped,只有 untyped 能力隐式转换.

一些总结吧

  1. Go 外面提供了多种多样的类型的变量, 应用对了诚然能够让程序节俭更多的资源, 然而应用时要特地留神抉择适当的类型, 防止造成一些莫名其妙的问题. 举荐大家程序里尽量应用 int 类型, 在 32 位零碎上其实 int 就是 int32 类型, 与 Java 的 int 的数值范畴是一样的 [-2147483648, 2147483647]. 咱们当初的服务器真的不缺这点资源. 除非你真的特地能把握这个变量的大小.
  2. 做无符号类型的加法时, 要特地留神, 肯定要先去判断做减法的变量的大小.

本篇题目是 const 导致程序后果错乱 ?, 其实也并不全是 const 造成的, 归根到底是无符号类型的数在做减法运算时的坑. 咱们写 Go 的时候要留神这一点. 当然 const 的类型大家也能够留心下, 说不定就是你下次的面试题.

看完本篇文章, 你是不是对 Go 的类型有了新的意识, 欢送关注我的公众号: HHFCodeRv 交换

正文完
 0