# 2.3 strconv — 字符串和基本数据类型之间转换 #
这里的基本数据类型包括:布尔、整型(包括有/无符号、二进制、八进制、十进制和十六进制)和浮点型等。
## 2.3.1 strconv 包转换错误处理 ##
介绍具体的转换之前,先看看 *strconv* 中的错误处理。
由于将字符串转为其他数据类型可能会出错,*strconv* 包定义了两个 *error* 类型的变量:*ErrRange* 和 *ErrSyntax*。其中,*ErrRange* 表示值超过了类型能表示的最大范围,比如将 "128" 转为 int8 就会返回这个错误;*ErrSyntax* 表示语法错误,比如将 "" 转为 int 类型会返回这个错误。
然而,在返回错误的时候,不是直接将上面的变量值返回,而是通过构造一个 *NumError* 类型的 *error* 对象返回。*NumError* 结构的定义如下:
// A NumError records a failed conversion.
type NumError struct {
Func string // the failing function (ParseBool, ParseInt, ParseUint, ParseFloat)
Num string // the input
Err error // the reason the conversion failed (ErrRange, ErrSyntax)
}
可见,该结构记录了转换过程中发生的错误信息。该结构不仅包含了一个 *error* 类型的成员,记录具体的错误信息,而且它自己也实现了 *error* 接口:
func (e *NumError) Error() string {
return "strconv." + e.Func + ": " + "parsing " + Quote(e.Num) + ": " + e.Err.Error()
}
包的实现中,定义了两个便捷函数,用于构造 *NumError* 对象:
func syntaxError(fn, str string) *NumError {
return &NumError{fn, str, ErrSyntax}
}
func rangeError(fn, str string) *NumError {
return &NumError{fn, str, ErrRange}
}
在遇到 *ErrSyntax* 或 *ErrRange* 错误时,通过上面的函数构造 *NumError* 对象。
## 2.3.2 字符串和整型之间的转换 ##
### 2.3.2.1 字符串转为整型 ###
包括三个函数:ParseInt、ParseUint 和 Atoi,函数原型如下:
func ParseInt(s string, base int, bitSize int) (i int64, err error)
func ParseUint(s string, base int, bitSize int) (n uint64, err error)
func Atoi(s string) (i int, err error)
其中,Atoi 是 ParseInt 的便捷版,内部通过调用 *ParseInt(s, 10, 0)* 来实现的;ParseInt 转为有符号整型;ParseUint 转为无符号整型,着重介绍 ParseInt。
参数 *base* 代表字符串按照给定的进制进行解释。一般的,base 的取值为 2~36,如果 base 的值为 0,则会根据字符串的前缀来确定 base 的值:"0x" 表示 16 进制; "0" 表示 8 进制;否则就是 10 进制。
参数 *bitSize* 表示的是整数取值范围,或者说整数的具体类型。取值 0、8、16、32 和 64 分别代表 int、int8、int16、int32 和 int64。
这里有必要说一下,当 bitSize==0 时的情况。
Go中,int/uint 类型,不同系统能表示的范围是不一样的,目前的实现是,32 位系统占 4 个字节;64 位系统占 8 个字节。当 bitSize==0 时,应该表示 32 位还是 64 位呢?这里没有利用 *runtime.GOARCH* 之类的方式,而是巧妙的通过如下表达式确定 intSize:
const intSize = 32 << uint(^uint(0)>>63)
const IntSize = intSize // number of bits in int, uint (32 or 64)
主要是 *^uint(0)>>63* 这个表达式。操作符 *^* 在这里是一元操作符 按位取反,而不是 按位异或。更多解释可以参考:[Go位运算:取反和异或](http://studygolang.com/topics/303)。
问题:下面的代码 n 和 err 的值分别是什么?
n, err := strconv.ParseInt("128", 10, 8)
在 *ParseInt/ParseUint* 的实现中,如果字符串表示的整数超过了 bitSize 参数能够表示的范围,则会返回 ErrRange,同时会返回 bitSize 能够表示的最大或最小值。因此,这里的 n 是 127。
另外,*ParseInt* 返回的是 int64,这是为了能够容纳所有的整型,在实际使用中,可以根据传递的 bitSize,然后将结果转为实际需要的类型。
转换的基本原理(以 "128" 转 为 10 进制 int 为例):
s := "128"
n := 0
for i := 0; i < len(s); i++ {
n *= 10 + s[i] // base
}
在循环处理的过程中,会检查数据的有效性和是否越界等。
### 2.3.2.2 整型转为字符串 ###
实际应用中,我们经常会遇到需要将字符串和整型连接起来,在Java中,可以通过操作符 "+" 做到。不过,在Go语言中,你需要将整型转为字符串类型,然后才能进行连接。这个时候,*strconv* 包中的整型转字符串的相关函数就派上用场了。这些函数签名如下:
func FormatUint(i uint64, base int) string // 无符号整型转字符串
func FormatInt(i int64, base int) string // 有符号整型转字符串
func Itoa(i int) string
其中,*Itoa* 内部直接调用 *FormatInt(i, 10)* 实现的。base 参数可以取 2~36(0-9,a-z)。
转换的基本原理(以 10 进制的 127 转 string 为例) :
const digits = "0123456789abcdefghijklmnopqrstuvwxyz"
u := uint64(127)
var a [65]byte
i := len(a)
b := uint64(base)
for u >= b {
i--
a[i] = digits[uintptr(u%b)]
u /= b
}
i--
a[i] = digits[uintptr(u)]
return string(a[1:])
即将整数每一位数字对应到相应的字符,存入字符数组中,最后字符数组转为字符串即为结果。
具体实现时,当 base 是 2 的幂次方时,有优化处理(移位和掩码);十进制也做了优化。
标准库还提供了另外两个函数:*AppendInt* 和 *AppendUint*,这两个函数不是将整数转为字符串,而是将整数转为字符数组 append 到目标字符数组中。(最终,我们也可以通过返回的 []byte 得到字符串)
除了使用上述方法将整数转为字符串外,经常见到有人使用 *fmt* 包来做这件事。如:
fmt.Sprintf("%d", 127)
那么,这两种方式我们该怎么选择呢?我们主要来考察一下性能。
startTime := time.Now()
for i := 0; i < 10000; i++ {
fmt.Sprintf("%d", i)
}
fmt.Println(time.Now().Sub(startTime))
startTime := time.Now()
for i := 0; i < 10000; i++ {
strconv.Itoa(i)
}
fmt.Println(time.Now().Sub(startTime))
我们分别循环转换了10000次。*Sprintf* 的时间是 3.549761ms,而 *Itoa* 的时间是 848.208us,相差 4 倍多。
*Sprintf* 性能差些可以预见,因为它接收的是 interface,需要进行反射等操作。个人建议使用 *strconv* 包中的方法进行转换。
注意:别想着通过 string(65) 这种方式将整数转为字符串,这样实际上得到的会是 ASCCII 值为 65 的字符,即 'A'。
思考:
给定一个 40 以内的正整数,如何快速判断其是否是 2 的幂次方?
*提示:在 strconv 包源码 itoa.go 文件中找答案*
## 2.3.3 字符串和布尔值之间的转换 ##
Go中字符串和布尔值之间的转换比较简单,主要有三个函数:
// 接受 1, t, T, TRUE, true, True, 0, f, F, FALSE, false, False 等字符串;
// 其他形式的字符串会返回错误
func ParseBool(str string) (value bool, err error)
// 直接返回 "true" 或 "false"
func FormatBool(b bool) string
// 将 "true" 或 "false" append 到 dst 中
// 这里用了一个 append 函数对于字符串的特殊形式:append(dst, "true"...)
func AppendBool(dst []byte, b bool)
## 2.3.4 字符串和浮点数之间的转换 ##
类似的,包含三个函数:
func ParseFloat(s string, bitSize int) (f float64, err error)
func FormatFloat(f float64, fmt byte, prec, bitSize int) string
func AppendFloat(dst []byte, f float64, fmt byte, prec int, bitSize int)
函数的命名和作用跟上面讲解的其他类型一致。
关于 *FormatFloat* 的 *fmt* 参数, 在第一章第三节[格式化IO](/chapter01/01.3.md)中有详细介绍。而 *prec* 表示有效数字(对 *fmt='b'* 无效),对于 'e', 'E' 和 'f',有效数字用于小数点之后的位数;对于 'g' 和 'G',则是所有的有效数字。例如:
strconv.FormatFloat(1223.13252, 'e', 3, 32) // 结果:1.223e+03
strconv.FormatFloat(1223.13252, 'g', 3, 32) // 结果:1.22e+03
由于浮点数有精度的问题,精度不一样,ParseFloat 和 FormatFloat 可能达不到互逆的效果。如:
s := strconv.FormatFloat(1234.5678, 'g', 6, 64)
strconv.ParseFloat(s, 64)
另外,fmt='b' 时,得到的字符串是无法通过 *ParseFloat* 还原的。
特别地(不区分大小写),+inf/inf,+infinity/infinity,-inf/-infinity 和 nan 通过 ParseFloat 转换分别返回对应的值(在 math 包中定义)。
同样的,基于性能的考虑,应该使用 *FormatFloat* 而不是 *fmt.Sprintf*。
## 2.3.5 其他导出的函数 ##
如果要输出这样一句话:*This is "studygolang.com" website*. 该如何做?
So easy:
fmt.Println(`This is "studygolang.com" website`)
如果没有 *``* 符号,该怎么做?转义:
fmt.Println("This is \"studygolang.com\" website")
除了这两种方法,*strconv* 包还提供了函数这做件事(Quote 函数)。我们称 "studygolang.com" 这种用双引号引起来的字符串为 Go 语言字面值字符串(Go string literal)。
上面的一句话可以这么做:
fmt.Println("This is", strconv.Quote("studygolang.com"), "website")
# 导航 #
- 上一节:[bytes — byte slice 便利操作](02.2.md)
- 下一节:[regexp — 正则表达式](02.4.md)
- 简介
- 第一章 输入输出 (Input/Output)
- 1.1 io — 基本的 IO 接口
- 1.2 ioutil — 方便的 IO 操作函数集
- 1.3 fmt — 格式化 IO
- 1.4 bufio — 缓存 IO
- 第二章 文本
- 2.1 strings — 字符串操作
- 2.2 bytes — byte slice 便利操作
- 2.3 strconv — 字符串和基本数据类型之间转换
- 2.4 regexp — 正则表达式
- 2.5 unicode — Unicode 码点、UTF-8/16 编码
- 第三章 数据结构与算法
- 3.1 sort — 排序算法
- 3.2 index/suffixarray — 后缀数组实现子字符串查询
- 3.3 container — 容器数据类型:heap、list 和 ring
- 第四章 日期与时间
- 4.1 主要类型概述
- 4.2 时区
- 4.3 Time类型详解
- 4.4 定时器
- 第六章 文件系统
- 6.1 os — 平台无关的操作系统功能实现
- 6.2 path/filepath — 操作路径
- 第七章 数据持久存储与交换
- 7.1 database/sql — SQL/SQL-Like 数据库操作接口
- 第八章 数据压缩与归档
- 8.1 flate * DEFLATE 压缩算法
- 第九章 测试
- 9.1 testing * 单元测试
- 9.2 testing * 基准测试
- 9.3 testing * 子测试
- 9.4 testing * 运行并验证示例
- 9.5 testing * 其他功能
- 9.6 httptest * HTTP 测试辅助工具
- 9.7 总结
- 第十章 进程、线程与 goroutine
- 10.1 创建进程
- 10.2 进程属性和控制
- 10.3 线程
- 10.4 进程间通信
- 第十三章 应用构建 与 debug
- 13.1 flag * 命令行参数解析
- 13.2 log * 日志记录
- 13.3 expvar * 公共变量的标准化接口
- 13.4 runtime/debug * 运行时的调试工具