Go 有什么方法或者有建议如何检查字符串是否只包含 ASCII 字符?正确的做法是什么?
根据我的研究,解决方案之一是检查是否存在大于 127 的字符。
func isASCII(s string) bool {
for _, c := range s {
if c > unicode.MaxASCII {
return false
}
}
return true
}
在 Go 中,我们关心性能,因此,我们会对您的代码进行基准测试:
func isASCII(s string) bool {
for _, c := range s {
if c > unicode.MaxASCII {
return false
}
}
return true
}
BenchmarkRange-4 20000000 82.0 ns/op
更快(更好、更惯用)的版本,避免不必要的符文转换:
func isASCII(s string) bool {
for i := 0; i < len(s); i++ {
if s[i] > unicode.MaxASCII {
return false
}
}
return true
}
BenchmarkIndex-4 30000000 55.4 ns/op
ascii_test.go
:
package main
import (
"testing"
"unicode"
)
func isASCIIRange(s string) bool {
for _, c := range s {
if c > unicode.MaxASCII {
return false
}
}
return true
}
func BenchmarkRange(b *testing.B) {
str := ascii()
b.ResetTimer()
for N := 0; N < b.N; N++ {
is := isASCIIRange(str)
if !is {
b.Fatal("notASCII")
}
}
}
func isASCIIIndex(s string) bool {
for i := 0; i < len(s); i++ {
if s[i] > unicode.MaxASCII {
return false
}
}
return true
}
func BenchmarkIndex(b *testing.B) {
str := ascii()
b.ResetTimer()
for N := 0; N < b.N; N++ {
is := isASCIIIndex(str)
if !is {
b.Log("notASCII")
}
}
}
func ascii() string {
byt := make([]byte, unicode.MaxASCII+1)
for i := range byt {
byt[i] = byte(i)
}
return string(byt)
}
输出:
$ go test ascii_test.go -bench=.
BenchmarkRange-4 20000000 82.0 ns/op
BenchmarkIndex-4 30000000 55.4 ns/op
$
看起来你的方式是最好的。
ASCII 简单地定义为:
ASCII 将 128 个指定字符编码为七位整数
因此,字符的值为 0-27(或 0-127、0x0-0x7F)。
Go 没有提供方法来检查字符串中的每个符文(或切片中的字节)是否具有特定范围内的数值,因此您的代码似乎是执行此操作的最佳方法。
另一种选择:
package main
import "golang.org/x/exp/utf8string"
func main() {
{
b := utf8string.NewString("south north").IsASCII()
println(b) // true
}
{
b := utf8string.NewString("🧡💛💚💙💜").IsASCII()
println(b) // false
}
}
https://pkg.go.dev/golang.org/x/exp/utf8string#String.IsASCII
接受的答案比最初提出的解决方案更快,但我认为最初提出的解决方案更惯用。最初的解决方案是惯用的,因为几乎总是,当您在 go 中查看字符串的内容时,您对代码点而不是单个 utf8 编码的字节值更感兴趣。这里的范围是 100% 惯用的。
在这种特定情况下,索引字符串和比较字节恰好更快,并且仍然给我们一个正确的结果。但这仍然不是一个优化的解决方案。还差得远呢
那么我们来建一个自行车棚吧。
因此,对于速度更快的非惯用(尽管基于 stdlib 代码)实现,我提出以下内容:
func isASCIIIndexBy8s32(s string) bool {
// idea adapted from here:
// https://cs.opensource.google/go/go/+/refs/tags/go1.21.5:src/unicode/utf8/utf8.go;l=528
for len(s) > 0 {
if len(s) >= 8 {
first32 := uint32(s[0]) | uint32(s[1])<<8 | uint32(s[2])<<16 | uint32(s[3])<<24
second32 := uint32(s[4]) | uint32(s[5])<<8 | uint32(s[6])<<16 | uint32(s[7])<<24
if (first32|second32)&0x80808080 != 0 {
return false
}
s = s[8:]
continue
}
if s[0] > unicode.MaxASCII {
return false
}
s = s[1:]
}
return true
}
这比索引示例快两倍多。针对 64 位 cpu 的进一步优化使我们比该 cpu 又提高了 20%。
完整代码在这里。
package main
import (
"testing"
"unicode"
)
func isASCIIRange(s string) bool {
for _, c := range s {
if c > unicode.MaxASCII {
return false
}
}
return true
}
func isASCIIIndex(s string) bool {
for i := 0; i < len(s); i++ {
if s[i] > unicode.MaxASCII {
return false
}
}
return true
}
func isASCIIIndexBy8s32(s string) bool {
// idea adapted from here:
// https://cs.opensource.google/go/go/+/refs/tags/go1.21.5:src/unicode/utf8/utf8.go;l=528
for len(s) > 0 {
if len(s) >= 8 {
first32 := uint32(s[0]) | uint32(s[1])<<8 | uint32(s[2])<<16 | uint32(s[3])<<24
second32 := uint32(s[4]) | uint32(s[5])<<8 | uint32(s[6])<<16 | uint32(s[7])<<24
if (first32|second32)&0x80808080 != 0 {
return false
}
s = s[8:]
continue
}
if s[0] > unicode.MaxASCII {
return false
}
s = s[1:]
}
return true
}
func isASCIIIndexBy8s64(s string) bool {
// optimizing the 32 bit example above for 64 bit cpus
for len(s) > 0 {
if len(s) >= 8 {
chunk := uint64(s[0]) | uint64(s[1])<<8 | uint64(s[2])<<16 | uint64(s[3])<<24 |
uint64(s[4])<<32 | uint64(s[5])<<40 | uint64(s[6])<<48 | uint64(s[7])<<56
if chunk&0x8080808080808080 != 0 {
return false
}
s = s[8:]
continue
}
if s[0] > unicode.MaxASCII {
return false
}
s = s[1:]
}
return true
}
var tests = []struct {
n string
f func(string) bool
}{
{
n: "range",
f: isASCIIRange,
}, {
n: "index",
f: isASCIIIndex,
}, {
n: "index 8 32",
f: isASCIIIndexBy8s32,
}, {
n: "index 8 64",
f: isASCIIIndexBy8s64,
},
}
func TestAsciis(t *testing.T) {
atxt := "this is ascii text"
utxt := "this is some unicode 💩💩and a lot more ascii text afterwards"
for _, tst := range tests {
t.Run(tst.n, func(t *testing.T) {
if !tst.f(atxt) {
t.Errorf("%s failed for %s", tst.n, atxt)
}
if tst.f(utxt) {
t.Errorf("%s failed for %s", tst.n, utxt)
}
})
}
}
func BenchmarkAsciis(b *testing.B) {
str := ascii()
for _, bnch := range tests {
b.Run(bnch.n, func(b *testing.B) {
b.ResetTimer()
for n := 0; n < b.N; n++ {
if !bnch.f(str) {
b.Errorf("not ascii")
}
}
})
}
}
func ascii() string {
byt := make([]byte, unicode.MaxASCII+1)
for i := range byt {
byt[i] = byte(i)
}
return string(byt)
}
运行此命令会给出:
=== RUN TestAsciis
=== RUN TestAsciis/range
=== RUN TestAsciis/index
=== RUN TestAsciis/index_8_32
=== RUN TestAsciis/index_8_64
--- PASS: TestAsciis (0.00s)
--- PASS: TestAsciis/range (0.00s)
--- PASS: TestAsciis/index (0.00s)
--- PASS: TestAsciis/index_8_32 (0.00s)
--- PASS: TestAsciis/index_8_64 (0.00s)
goos: darwin
goarch: amd64
pkg: playground
cpu: Intel(R) Core(TM) i7-1068NG7 CPU @ 2.30GHz
BenchmarkAsciis
BenchmarkAsciis/range
BenchmarkAsciis/range-8 13469913 86.72 ns/op
BenchmarkAsciis/index
BenchmarkAsciis/index-8 24108583 49.56 ns/op
BenchmarkAsciis/index_8_32
BenchmarkAsciis/index_8_32-8 62092825 20.89 ns/op
BenchmarkAsciis/index_8_64
BenchmarkAsciis/index_8_64-8 77044797 15.91 ns/op
PASS