Go 使用WithBlock()选项创建到异步启动的gRPC服务器块的gRPC客户端连接?

Go 使用WithBlock()选项创建到异步启动的gRPC服务器块的gRPC客户端连接?,go,grpc,grpc-go,Go,Grpc,Grpc Go,我想编写一个单元测试,其中运行一个临时的gRPC服务器,该服务器在测试中的一个单独的Goroutine中启动,并在测试运行后停止。为此,我尝试将本教程()中的“Hello,world”示例改编为这样一个示例:使用单独的main.gos来代替服务器和客户端,使用一个单独的测试函数异步启动服务器,然后使用grpc.WithBlock()选项建立客户端连接 我已经将简化的示例放在这个存储库中,这里是main\u test.go: 主程序包 进口( “上下文” “fmt” “日志” “净额” “测试”

我想编写一个单元测试,其中运行一个临时的gRPC服务器,该服务器在测试中的一个单独的Goroutine中启动,并在测试运行后停止。为此,我尝试将本教程()中的“Hello,world”示例改编为这样一个示例:使用单独的
main.go
s来代替服务器和客户端,使用一个单独的测试函数异步启动服务器,然后使用
grpc.WithBlock()
选项建立客户端连接

我已经将简化的示例放在这个存储库中,这里是
main\u test.go

主程序包
进口(
“上下文”
“fmt”
“日志”
“净额”
“测试”
“时间”
“github.com/stretchr/authentic/require”
“google.golang.org/grpc”
“google.golang.org/grpc/examples/helloworld/helloworld”
)
常数(
端口=“:50051”
)
类型服务器结构{
helloworld.UnimplementdGreeterServer
}
func(s*server)SayHello(ctx context.context,在*helloworld.HelloRequest中)(*helloworld.HelloReply,错误){
log.Printf(“已接收:%v”,在.GetName()中)
return&helloworld.HelloReply{Message:“Hello”+in.GetName()},无
}
func TestHelloWorld(t*testing.t){
lis,err:=net.Listen(“tcp”,端口)
require.NoError(t,err)
s:=grpc.NewServer()
helloworld.RegisterGreeterServer,&server{}
去s.Serve(lis)
推迟s.停止
log.Println(“拨号gRPC服务器…”)
conn,err:=grpc.Dial(fmt.Sprintf(“本地主机:%s”,端口),grpc.withunsecure(),grpc.WithBlock())
require.NoError(t,err)
延迟连接关闭()
c:=helloworld.NewGreeterClient(康涅狄格州)
ctx,cancel:=context.WithTimeout(context.Background(),time.Second)
推迟取消
log.Println(“提出gRPC请求…”)
r、 err:=c.SayHello(ctx,&helloworld.HelloRequest{Name:“johndoe”})
require.NoError(t,err)
log.Printf(“问候语:%s”,r.GetMessage())
}
问题是,当我运行此测试时,它超时:

> go test -timeout 10s ./... -v
=== RUN   TestHelloWorld
2020/06/30 11:17:45 Dialing gRPC server...
panic: test timed out after 10s

我很难理解为什么没有建立连接?在我看来,服务器已正确启动…

您在此处发布的代码似乎有输入错误:

fmt.Sprintf(“本地主机:%s”,端口)

如果我在没有
grpc.WithBlock()
选项的情况下运行测试函数,
c.SayHello
会给出以下错误:

rpc error: code = Unavailable desc = connection error: desc = "transport: Error while dialing dial tcp: address localhost::50051: too many colons in address"
罪魁祸首似乎是
localhost::50051

const
声明(或者从
fmt.Sprintf(“localhost:%s,port”)
中删除额外的冒号后,测试通过

const (
    port = "50051" // without the colon
)
输出:

2020/06/30 23:59:01 Dialing gRPC server...
2020/06/30 23:59:01 Making gRPC request...
2020/06/30 23:59:01 Received: John Doe
2020/06/30 23:59:01 Greeting: Hello John Doe
但是,从
grpc.WithBlock()的文档中

否则,拨号会立即返回,并在后台连接服务器

因此,使用此选项,任何连接错误都应立即从
grpc返回。拨号
呼叫:

conn, err := grpc.Dial("bad connection string", grpc.WithBlock()) // can't connect
if err != nil {
    panic(err) // should panic, right?
}
那么,为什么您的代码挂起

通过查看
grpc
包的源代码(我根据
v1.30.0
构建了测试):

因此,
s
此时确实处于
TransientFailure
状态,但是
FailOnNonTempDialError
选项默认为
false
,并且当上下文过期时,
WaitForStateChange
为false,这不会发生,因为
Dial
在后台上下文中运行:

// Dial creates a client connection to the given target.
func Dial(target string, opts ...DialOption) (*ClientConn, error) {
    return DialContext(context.Background(), target, opts...)
}
现在我不知道这是否是预期的行为,因为从
v1.30.0
开始的一些API被标记为实验性的

无论如何,为了确保您在
拨号上捕捉到此类错误,您还可以将代码重写为:

    conn, err := grpc.Dial(
        "localhost:50051", 
        grpc.WithInsecure(),
        grpc.FailOnNonTempDialError(true),
        grpc.WithBlock(), 
    )

如果连接字符串不正确,则会立即失败并显示相应的错误消息。

检查
s.service(lis)
是否返回错误。可能该端口正在从另一个终端进程使用?如果该端口正在使用,
net.Listen
将出错-但可能还有其他一些引导错误,
err:=s.Serve(lis)
可能会显示出来。我已经检查了
s.Serve(lis)
没有返回错误(通过像调用
go require.NoError(t,s.Serve(lis))那样调用它)
)。好的,这可能不是问题,但仅供参考:千万不要运行这样的goroutine,即
go f1(f2())
-因为参数是在创建go例程之前计算的。因此,如果
f2()
阻塞,您的主goroutine将被阻塞。所以改为:
go func(){err:=f1(f2())}()
    conn, err := grpc.Dial(
        "localhost:50051", 
        grpc.WithInsecure(),
        grpc.FailOnNonTempDialError(true),
        grpc.WithBlock(), 
    )