如何编写golang验证功能,以便客户端能够优雅地处理?

type Request struct{
  A string
  B string
  C string
  D string
  //...
}

func validator(req *Request)error{

  if req.A == "" && req.B != ""{
    return errors.New("Error 1 !!")
  }

  //...


}

I have some existing code like above which is already being used so I can not change the function signature.

I am writing a caller function which has to throttle some types of errors. All existing errors are created using either errors.New("some string") or fmt.Errorf("some string"). What I can do is

if err.Error() == "Error 1 !!" {
    return nil
  }

But this is not ideal. If on the server side, the message changes, client side breaks.

I thought about naming all the errors on the server side like:

const ErrorType1 =  "Error 1 !!"

But it's difficult to name each error.

Any better solutions?

error is an interface, so you can dynamically check - using type assertions - some specific types and act accordingly.

Here's a code snippet that may be useful (playground link):

package main

import (
    "errors"
    "fmt"
)

type MyError struct {
    msg string
    id  int
}

func (me MyError) Error() string {
    return me.msg
}

func checkErr(e error) {
    if me, ok := e.(MyError); ok {
        fmt.Println("found MyError", me)
    } else {
        fmt.Println("found error", e)
    }
}

func main() {
    checkErr(errors.New("something bad"))
    checkErr(MyError{msg: "MyError bad"})
}

The first line in checkErr is the ticket here - it checks if e is of some special underlying type.

While the use of typed errors surely has its place and can be used, there are different approaches.

As for the validation part, I prefer not to reinvent the wheel and use [govalidator][gh:govalidator]. Custom validations can be easily added and if your validation needs are not rather complicated, it may already give you what you need.

However, as per the second part your question consists of, the elegant handling of errors, there is an alternative to an implementation of the Error interface: predefined variables which you can directly compare, as shown in the switch statement in the example program below.

package main

import (
    "errors"
    "log"

    "github.com/asaskevich/govalidator"
)

// Error Examples
var (
    NoUrlError        = errors.New("Not an URL")
    NotLowerCaseError = errors.New("Not all lowercase")
)

func init() {
    govalidator.SetFieldsRequiredByDefault(true)
}

// Request is your rather abstract domain model
type Request struct {
    A string `valid:"-"`
    B string `valid:"uppercase"`
    // Note the custom error message
    C string `valid:"url,lowercase~ALL lowercase!!!"`
    D string `valid:"length(3|10),lowercase"`
    E string `valid:"email,optional"`
}

// Validate takes the whole request and validates it against the struct tags.
func (r Request) Validate() (bool, error) {
    return govalidator.ValidateStruct(r)
}

// ValidC does a custom validation of field C.
func (r Request) ValidC() (bool, error) {

    if !govalidator.IsURL(r.C) {
        return false, NoUrlError
    }

    if !govalidator.IsLowerCase(r.C) {
        return false, NotLowerCaseError
    }

    return true, nil
}

func main() {

    // Setup some Requests
    r1 := Request{C: "asdf"}
    r2 := Request{C: "http://www.example.com"}
    r3 := Request{C: "http://WWW.EXAMPLE.com"}
    r4 := Request{B: "HELLO", C: "http://world.com", D: "foobarbaz", E: "you@example.com"}

    for i, r := range []Request{r1, r2, r3, r4} {
        log.Printf("=== Request %d ===", i+1)
        log.Printf("\tValidating struct:")

        // Validate the whole struct...
        if isValid, err := r.Validate(); !isValid {
            log.Printf("\tRequest %d is invalid:", i+1)

            // ... and iterate over the validation errors
            for k, v := range govalidator.ErrorsByField(err) {
                log.Printf("\t\tField %s: %s", k, v)
            }
        } else {
            log.Printf("\t\tValid!")
        }

        log.Println("\tValidating URL")

        valid, e := r.ValidC()

        if !valid {
            switch e {
            // Here you got your comparison against a predefined error
            case NoUrlError:
                log.Printf("\t\tRequest %d: No valid URL!", i)
            case NotLowerCaseError:
                log.Printf("\t\tRequest %d: URL must be all lowercase!", i)
            }
        } else {
            log.Printf("\t\tValid!")
        }
    }
}

Imho, a custom implementation only makes sense if you want to add behavior. But then, this would first call for a custom interface and an according implementation as a secondary necessity:

package main

import (
    "errors"
    "log"
)

type CustomReporter interface {
    error
    LogSomeCoolStuff()
}

type PrefixError struct {
    error
    Prefix string
}

func (p PrefixError) LogSomeCoolStuff() {
    log.Printf("I do cool stuff with a prefix: %s %s", p.Prefix, p.Error())
}

func report(r CustomReporter) {
    r.LogSomeCoolStuff()
}

func isCustomReporter(e error) {
    if _, ok := e.(CustomReporter); ok {
        log.Println("Error is a CustomReporter")
    }
}

func main() {
    p := PrefixError{error: errors.New("AN ERROR!"), Prefix: "Attention -"}
    report(p)
    isCustomReporter(p)

}

Run on playground

So, in short: If you want to make sure that the user can identify the kind of error use a variable, say yourpackage.ErrSomethingWentWrong. Only if you want to ensure a behavior implement a custom interface. Creating the type just for positively identifying a semantic value is not the way to do it. Again, imho.

I do it like:

  • normal cases:

request is well formated and server side handle it well.

status :200, body:{"message":"success"}

  • client bad request:

client sent a bad request , maybe lack of args.It should be fixed by your client mate, and avoid appearing when online.

status:400, body: {"message":"error reasson"}

  • client normal request but not success:

maybe users use api to get award times more than default value.The request is normal but should be limit.

status:200, body: {"message":"success", "tip":"Dear,you've finish it", "tip_id":1}

  • server inner error:

some bugs or unnavoided error happened on server side.

status:500 body: {"message":"error_stack_trace", "error_id":"XKZS-KAKZ-SKD-ZKAQ"}

Above all, client should divide response.status into three possible values(200,400,500) and has different handle ways.

On 200 case, show anythings client want or tip. On 400 case, show message. On 500 case, show error_id.