字符串与数字转换

几种常见的字符转数字方式

假设我们要将以下字符转换为数字

const s = "123456789"

ParseInt

这是一种将字符转换为int64的方式, 优点是可以将多种格式(比如二进制)的字符串转换成int64

func strconvConvert(s string) {
	strconv.ParseInt(s, 10, 64)
}

Atoi

与ParseInt(s, 10, 0)行为一致, 返回int

func atoiConvert(s string) {
	strconv.Atoi(s)
}

ASCII 值

必须确定字符串为可转换的格式

func asciiConvert(s string) {
	n := 0
	for _, ch := range []byte(s) {
		ch -= '0'
		n = n*10 + int(ch)
	}
}

测试

goos: darwin
goarch: amd64
pkg: daemon
cpu: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz
BenchmarkStrconvConvert
BenchmarkStrconvConvert-12    	61132210	        18.36 ns/op
BenchmarkAsciiConvert
BenchmarkAsciiConvert-12      	362531002	         3.327 ns/op
BenchmarkAtoiConvert
BenchmarkAtoiConvert-12       	154894201	         7.748 ns/op

可以看到, ascii值计算的方式效率最高。 所以在明确传入参数的格式情况下, 用这种方式是最优的。

原因

查看ParseInt源码, 发现它为了兼容多种进制格式的转换,进行多次计算,与字符串判断,所以效率最低。

func ParseInt(s string, base int, bitSize int) (i int64, err error) {
	const fnParseInt = "ParseInt"

	if s == "" {
		return 0, syntaxError(fnParseInt, s)
	}

	// 摘掉符号
	s0 := s
	neg := false
	if s[0] == '+' {
		s = s[1:]
	} else if s[0] == '-' {
		neg = true
		s = s[1:]
	}

	// 转换为无符号并计算范围
	var un uint64
    // ParseUint中兼容多种进制格式, 还有多个判断
	un, err = ParseUint(s, base, bitSize)
	if err != nil && err.(*NumError).Err != ErrRange {
		err.(*NumError).Func = fnParseInt
		err.(*NumError).Num = s0
		return 0, err
	}

	if bitSize == 0 {
		bitSize = IntSize
	}

	cutoff := uint64(1 << uint(bitSize-1))
	if !neg && un >= cutoff {
		return int64(cutoff - 1), rangeError(fnParseInt, s0)
	}
	if neg && un > cutoff {
		return -int64(cutoff), rangeError(fnParseInt, s0)
	}
	n := int64(un)
	if neg {
		n = -n
	}
	return n, nil
}

Atoi中也是一样使用了Ascii计算的方式, 并且逻辑更完善。

func Atoi(s string) (int, error) {
	const fnAtoi = "Atoi"

	sLen := len(s)
    // 范围内的直接转换
	if intSize == 32 && (0 < sLen && sLen < 10) ||
		intSize == 64 && (0 < sLen && sLen < 19) {
		// Fast path for small integers that fit int type.
		s0 := s
		if s[0] == '-' || s[0] == '+' {
			s = s[1:]
			if len(s) < 1 {
				return 0, &NumError{fnAtoi, s0, ErrSyntax}
			}
		}

		n := 0
		for _, ch := range []byte(s) {
			ch -= '0'
			if ch > 9 {
				return 0, &NumError{fnAtoi, s0, ErrSyntax}
			}
			n = n*10 + int(ch)
		}
		if s0[0] == '-' {
			n = -n
		}
		return n, nil
	}

	// 上面代码无法转换时才会走到这个慢路径
	i64, err := ParseInt(s, 10, 0)
	if nerr, ok := err.(*NumError); ok {
		nerr.Func = fnAtoi
	}
	return int(i64), err
}

而我们直接转换的方式快的原因其实是舍弃了完整的逻辑判断。