使用goroutines在Golang中验证结构体

huangapple go评论104阅读模式
英文:

Validate struct in Golang using goroutines

问题

我有一个简单的结构体,我想要验证其中的字段。这将来会是一个相对复杂的嵌套结构体,但现在很简单。

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
}

这是一个异步函数(暂时这么称呼):

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

    var errors []Error
    //一些自定义逻辑,将返回错误列表
    errr := &Error{
        Field:  "myField",
        Reason: "value must start with upper case",
    }
    ch <- append(errors, *errr)
}

主要代码:

ch := make(chan []Error, 1) //我将第二个参数的值设置为1,因为我将返回一个包含零个或多个Error结构体的列表
go validateLocation(&spotRequest.Location, ch)
for i := 0; i < 1; i++ { //只迭代一次,因为我需要从通道ch中获取一个值
    select {
    case msg1 := <-ch:
        fmt.Printf("位置验证结果:%v \n", msg1)
    default:
        fmt.Println("不应该打印")
    }
}

流程只会进入默认分支。我错在哪里?第一次使用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 := &amp;Error{
		Field:  &quot;myField&quot;,
		Reason: &quot;value must start with upper case&quot;,
	}
	ch &lt;- 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(&amp;spotRequest.Location, ch)
for i := 0; i &lt; 1; i++ { //iterating one time since I need to get one value from channel ch
	select {
	case msg1 := &lt;-ch:
		fmt.Printf(&quot;Result from location validations: %v \n&quot;, msg1)
	default:
		fmt.Println(&quot;Should not print&quot;)
	}
}

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

答案1

得分: 2

让我们从语言规范中的一些引用开始,首先是go语句:

"go"语句在同一地址空间内启动一个独立的并发控制线程,或者称为goroutine,来执行函数调用。

其次是select语句中的default case

如果有一个或多个通信可以进行,那么通过均匀的伪随机选择来选择一个可以进行的通信。否则,如果有一个默认情况,就选择该情况。

将这个应用到你的程序中,首先执行go validateLocation(&spotRequest.Location, ch)。这个"启动执行"函数的执行;"启动"这个词很重要,goroutine是独立的,所以当for循环开始时,可能会出现以下情况:

  • 没有运行goroutine函数中的任何代码。
  • 在goroutine函数中运行了一些代码(并且可能同时在另一个核心上运行)。
  • 在goroutine函数中运行了所有代码。

你不能做出关于这三个选项中哪个是真实的任何假设。Goroutines是独立的;如果没有某种形式的同步(例如通道、等待组、互斥锁等),你无法知道它们的状态。

根据你的特定代码(使用这个版本的Go编译器和你特定的环境),goroutine还没有运行到执行ch <- append(s, *errr)之前,主函数就已经到达了select语句。所以看看这个:

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

由于goroutine还没有运行到ch <- append(s, *errr),所以ch中没有任何内容,因此该case是"not ready"的。这意味着,根据上面规范的摘录,选择了默认情况。在select之前添加time.Sleep(time.Nanosecond)可能(不能保证!)会给goroutine完成的时间(这意味着case msg1 := <-ch:将被选中)。

"修复"这个问题的快速方法(如果你知道通道上总会有一个且只有一个东西发送)是删除default子句。这将导致select等待通道上有可用的内容。

然而,这可能不是你想要的,因为我猜测你需要处理以下情况:

  • 没有错误(通道上没有发送任何内容)
  • 一个错误
  • 多个错误(如果发生这种情况,当第一个错误发生时,其他例程应该停止吗?)

所以你当前的算法可能不够用(你没有明确说明你的要求,所以这只是一个猜测)。

此时值得一提的是,你可能正在进行过早的优化。除非你知道你的验证例程将成为瓶颈(例如它们执行缓慢的数据库查询),否则我建议编写尽可能简单的算法(没有goroutines!)。如果发现这很慢,那么你可以进行优化(并且你有一个基准线-不要假设添加goroutines会加速!)。添加goroutines可能会使你的代码变得更加复杂,你需要考虑同步和诸如竞态条件之类的问题。

话虽如此,如果需要并行运行验证,也许考虑使用errgroup之类的东西。文档中的"JustErrors"示例似乎非常接近你的要求。这里是一个修改过的使用它的代码版本(没有更多的验证例程,它没有太大用处!)。

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

if err := g.Wait(); err != nil { // 等待错误或所有goroutine完成
	panic(fmt.Sprintf("Error: %s", err))
}
fmt.Println("successful")
英文:

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(&amp;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 &lt;- append(s, *errr) before main function hits the select. So looking at that:

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

As the goroutine has not yet run ch &lt;- 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 := &lt;-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(&amp;Location{}) })

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

huangapple
  • 本文由 发表于 2023年6月19日 04:42:14
  • 转载请务必保留本文链接:https://go.coder-hub.com/76502465.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定