Repository: gin-gonic/gin (go)

Issue:
Can UnmarshalParam support custom slice of string
## Feature
Support UnmarshalParam for custom string slice type in tag form. 

## Description

<!-- Description of a problem -->
If we define a custom type of string slice and implement the Gin's interface of UnmarshalParam, then we can't get the expected result.

## How to reproduce

<!-- The smallest possible code example to show the problem that can be compiled, like -->
```
package main

import (
	"fmt"
	"net/http"
	"strings"

	"github.com/gin-gonic/gin"
)

type CustomPath []string

func (b *CustomPath) UnmarshalParam(param string) error {
	elems := strings.Split(param, "/")
	n := len(elems)
	if n < 2 {
		return fmt.Errorf("invalid path: %s", param)
	}

	*b = elems
	return nil
}

type PathRequest struct {
	Paths CustomPath `form:"path"`
}

func main() {
	g := gin.Default()
	g.GET("", func(c *gin.Context) {
		var request PathRequest
		if err := c.ShouldBind(&request); err != nil {
			c.String(http.StatusBadRequest, "request parse err: %v", err)
			return
		}

		c.String(200, "Hello %s", request.Paths)
	})
	g.Run(":9000")
}

```

## Expectations

<!-- Your expectation result of 'curl' command, like -->
```
$ curl 'http://127.0.0.1:9000?path=hello/world' 
CustomPath:  [hello world]
```

## Actual result

<!-- Actual result showing the problem -->
```
$ curl 'http://127.0.0.1:9000?path=hello/world' 
CustomPath:  [hello/world]
```

## Environment

- go version: go1.22.0 darwin/arm64
- gin version (or commit ref): v1.10.0
- operating system: macOS 13.5


Pull request:
feat(form): add custom string slice for form tag unmarshal (#3970)
Resolves #3970
```golang
package main

import (
	"fmt"
	"net/http"
	"strings"

	"github.com/gin-gonic/gin"
)

type CustomPath []string

func (b *CustomPath) UnmarshalParam(param string) error {
	elems := strings.Split(param, "/")
	n := len(elems)
	if n < 2 {
		return fmt.Errorf("invalid path: %s", param)
	}

	*b = elems
	return nil
}

type PathRequest struct {
	Paths CustomPath `form:"path"`
}

func main() {
	g := gin.Default()
	g.GET("", func(c *gin.Context) {
		var request PathRequest
		if err := c.ShouldBind(&request); err != nil {
			c.String(http.StatusBadRequest, "request parse err: %v", err)
			return
		}

		c.String(200, "Hello %s", request.Paths)
	})
	g.Run(":9000")
}



Calling this server with a string parameters path will get the expected slices.

Example: 
$ curl 'http://127.0.0.1:9000?path=hello/world' 
CustomPath:  [hello world]

Patch:
diff --git a/binding/form_mapping.go b/binding/form_mapping.go
index 108606f..33389b2 100644
--- a/binding/form_mapping.go
+++ b/binding/form_mapping.go
@@ -193,14 +193,25 @@ func setByForm(value reflect.Value, field reflect.StructField, form map[string][
 		if !ok {
 			vs = []string{opt.defaultValue}
 		}
+
+		if ok, err = trySetCustom(vs[0], value); ok {
+			return ok, err
+		}
+
 		return true, setSlice(vs, value, field)
 	case reflect.Array:
 		if !ok {
 			vs = []string{opt.defaultValue}
 		}
+
+		if ok, err = trySetCustom(vs[0], value); ok {
+			return ok, err
+		}
+
 		if len(vs) != value.Len() {
 			return false, fmt.Errorf("%q is not valid value for %s", vs, value.Type().String())
 		}
+
 		return true, setArray(vs, value, field)
 	default:
 		var val string


Test patch:
diff --git a/binding/form_mapping_test.go b/binding/form_mapping_test.go
index ed01a08..afd51f9 100644
--- a/binding/form_mapping_test.go
+++ b/binding/form_mapping_test.go
@@ -5,6 +5,7 @@
 package binding
 
 import (
+	"encoding/hex"
 	"fmt"
 	"mime/multipart"
 	"reflect"
@@ -422,3 +423,89 @@ func TestMappingCustomPointerStructTypeWithURITag(t *testing.T) {
 	assert.EqualValues(t, "/foo", s.FileData.Path)
 	assert.EqualValues(t, "happiness", s.FileData.Name)
 }
+
+type customPath []string
+
+func (p *customPath) UnmarshalParam(param string) error {
+	elems := strings.Split(param, "/")
+	n := len(elems)
+	if n < 2 {
+		return fmt.Errorf("invalid format")
+	}
+
+	*p = elems
+	return nil
+}
+
+func TestMappingCustomSliceUri(t *testing.T) {
+	var s struct {
+		FileData customPath `uri:"path"`
+	}
+	err := mappingByPtr(&s, formSource{"path": {`bar/foo`}}, "uri")
+	assert.NoError(t, err)
+
+	assert.EqualValues(t, "bar", s.FileData[0])
+	assert.EqualValues(t, "foo", s.FileData[1])
+}
+
+func TestMappingCustomSliceForm(t *testing.T) {
+	var s struct {
+		FileData customPath `form:"path"`
+	}
+	err := mappingByPtr(&s, formSource{"path": {`bar/foo`}}, "form")
+	assert.NoError(t, err)
+
+	assert.EqualValues(t, "bar", s.FileData[0])
+	assert.EqualValues(t, "foo", s.FileData[1])
+}
+
+type objectID [12]byte
+
+func (o *objectID) UnmarshalParam(param string) error {
+	oid, err := convertTo(param)
+	if err != nil {
+		return err
+	}
+
+	*o = oid
+	return nil
+}
+
+func convertTo(s string) (objectID, error) {
+	var nilObjectID objectID
+	if len(s) != 24 {
+		return nilObjectID, fmt.Errorf("invalid format")
+	}
+
+	var oid [12]byte
+	_, err := hex.Decode(oid[:], []byte(s))
+	if err != nil {
+		return nilObjectID, err
+	}
+
+	return oid, nil
+}
+
+func TestMappingCustomArrayUri(t *testing.T) {
+	var s struct {
+		FileData objectID `uri:"id"`
+	}
+	val := `664a062ac74a8ad104e0e80f`
+	err := mappingByPtr(&s, formSource{"id": {val}}, "uri")
+	assert.NoError(t, err)
+
+	expected, _ := convertTo(val)
+	assert.EqualValues(t, expected, s.FileData)
+}
+
+func TestMappingCustomArrayForm(t *testing.T) {
+	var s struct {
+		FileData objectID `form:"id"`
+	}
+	val := `664a062ac74a8ad104e0e80f`
+	err := mappingByPtr(&s, formSource{"id": {val}}, "form")
+	assert.NoError(t, err)
+
+	expected, _ := convertTo(val)
+	assert.EqualValues(t, expected, s.FileData)
+}