Wrapper
Wrapper

Reputation: 932

Validate struct in Golang using goroutines

I have simple struct which fields I want to validate. This will later be a somewhat deeply nested struct, for now it is simple.

type SpotRequest struct { //holder struct
    Location Location
}
type Location struct { // target struc, this is going to be validated
    Longitude float64 
    Latitude  float64
}
type Error struct { //return to caller of a service
    Field  string
    Reason string
}

This is a async function (in the lack of better word):

func validateLocation(location *Location, ch chan []Error) {

    var errors []Error
    //some custom logic which will return list of errors
    errr := &Error{
        Field:  "myField",
        Reason: "value must start with upper case",
    }
    ch <- append(s, *errr)
}

Main code:

ch := make(chan []Error, 1) //I have set for the second argument value of 1 since I will be returning single list with zero or more Error structs
go validateLocation(&spotRequest.Location, ch)
for i := 0; i < 1; i++ { //iterating one time since I need to get one value from channel ch
    select {
    case msg1 := <-ch:
        fmt.Printf("Result from location validations: %v \n", msg1)
    default:
        fmt.Println("Should not print")
    }
}

The flow only ends up in default clause. Where I'm wrong? First time with gorountins.

Upvotes: 2

Views: 350

Answers (1)

Brits
Brits

Reputation: 18370

Lets start with a few quotes from the language spec, firstly go:

A "go" statement starts the execution of a function call as an independent concurrent thread of control, or goroutine, within the same address space.

And secondly the default case in select:

If one or more of the communications can proceed, a single one that can proceed is chosen via a uniform pseudo-random selection. Otherwise, if there is a default case, that case is chosen.

Apply this to your program, firstly go validateLocation(&spotRequest.Location, ch) is executed. This "starts the execution" of the function; the word "starts" is important, the goroutine is independent so when the for loop starts it's possible that:

  • No code within the goroutine function has been run.
  • Some code within the goroutine function has been run (and it may be running simultaneously on another core).
  • All code within the goroutine function has been run.

You should not make any assumptions about which of those three options is true. Goroutines are independent; you cannot know their status without some form of synchronization (e.g. channel, waitgroup, mutex etc).

With your particular code (with this version of the Go compiler and in your specific environment) the goroutine has not run to the point where it executes ch <- append(s, *errr) before main function hits the select. So looking at that:

select {
case msg1 := <-ch:
    fmt.Printf("Result from location validations: %v \n", msg1)
default:
    fmt.Println("Should not print")
}

As the goroutine has not yet run ch <- append(s, *errr) there is nothing in ch so that case is "not ready". This means that, as per the extract of the spec above, the default case is chosen. Adding time.Sleep(time.Nanosecond) before the select is likely (no guarantees!) to allow time for the goroutine to complete (meaning case msg1 := <-ch: will be selected).

The quick way to "fix" this (if you know that there will always be one, and only one thing sent on the channel) is to remove the default clause. This will cause the select to wait for something to be available on the channel.

However this is probably now what you want because I would guess you need to cope with:

  • No errors (nothing sent on channel)
  • One Error
  • More than one error (if this happens should other routines be stopped when the first error occurs?)

So your current algorithm is probably insufficient (you don't really set out your requirements so this is a guess).

At this point it's worth mentioning that you are probably engaged in premature optimisation. Unless you know that your validation routines will be a bottleneck (e.g. they perform slow database queries) I would recommand writing the simplest algorithm possible (no goroutines!). If it turns out that this is slow you can then optimise it (and you have a baseline to work from - don't assume that adding goroutines will speed things up!). Adding goroutines can make your code a lot more complex, you need to consider syncronization and things like race conditions.

Having said that, if running validation in parallel is needed then maybe consider something like errgroup. The "JustErrors" example in the documentation seems pretty close to your requirements. Here is a modified version of your code that uses this (without more validation routines it's not that useful!).

g := new(errgroup.Group)
g.Go(func() error { return validateLocation(&Location{}) })

if err := g.Wait(); err != nil { // Waits for either an error or completion of all go routines
    panic(fmt.Sprintf("Error: %s", err))
}
fmt.Println("successful")

Upvotes: 2

Related Questions