How can I use golang generics in 1.18 to simplify the code snippet?

Issue

I’ve been using 2 SDKs that were generated by OpenAPI generator in a separate packages that share an identical code that is duplicated in different packages foo, bar:

package foo
// there's the exact same piece of code under another package bar

// GenericOpenAPIError Provides access to the body, error and model on returned errors.
type GenericOpenAPIError struct {
    ...
    model interface{}
}

// Model returns the unpacked model of the error
func (e GenericOpenAPIError) Model() interface{} {
    return e.model
}

// Failure Provides information about problems encountered while performing an operation.
type Failure struct {
    // List of errors which caused this operation to fail
    Errors []Error `json:"errors"`
}

// GetErrors returns the Errors field value
func (o *Failure) GetErrors() []Error {...}

// Error Describes a particular error encountered while performing an operation.
type Error struct {
    ...
    // A human-readable explanation specific to this occurrence of the problem.
    Detail *string `json:"detail,omitempty"`
    ...
}

Separately, there’s an app where I use both SDKs: foo, bar and extract the first error from a list of errors where I try to cast an error to each of SDK’s error types and have to duplicate the code because of it. Is there a way to simplify it using 1.18 that supports generics?

import (
    foo "..."
    bar "..."
)

func getErrorMessage(err error) (string) {
    result := err.Error()

    if fooError, ok1 := err.(foo.GenericOpenAPIError); ok1 {
        if fooFailure, ok2 := fooError.Model().(foo.Failure); ok1 {
            fooFailureErrors := fooFailure.GetErrors()
            // it's guaranteed to be non-empty and .Detail != nil
            result = *fooFailureErrors[0].Detail
        }
    }

    if barError, ok1 := err.(bar.GenericOpenAPIError); ok2 {
        if barFailure, ok2 := barError.Model().(bar.Failure); ok2 {
            barFailureErrors := barFailure.GetErrors()
            // it's guaranteed to be non-empty and .Detail != nil
            result = *barFailureErrors[0].Detail
        }
    }

    return result
}

To start, I was thinking I could redeclare these common types as interfaces in my app’s code like:

type GenericOpenAPIError interface {
    Model() interface{}
}
...
type Failure interface {
    GetErrors() []Error
}
type DetailedError interface {
    GetDetail() string
}

and then cast directly to these interfaces, is that a reasonable approach?

if mainError, ok1 := err.(GenericOpenAPIError); ok1 {
    var mainErrorModel = mainError.Model()
    if failure2, ok2 := mainErrorModel.(Failure); ok2 {
        var ff = failure2.GetErrors()
        if len(ff) > 0 {
            result := ff[0].GetDetail()
        }
    }
}

When I try to test it, it seems like

if failure2, ok2 := mainErrorModel.(Failure); ok2 {

this casting is not successful. How can I debug it? The problem seems to be related to the face that GetErrors is a method on pointer:

func (o *Failure) GetErrors() []Error {...}

and I don’t use pointers here:

if failure2, ok2 := mainErrorModel.(Failure); ok2 {

This question seems to be related.

Solution

You can change mainErrorModel to struct with no pointer with type assertion, then change that struct to struct with pointer, and finaly you can change again to type Failure interface with type assertion.

this is the example.

// You can edit this code!
// Click here and start typing.
package main

import (
    "fmt"
)

type GenericOpenAPIError interface {
    Model() any
}

type Failure interface {
    GetErrors() string
}

type GenericOpenAPI struct {
    model any
}

func (g *GenericOpenAPI) Model() any {
    return g.model
}

func (g *GenericOpenAPI) Error() string {
    return "goae error"
}

type A struct {
    b string
}

func (a *A) GetErrors() string {
    return a.b
}

type B struct {
    c string
}

func (b *B) GetErrors() string {
    return b.c
}

type GetErrores interface {
    A | B
}

func getErrorMessage[T GetErrores](err error) string {
    if mainError, ok1 := err.(GenericOpenAPIError); ok1 {
        var mainErrorModel = mainError.Model()
        if t, ok := mainErrorModel.(T); ok {
            if f, ok := any(&t).(Failure); ok { // must change &t to any then type assert to Failure to tell app that there is a method GetErrors()
                return f.GetErrors()
            }
        }
    }
    return err.Error()
}

func main() {
    var err any

    err = &GenericOpenAPI{A{b: "this is error a"}}

    e, ok := err.(error)
    if !ok {
        panic("not ok!")
    }

    fmt.Println(getErrorMessage[A](e))

    // change model to B
    err = &GenericOpenAPI{B{c: "this is error b"}}

    e, ok = err.(error)
    if !ok {
        panic("not ok!")
    }

    fmt.Println(getErrorMessage[B](e))
}

Answered By – Rahmat Fathoni

Answer Checked By – Candace Johnson (GoLangFix Volunteer)

Leave a Reply

Your email address will not be published.