What is the best practice for error matching when using dependency injection?
If we have an interface like this used in a function:
type MyIF interface {
Send(reciever, msg string) error
}
function mySender(s MyIF) {
err := s.Send("me#myself.com", "hello")
if err != nil {
if err == ??? {
}
}
}
How can err be matched? In contrast to an import from a package, I can not import a defined error variable which can be used for comparison or check with .Is().
The solution linked by #Christian works for me (see blog.golang.org/error-handling-and-go). By adding an Error interface as in the net package in the blog post I can do what I want.
Thanks.
The code snippet would then look like this:
type MyIF interface {
Send(reciever, msg string) error
}
type MyError interface {
Error() string
ReceiverUnknown() bool
}
function mySender(s MyIF) {
err := s.Send("me#myself.com", "hello")
if err != nil {
if nerr, ok := err.(MyError); ok && nerr.ReceiverUnknown() {
// handle unknown Receiver
}
}
}
Related
In short - I would like to be able to cast an interface type whose underlying type implements a specific interface to that specific interface.
I am using the plugin package to lookup a New function which looks like so (I have many others the same):
func NewDomainPrimaryKey() any { return DomainPrimaryKey{} }
(This is generated at run-time so I can't just reference it as DomainPrimaryKey)
My lookup and call is like so:
plugin, err := plugin.Open("my-plugin")
if err != nil {
return err
}
symGet, err := plugin.Lookup("New" + pluginName)
if err != nil {
return err
}
newGenModel, ok := symGet.(func() any)
if !ok {
return errors.New("unexpected type from module symbol")
}
anyGenModel := newGenModel()
genModel, ok := anyGenModel.(GenModel) // **this is where the problem is
if !ok {
return errors.New("unexpected type from module symbol")
}
genModelInstance := genModel.Get()
In the above I am trying to cast 'anyGenModel' (an interface) to the 'GenModel' interface which it implements, however, this doesn't work.
I am certain it implements this interface because when I do the following, I get no errors.
type GenModel interface {
Get() any
TableName() string
}
var _ GenModel = (*DomainPrimaryKey)(nil) // this doesn't complain
How can I do this? I found this article which I don't think is what I am looking for but seems similar.
Thanks in advance for any help on this - this has become a real blocker for me.
If the underlying type implements both interfaces, this is very straightforward:
package main
import "fmt"
type IFace1 interface {
DoThis()
}
type IFace2 interface {
DoThat()
}
type impl struct{}
func (i *impl) DoThis() {
fmt.Println("I implement IFace1")
}
func (i *impl) DoThat() {
fmt.Println("I implement IFace2")
}
func GetIFace1() IFace1 {
return &impl{}
}
func main() {
i1 := GetIFace1()
i1.DoThis()
i2 := i1.(IFace2)
i2.DoThat()
}
playground
If your code is not working, then I would begin by questioning your assertion that the underlying type of anyGenModel actually implements GenModel and work from there.
I am trying to do dependency injection in golang with applying dependency inversion principle, so I have the following service
package account
import (
types "zaClouds/modules/account/domain/types"
"zaClouds/modules/shared"
)
type IPlanDomainService interface {
GetUsagePlanById(string) *shared.Result[types.UsagePlan]
}
type PlanDomainService struct {
usagePlanService types.IUsagePlanService
}
func (planDomainService *PlanDomainService) GetUsagePlanById(id string) *shared.Result[types.UsagePlan] {
result := &shared.Result[types.UsagePlan]{}
usagePlanResult := planDomainService.usagePlanService.GetPlanById(id)
if usagePlanResult.Err != nil {
result.Err = usagePlanResult.Err
return result
}
result.Data = usagePlanResult.Data
return result
}
func PlanDomainServiceFactory(usagePlanService types.IUsagePlanService) IPlanDomainService {
return &PlanDomainService{usagePlanService: usagePlanService}
}
as you can see, it accepts another service with type IUsagePlanService
and here is the interface for it
package account
import (
"zaClouds/modules/shared"
"github.com/shopspring/decimal"
)
type UsagePlan struct {
ID string
Title string
Description interface{}
PlanID string
Price decimal.Decimal
Duration int
Features map[string]map[string]string
}
type IUsagePlanService interface {
GetPlanById(string) *shared.Result[UsagePlan]
}
and here is the way I am injecting this service to domain service
func DiInit(usagePlanService interface{}) domainServices.IPlanDomainService {
domainServices.PlanDomainServiceFactory(types.IUsagePlanService(usagePlanService))
return domainServices.PlanDomainServiceFactory(usagePlanService.(types.IUsagePlanService))
}
as you can see, I am trying to do a type assertion but it doesn't work, and gives me the following error:
panic: interface conversion: *usagePlan.UsagePlanRepository is not account.IUsagePlanService: missing method GetPlanById
Edit
Here is the actual implementation for usagePlanService
type IUsagePlanRepository interface {
createClient(string) *http.Request
GetPlanById(string) *shared.Result[usagePlanRepoModels.UsagePlan]
}
type UsagePlanRepository struct {
plansEndpoint string
httpClient *http.Client
}
func (r *UsagePlanRepository) GetPlanById(id string) *shared.Result[usagePlanRepoModels.UsagePlan] {
result := &shared.Result[usagePlanRepoModels.UsagePlan]{}
req := r.createClient(id)
resp, err := r.httpClient.Do(req)
if err != nil {
log.Println("failed to load plan details \n[ERROR]", err)
result.Err = err
return result
}
defer func() {
bodyError := resp.Body.Close()
if bodyError != nil {
result.Err = bodyError
}
}()
if result.Err != nil {
return result
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
utils.Logger.Error("failed to load plan details \n[ERROR]", err, nil)
result.Err = err
return result
}
if resp.StatusCode >= 400 {
result.Err = errors.New(string(body))
utils.Logger.Info("getPlanById", string(body))
}
getUsagePlanResponse, foundError := usagePlanRepoModels.CreateGetUsagePlanResponse(body)
if foundError != nil {
result.Err = foundError
return result
}
result.Data = *getUsagePlanResponse
return result
}
When using an interface, you need to define all functions that you will use with the same name and signature as the implementation.
The error message you got indicates that the implementation and the interface are different.
The implementation is not shown in your question, but you defined the function for your interface like this: GetPlanById(string) *shared.Result[UsagePlan]. Any deviation from it will result in error. One common mistake is with the pointers. Adding or removing the * to the return type will incur in error if it differs from the original.
Edit:
Your interface should look like this:
type IUsagePlanService interface {
GetPlanById(id string) *shared.Result[usagePlanRepoModels.UsagePlan]
}
If your function is returning a private type, and you can change that, you should. If you cant change it, than you should create a function that wraps the function you are trying to abstract with the interface.
I have a function with this signature in Go:
func GetAccount(ctx context.Context, id uuid.UUID) (*Account, error)
It returns an error if there's an internal error (like the database query fails for some reason), but I'm not sure what I should return if the account is not found. I can think of two different approaches:
Just return a nil account and nil error if no account is found
Return a custom error type like this:
type accountNotFoundErr struct {
id uuid.UUID
}
func (err accountNotFoundErr) Error() string {
return fmt.Sprintf("account not found for user: %v", err.id)
}
func IsAccountNotFoundErr(err error) bool {
_, ok := err.(accountNotFoundErr)
return ok
}
func GetAccount(ctx context.Context, id uuid.UUID) (*Account, error) {
// if the account is not found
return nil, accountNotFoundErr{id}
}
I like the first one because it's simple, but I don't often see Go code which returns a nil result if the error is non-nil. I think the expectation is that, if the error is nil, the result is valid. The second approach fixes that, but it's also a bit more complicated for callers.
What is an idiomatic approach for handling cases like this in Go?
I have read a lot of posts about custom errors in go. Most of them created their own struct that implements the error interface.
The issue I found with that approach was that I did not manage to easily check if an error was of a certain type. The same way, you may be able to check some std lib error like if error == EOF.
Therefore, my favourite way to do that is creating a simple var with erros.New.
var ErrNotFound = errors.New("Resource was not found")
func main() {
err := raise()
if err == ErrNotFound {
fmt.Println("impossibru")
return
}
if err != nil {
fmt.Println("unexpected error")
return
}
}
func raise() error {
return ErrNotFound
}
https://play.golang.com/p/s0ZQfsdLqxB
As #Gavin pointed out in the comments, if you want to provide more context to the error by wrapping it with fmt.Errorf, you need to use errors.Is to check if the specific error was wrapped.
var ErrNotFound = errors.New("Resource was not found")
func main() {
err := raise(42)
if errors.Is(err, ErrNotFound) {
fmt.Println(err)
return
}
if err != nil {
fmt.Println("unexpected error")
return
}
}
func raise(id int) error {
return fmt.Errorf("id %d does not exist, error: %w", id, ErrNotFound)
}
https://play.golang.com/p/hSrkb1Xp4Hn
I pasted a section of code that was supposed to catch an AllTopologyNodesDownError error which doesn't work and I have no idea why.
func (sc *ServerConfig) addNodesToCluster(store *ravendb.DocumentStore) error {
clusterTopology, err := sc.getClusterTopology(store)
if errors.Is(err, &ravendb.AllTopologyNodesDownError{}) {
for _, url := range sc.Url.List {
err = addNodeToCluster(store, url)
if err != nil {
return err
}
}
} else if err != nil {
return err
}
the structure of the ravendb.AllTopologyNodesDownError is
// AllTopologyNodesDownError represents "all topology nodes are down" error
type AllTopologyNodesDownError struct {
errorBase
}
type errorBase struct {
wrapped error
ErrorStr string
}
screen shot of the error when debugging the code
errors.Is() is used to tell if any error in the chain is the same instance as the provided error1, that can never be the case here because you provided a literal of your error type, no other code could hold that instance or a reference to it.
Your error looks like a type, to tell if any error in the chain is a given type you should use errors.As():
clusterTopology, err := sc.getClusterTopology(store)
var errAllDown *AllTopologyNodesDownError
if errors.As(err, &errAllDown) {
// err had an *AllTopologyNodesDownError in its
// chain and errAllDown now contains it.
}
Can be overridden by implementing the Unwrap() interface which your error type does not.
In Go's standard libraries, the built-in error is used for error handling. This post used some examples from the std libs to demonstrate how error handling works in Go.
package net
type Error interface {
error
Timeout() bool // Is the error a timeout?
Temporary() bool // Is the error temporary?
}
if nerr, ok := err.(net.Error); ok && nerr.Temporary() {
time.Sleep(1e9)
continue
}
if err != nil {
log.Fatal(err)
}
I am wondering what would be the benefits of sticking to this convention.
What if we return the custom type? e.g. net.Error type.
I assume that you are talking about the difference between DoRequest() error vs DoRequest() net.SomeSpecificError.
Following code
type CustomError struct {
}
func (CustomError) Error() string {
return "custom error"
}
func DoRequest() *CustomError {
return nil
}
func MarkTaskComplete() error {
return nil
}
func main() {
err := DoRequest()
if err != nil {
log.Fatal(err)
}
err = MarkTaskComplete()
if err != nil {
log.Fatal(err)
}
}
will fail to compile because error returned by MarkTaskComplete cannot be assigned to err variable which is of type *SomeError.
Second disadvantage is that you cannot return from DoRequest any other error than SomeError. There may be a few situations where you would benefit from such a restriction, but in general in most cases the approach with error is more flexible. Especially when your function calls another one.
It is also worth to read about the errors wrapping
The benefits of the customer type, ie net.Error is:
type Error interface {
error
Timeout() bool // Is the error a timeout?
Temporary() bool // Is the error temporary?
}
The above has the advantage of being more descriptive. We get more information about the error like whether it is a Timeout error or a Temporary error.
In addition to that, we also get to know the primary data which is in the primary field "error", in the above example, which is a string.
type error interface {
Error() string
}