1. Элегантное решение
Вот еще одно решение с использованием fmt.Sscanf()
. Это конечно не самое быстрое решение, но оно элегантное. Он сканирует прямо в поля color.RGBA
struct:
func ParseHexColor(s string) (c color.RGBA, err error) {
c.A = 0xff
switch len(s) {
case 7:
_, err = fmt.Sscanf(s, "#%02x%02x%02x", &c.R, &c.G, &c.B)
case 4:
_, err = fmt.Sscanf(s, "#%1x%1x%1x", &c.R, &c.G, &c.B)
// Double the hex digits:
c.R *= 17
c.G *= 17
c.B *= 17
default:
err = fmt.Errorf("invalid length, must be 7 or 4")
}
return
}
Тестирование:
hexCols := []string{
"#112233",
"#123",
"#000233",
"#023",
"invalid",
"#abcd",
"#-12",
}
for _, hc := range hexCols {
c, err := ParseHexColor(hc)
fmt.Printf("%-7s = %3v, %v\n", hc, c, err)
}
Вывод (попробуйте на Go Playground ):
#112233 = { 17 34 51 255}, <nil>
#123 = { 17 34 51 255}, <nil>
#000233 = { 0 2 51 255}, <nil>
#023 = { 0 34 51 255}, <nil>
invalid = { 0 0 0 255}, input does not match format
#abcd = { 0 0 0 255}, invalid length, must be 7 or 4
#-12 = { 0 0 0 255}, expected integer
2. Быстрое решение
Если производительность имеет значение, fmt.Sscanf()
- действительно плохой выбор. Для этого требуется строка формата, которую реализация должна проанализировать, и в соответствии с ней проанализировать входные данные и использовать отражение, чтобы сохранить результат в указанных значениях.
Поскольку задача в основном состоит в "разборе" шестнадцатеричного значения, мы можем добиться большего успеха, чем эта. Нам даже не нужно вызывать обычную библиотеку шестнадцатеричного синтаксического анализа (например, encoding/hex
), мы можем сделать это самостоятельно. Нам даже не нужно обрабатывать входные данные как string
, или даже как серию rune
с, мы можем понизить до уровня обработки их как серии байтов. Да, Go хранит значения string
в виде байтовых последовательностей UTF-8 в памяти, но если вход является допустимой цветовой строкой, все его байты должны находиться в диапазоне 0..127
, что соответствует байтам 1-к-1. Если это не так, входные данные уже будут недействительными, что мы обнаружим, но какой цвет мы возвращаем в этом случае, не должно иметь значения (не имеет значения).
Теперь посмотрим на быструю реализацию:
var errInvalidFormat = errors.New("invalid format")
func ParseHexColorFast(s string) (c color.RGBA, err error) {
c.A = 0xff
if s[0] != '#' {
return c, errInvalidFormat
}
hexToByte := func(b byte) byte {
switch {
case b >= '0' && b <= '9':
return b - '0'
case b >= 'a' && b <= 'f':
return b - 'a' + 10
case b >= 'A' && b <= 'F':
return b - 'A' + 10
}
err = errInvalidFormat
return 0
}
switch len(s) {
case 7:
c.R = hexToByte(s[1])<<4 + hexToByte(s[2])
c.G = hexToByte(s[3])<<4 + hexToByte(s[4])
c.B = hexToByte(s[5])<<4 + hexToByte(s[6])
case 4:
c.R = hexToByte(s[1]) * 17
c.G = hexToByte(s[2]) * 17
c.B = hexToByte(s[3]) * 17
default:
err = errInvalidFormat
}
return
}
Тестирование с теми же входами, что и в первом примере, вывод (попробуйте на Go Playground ):
#112233 = { 17 34 51 255}, <nil>
#123 = { 17 34 51 255}, <nil>
#000233 = { 0 2 51 255}, <nil>
#023 = { 0 34 51 255}, <nil>
invalid = { 0 0 0 255}, invalid format
#abcd = { 0 0 0 255}, invalid format
#-12 = { 0 17 34 255}, invalid format
3. Ориентиры
Давайте оценим эти 2 решения. Код сравнения будет включать вызов их в длинных и коротких форматах. Ошибка исключена.
func BenchmarkParseHexColor(b *testing.B) {
for i := 0; i < b.N; i++ {
ParseHexColor("#112233")
ParseHexColor("#123")
}
}
func BenchmarkParseHexColorFast(b *testing.B) {
for i := 0; i < b.N; i++ {
ParseHexColorFast("#112233")
ParseHexColorFast("#123")
}
}
А вот результаты теста:
go test -bench . -benchmem
BenchmarkParseHexColor-4 500000 2557 ns/op 144 B/op 9 allocs/op
BenchmarkParseHexColorFast-4 100000000 10.3 ns/op 0 B/op 0 allocs/op
Как мы видим, «быстрое» решение примерно в 1050 * 250 раз быстрее и не использует распределения (в отличие от «элегантного» решения).