Repository: zeromicro/go-zero (go)

Issue:
rest/httpx.SetValidator might panic
https://github.com/zeromicro/go-zero/blob/4307ce45fc5200dc83856d542b400b8d19f0a2f4/rest/httpx/requests.go#L134-L139

由于 `atomic.Value` Strore 方法会 [校验和首次存储的数据类型是否一致](https://github.com/golang/go/blob/go1.23.5/src/sync/atomic/value.go#L120-L123)，此处存在 panic 风险。

复现代码：

```go
package main

import (
	"net/http"

	"github.com/zeromicro/go-zero/rest/httpx"
)

func main() {
	httpx.SetValidator(Valid1{})
	httpx.SetValidator(Valid2{}) // panic: sync/atomic: store of inconsistently typed value into Value
}

type Valid1 struct{}

func (v Valid1) Validate(*http.Request, any) error { return nil }

type Valid2 struct{}

func (v Valid2) Validate(*http.Request, any) error { return nil }
```

`SetValidator` 方法改进建议：

```go
func SetValidator(val Validator) {
	validator.Store(&validate{val: val})
}

type validate struct {
	val Validator
}

func (v *validate) Validate(r *http.Request, data any) error {
	if v.val != nil {
		return v.val.Validate(r, data)
	}
	
	return nil
}
```



Pull request description:
fix: different concreate types still cause panic
fix #4594 

Patch:
diff --git a/rest/httpx/requests.go b/rest/httpx/requests.go
index 87c576a9..4648a46d 100644
--- a/rest/httpx/requests.go
+++ b/rest/httpx/requests.go
@@ -5,7 +5,7 @@ import (
 	"net/http"
 	"reflect"
 	"strings"
-	"sync/atomic"
+	"sync"
 
 	"github.com/zeromicro/go-zero/core/mapping"
 	"github.com/zeromicro/go-zero/core/validation"
@@ -33,7 +33,11 @@ var (
 		pathKey,
 		mapping.WithStringValues(),
 		mapping.WithOpaqueKeys())
-	validator atomic.Value
+
+	// panic: sync/atomic: store of inconsistently typed value into Value
+	// don't use atomic.Value to store the validator, different concrete types still panic
+	validator     Validator
+	validatorLock sync.RWMutex
 )
 
 // Validator defines the interface for validating the request.
@@ -65,8 +69,8 @@ func Parse(r *http.Request, v any) error {
 
 	if valid, ok := v.(validation.Validator); ok {
 		return valid.Validate()
-	} else if val := validator.Load(); val != nil {
-		return val.(Validator).Validate(r, v)
+	} else if val := getValidator(); val != nil {
+		return val.Validate(r, v)
 	}
 
 	return nil
@@ -135,7 +139,15 @@ func ParsePath(r *http.Request, v any) error {
 // The validator is used to validate the request, only called in Parse,
 // not in ParseHeaders, ParseForm, ParseHeader, ParseJsonBody, ParsePath.
 func SetValidator(val Validator) {
-	validator.Store(val)
+	validatorLock.Lock()
+	defer validatorLock.Unlock()
+	validator = val
+}
+
+func getValidator() Validator {
+	validatorLock.RLock()
+	defer validatorLock.RUnlock()
+	return validator
 }
 
 func withJsonBody(r *http.Request) bool {


Test patch:
diff --git a/rest/httpx/requests_test.go b/rest/httpx/requests_test.go
index a544c2d9..c81a5707 100644
--- a/rest/httpx/requests_test.go
+++ b/rest/httpx/requests_test.go
@@ -734,6 +734,22 @@ func TestParseJsonStringRequest(t *testing.T) {
 	})
 }
 
+type valid1 struct{}
+
+func (v valid1) Validate(*http.Request, any) error { return nil }
+
+type valid2 struct{}
+
+func (v valid2) Validate(*http.Request, any) error { return nil }
+
+func TestSetValidatorTwice(t *testing.T) {
+	// panic: sync/atomic: store of inconsistently typed value into Value
+	assert.NotPanics(t, func() {
+		SetValidator(valid1{})
+		SetValidator(valid2{})
+	})
+}
+
 func BenchmarkParseRaw(b *testing.B) {
 	r, err := http.NewRequest(http.MethodGet, "http://hello.com/a?name=hello&age=18&percent=3.4", http.NoBody)
 	if err != nil {