Go articles
Go articles
学会 gin 参数校验之 validator 库,看这一篇就足够了
字符串约束
excludesall:不包含参数中任意的 UNICODE 字符,例如 excludesall=ab
excludesrune:不包含参数表示的 rune 字符,excludesrune=asong
startswith:以参数子串为前缀,例如 startswith=hi
endswith:以参数子串为后缀,例如 endswith=bye。
contains=:包含参数子串,例如 contains=email
containsany:包含参数中任意的 UNICODE 字符,例如 containsany=ab
containsrune:包含参数表示的 rune 字符,例如`containsrune=asong
excludes:不包含参数子串,例如 excludes=email
范围约束
范围约束的字段类型分为三种:
对于数值,我们则可以约束其值 对于切片、数组和 map,我们则可以约束其长度 对于字符串,我们则可以约束其长度
常用 tag 介绍:
ne:不等于参数值,例如 ne=5 gt:大于参数值,例如 gt=5 gte:大于等于参数值,例如 gte=50 lt:小于参数值,例如 lt=50 lte:小于等于参数值,例如 lte=50 oneof:只能是列举出的值其中一个,这些值必须是数值或字符串,以空格分隔,如果字符串中有空格,将字符串用单引号包围,例如 oneof=male female。 eq:等于参数值,注意与 len 不同。对于字符串,eq 约束字符串本身的值,而 len 约束字符串长度。例如 eq=10 len:等于参数值,例如 len=10 max:小于等于参数值,例如 max=10 min:大于等于参数值,例如 min=10
Fields 约束
eqfield:定义字段间的相等约束,用于约束同一结构体中的字段。例如:eqfield=Password eqcsfield:约束统一结构体中字段等于另一个字段(相对),确认密码时可以使用,例如:eqfiel=ConfirmPassword nefield:用来约束两个字段是否相同,确认两种颜色是否一致时可以使用,例如:nefield=Color1 necsfield:约束两个字段是否相同(相对)
常用约束
unique:指定唯一性约束,不同类型处理不同:
对于 map,unique 约束没有重复的值 对于数组和切片,unique 没有重复的值 对于元素类型为结构体的碎片,unique 约束结构体对象的某个字段不重复,使用 unique=field 指定字段名
email:使用 email 来限制字段必须是邮件形式,直接写 eamil 即可,无需加任何指定。
omitempty:字段未设置,则忽略
-:跳过该字段,不检验
|:使用多个约束,只需要满足其中一个,例如 rgb|rgba
required:字段必须设置,不能为默认值
type Info struct {
CreateTime time.Time `form:"create_time" binding:"required,timing" time_format:"2006-01-02"`
UpdateTime time.Time `form:"update_time" binding:"required,timing" time_format:"2006-01-02"`
}
没有什么不可能:修改 Go 结构体的私有字段
在我们的 main 函数中,你不能访问 Person 的 age 字段:
package main;
import (
"fmt"
"reflect"
"unsafe"
"github.com/smallnest/private/model"
)
func main() {
p := model.NewPerson("Alice", 30)
fmt.Printf("Person: %+v\n", p)
// fmt.Println(p.age) // error: p.age undefined (cannot refer to unexported field or method age)
t := model.NewTeacher("smallnest", 18)
fmt.Printf("Teacher: %+v\n", t) // Teacher: {Name:Alice Age:30}
}
那么真的就无法访问了吗?也不一定,我们可以通过反射的方式访问私有字段:
p := model.NewPerson("Alice", 30)
age := reflect.ValueOf(p).FieldByName("age")
fmt.Printf("原始值: %d, CanSet: %v\n", age.Int(), age.CanSet()) // 30, false
运行这个程序,可以看到我们获得了这个私有字段 age 的值:
原始值: 30, CanSet: false
这里我们以标准库的 sync.Mutex 结构体为例,sync.Mutex 包含两个字段,这两个字段都是私有的:
type Mutex struct {
state int32
sema uint32
}
正常情况下你只能通过 Mutex.Lock 和 Mutex.Unlock 来间接的修改这两个字段。
现在我们演示通过 hack 的方式修改 Mutex 的 state 字段的值:
func setPrivateField() {
var mu sync.Mutex
mu.Lock()
field := reflect.ValueOf(&mu).Elem().FieldByName("state")
state := field.Interface().(*int32)
fmt.Println(*state) // ❶
flagField := reflect.ValueOf(&field).Elem().FieldByName("flag")
flagPtr := (*uintptr)(unsafe.Pointer(flagField.UnsafeAddr()))
// 修改 flag 字段的值
*flagPtr &= ^uintptr(flagRO) // ❷
field.Set(reflect.ValueOf(int32(0)))
mu.Lock() // ❸
fmt.Println(*state)
}
type flag uintptr
const (
flagKindWidth = 5 // there are 27 kinds
flagKindMask flag = 1<<flagKindWidth - 1
flagStickyRO flag = 1 << 5
flagEmbedRO flag = 1 << 6
flagIndir flag = 1 << 7
flagAddr flag = 1 << 8
flagMethod flag = 1 << 9
flagMethodShift = 10
flagRO flag = flagStickyRO | flagEmbedRO
)
❶ 处我们已经介绍过了,访问私有字段的值,这里会打印出 1 ❶ 处我们清除了 flag 字段的 flagRO 标志位,这样就不会报 reflect: reflect.Value.SetInt using value obtained using unexported field 错误了 ❸ 处不会导致二次加锁带来的死锁,因为 state 字段的值已经被修改为 0 了,所以不会阻塞。最后打印结果还是 1
这样我们就可以实现了修改私有字段的值了。
使用 unexported 字段的 Value 设置公开字段
看 reflect.Value.Set 的源码,我们可以看到它会检查参数的值是否 unexported,如果是,就会报错,下面就是一个例子:
func setUnexportedField2() {
alice := model.NewPerson("Alice", 30)
bob := model.NewTeacher("Bob", 40)
bobAgent := reflect.ValueOf(&bob).Elem().FieldByName("Age")
aliceAge := reflect.ValueOf(&alice).Elem().FieldByName("age")
bobAgent.Set(aliceAge) // ❹
}
注意 ❹ 处,我们尝试把 alice 的私有字段 age 的值赋值给 bob 的公开字段 Age,这里会报错:
panic: reflect: reflect.Value.Set using value obtained using unexported field
goroutine 1 [running]:
reflect.flag.mustBeExportedSlow(0x1400012a000?)
/usr/local/go/src/reflect/value.go:250 +0x70
reflect.flag.mustBeExported(...)
/usr/local/go/src/reflect/value.go:241
reflect.Value.Set({0x102773a60?, 0x1400012a028?, 0x60?}, {0x102773a60?, 0x1400012a010?, 0x1027002b8?})
/usr/local/go/src/reflect/value.go:2320 +0x88
main.setUnexportedField2()
/Users/smallnest/workspace/study/private/main.go:50 +0x168
main.main()
/Users/smallnest/workspace/study/private/main.go:18 +0x1c
exit status 2
原因 alice 的 age 值被识别为私有字段,它是不能用来赋值给公开字段的。
有了上一节的经验,我们同样可以绕过这个检查,实现这个赋值:
func setUnexportedField2() {
alice := model.NewPerson("Alice", 30)
bob := model.NewTeacher("Bob", 40)
bobAgent := reflect.ValueOf(&bob).Elem().FieldByName("Age")
aliceAge := reflect.ValueOf(&alice).Elem().FieldByName("age")
// 修改 flag 字段的值
flagField := reflect.ValueOf(&aliceAge).Elem().FieldByName("flag")
flagPtr := (*uintptr)(unsafe.Pointer(flagField.UnsafeAddr()))
*flagPtr &= ^uintptr(flagRO) // ❺
bobAgent.Set(reflect.ValueOf(50))
bobAgent.Set(aliceAge) // ❻
}
❺ 处我们修改了 aliceAge 的 flag 字段,去掉了 flagRO 标志位,这样就不会报错了,❻ 处我们成功的把 alice 的私有字段 age 的值赋值给 bob 的公开字段 Age。
这样我们就可以实现了使用私有字段的值给其他 Value 值进行赋值了。
给 unaddressable 的值设置值
回到最初的问题,我们尝试给一个 unaddressable 的值设置值,会报错。
结合上面的 hack 手段,我们也可以绕过限制,给 unaddressable 的值设置值:
func setUnaddressableValue() {
var x = 47
v := reflect.ValueOf(x)
fmt.Printf("原始值: %d, CanSet: %v\n", v.Int(), v.CanSet()) // 47, false
// v.Set(reflect.ValueOf(50))
flagField := reflect.ValueOf(&v).Elem().FieldByName("flag")
flagPtr := (*uintptr)(unsafe.Pointer(flagField.UnsafeAddr()))
// 修改 flag 字段的值
*flagPtr |= uintptr(flagAddr) // 设置可寻址标志位
fmt.Printf("CanSet: %v\n", v.CanSet()) // true
v.SetInt(50)
fmt.Printf("修改后的值:%d\n", v.Int()) // 50
}
四种字符串和 bytes 互相转换方式的性能比较
三、新型 unsafe 方式
func toBytes(s string) []byte {
if len(s) == 0 {
return nil
}
return unsafe.Slice(unsafe.StringData(s), len(s))
}
func toString(b []byte) string {
if len(b) == 0 {
return ""
}
return unsafe.String(unsafe.SliceData(b), len(b))
}
四、kubernetes 的实现
func toK8sBytes(s string) []byte {
return *(*[]byte)(unsafe.Pointer(&s))
}
func toK8sString(b []byte) string {
return *(*string)(unsafe.Pointer(&b))
}
在 Mac mini M2 上运行,go1.22.6 darwin/arm64,结果如下:
goos: darwin
goarch: arm64
pkg: github.com/smallnest/study/str2bytes
BenchmarkStringToBytes/强制转换-8 78813638 14.73 ns/op 16 B/op 1 allocs/op
BenchmarkStringToBytes/传统转换-8 599346962 2.010 ns/op 0 B/op 0 allocs/op
BenchmarkStringToBytes/新型转换-8 624976126 1.929 ns/op 0 B/op 0 allocs/op
BenchmarkStringToBytes/k8s转换-8 887370499 1.211 ns/op 0 B/op 0 allocs/op
BenchmarkBytesToString/强制转换-8 92011309 12.68 ns/op 16 B/op 1 allocs/op
BenchmarkBytesToString/传统转换-8 815922964 1.471 ns/op 0 B/op 0 allocs/op
BenchmarkBytesToString/新型转换-8 624965414 1.922 ns/op 0 B/op 0 allocs/op
BenchmarkBytesToString/k8s转换-8 1000000000 1.194 ns/op 0 B/op 0 allocs/op
string 转 bytes 性能最好的是 k8s 方案,新型转换和传统转换性能差不多,新型方案略好,强制转换性能最差。
而对于 bytes 转 string,k8s 方案性能最好,传统转换次之,新型转换性能再次之,强制转换性能非常不好。
在 Linux amd64 上运行,go1.22.0 linux/amd64,结果如下:
goos: linux
goarch: amd64
pkg: test
cpu: Intel(R) Xeon(R) Platinum
BenchmarkStringToBytes/强制转换-2 30606319 42.02 ns/op 16 B/op 1 allocs/op
BenchmarkStringToBytes/传统转换-2 315913948 3.779 ns/op 0 B/op 0 allocs/op
BenchmarkStringToBytes/新型转换-2 411972518 2.753 ns/op 0 B/op 0 allocs/op
BenchmarkStringToBytes/k8s转换-2 449640819 2.770 ns/op 0 B/op 0 allocs/op
BenchmarkBytesToString/强制转换-2 38716465 29.18 ns/op 16 B/op 1 allocs/op
BenchmarkBytesToString/传统转换-2 458832459 2.593 ns/op 0 B/op 0 allocs/op
BenchmarkBytesToString/新型转换-2 439537762 2.762 ns/op 0 B/op 0 allocs/op
BenchmarkBytesToString/k8s转换-2 478885546 2.375 ns/op 0 B/op 0 allocs/op
整体上看,k8s 方案、传统转换、新型转换性能都挺好,强制转换性能最差。k8s 在 bytes 转 string 上性能最好。