Published on

Golang 使用过程中遇到的一些问题(1)

Authors

前段时间使用 golang 做了一些项目,总结一下遇到的问题。

没有枚举

在需要用到枚举的地方,Go 语言通常是用一组常量来模拟,这会带来几个问题:

  1. 类型安全问题

    在 Go 中,模拟实际上是基本类型 int 的别名,这意味着来自不同枚举集的值可以混用,因为它们本质上是相同的底层类型。请看下面的代码片段:

    const (
        UserStatusAvailable = 1
        UserStatusSuspended = 9
        UserStatusDeleted   = 10
    )
    
    const (
        EmailStatusAvailable    = 1
        EmailStatusToBeVerified = 2
    )
    
    const (
        LocalUserSource  = 0
        RemoteUserSource = 10
    )
    
    if exist && userInfo.Source == entity.UserStatusDeleted {
            fmt.Println("remote user already exist: ", userInfo)
            resp, err := us.buildLoginResp(ctx, userInfo, token.AccessToken)
            return c.RemoteAuthEnable, resp, err
        }
    

    本来需要比较的是用户的 Source,结果真正比较的是用户的 Status,这不会有任何错误,但是在逻辑上是不允许这样比较的,所以在使用常量进行比较的时候要格外小心,这回带来额外的心智负担。

  2. 要特别注意声明的顺序

    type Color int
    const (
        Red Color = iota
        Green
        Blue
    )
    

    在Go语言中,使用iota来模拟枚举时,值是顺序递增的。iota是Go语言的一个特殊常量,它在每当定义一个新的常量时会自动递增。它通常在const块中使用,用于生成一系列相关的值。

  3. 缺乏自描述性

    我们在 Go 语言里打印这些枚举值的时候,打印出的都是基本值,看不出枚举值包含的枚举信息,这也降低了代码的可读性于可维护性。

  4. 不支持枚举方法

    与一些其他语言不同,在Go中不能直接为枚举类型定义方法,因为它们实际上是基础类型的别名。

错误处理写到吐

一个新手学习 Go 语言,最先接触到的也许就是错误处理了,可以随手贴出一段代码来看看:

func GetUserInformationByBearer(token string) (*schema.UserAccountInfoResp, error) {
    c, err := readConfig(cli.GetConfigFilePath())
    if err != nil {
        return nil, err
    }
    remoteEndpoint := c.RemoteAuth + "/api/user/profile-direct"
    req, err := http.NewRequest("GET", remoteEndpoint, nil)
    if err != nil {
        return nil, err
    }

    req.Header.Set("Authorization", "Bearer "+token)
    client := &http.Client{}
    resp, err := client.Do(req)
    //fmt.Println("resp", resp, err)
    if err != nil {
        return nil, err
    }
    defer resp.Body.Close()

    if resp.StatusCode != http.StatusOK {
        return nil, fmt.Errorf("received non-200 response status code: %d", resp.StatusCode)
    }

    body, err := io.ReadAll(resp.Body)
    if err != nil {
        return nil, err
    }

    var userEntity schema.UserAccountInfoResp
    err = json.Unmarshal(body, &userEntity)
    fmt.Println("userEntity", userEntity)
    if err != nil {
        return nil, err
    }

    return &userEntity, nil
}

目测下来有接近一半的代码都是 if err != nil 这种代码,特别是在一些网络编程领域,可能也三分之二的代码都是处理错误。

因为 Go 声称不支持异常,所以可能有错误的函数都要在结尾返回一个 error 作为结果,这种冗长的方式在 Go 语言的代码中极为普遍,这就造成在实际的编码过程中引入很多噪音,很难找到实际起作用的代码。错误处理代码经常与业务逻辑代码混在一起,这可能导致业务逻辑的阅读和理解变得更加困难。

Go语言没有提供类似于异常处理机制的统一错误处理方式,这可能导致在处理多个不同来源的错误时代码变得冗杂和重复。

在某些情况下,Go中的函数可能会返回过多的错误,甚至是在不需要错误处理的情况下也返回错误。这种设计可能导致错误处理的滥用和代码的臃肿。

Go 的错误处理在实际语义上也存在一些问题,主要是错误信息的丢失。

在错误被向上层传递的过程中,原始错误信息可能会丢失或被覆盖,特别是在不正确地封装错误时。这会导致调试和定位问题变得更加困难。

return fmt.Errorf("failed to process: %v", err)

在复杂的应用中,错误可能需要通过多个层级传递,每一层都可能增加额外的封装或日志记录。这可能导致复杂的错误链,使得最终的错误难以理解和处理。

context 传递的问题

在使用 Gin 进行 Web 开发的时候,常常将 context(如*gin.Context)从控制器(controller)一直传递到存储库(repository),这种做法非常普遍。

这会引入一些问题,框架本身对业务代码的侵入性过大,这绝不是只有 Gin 才存在的问题。

这就导致在整个调用链上,每个方法默认第一个参数就是 *gin.Context,导致 controller 和 repo 的过度耦合,也降低代码的可读性,给单元测试带来额外的复杂性。当context被深入传递时,对那些依赖于context的函数或方法进行单元测试可能变得更加复杂。你可能需要模拟更多的context内容,这可能导致测试代码的冗余。

请看下面的代码:

func (uc *UserController) UserLogout(ctx *gin.Context) {
    accessToken := middleware.ExtractToken(ctx)
    if len(accessToken) == 0 {
        handler.HandleResponse(ctx, nil, nil)
        return
    }
    _ = uc.authService.RemoveUserCacheInfo(ctx, accessToken)
    _ = uc.authService.RemoveAdminUserCacheInfo(ctx, accessToken)
    uc.userService.UserRemoteLogout(ctx, accessToken)
    uc.userService.ClearAccessCookieToken(ctx)
    handler.HandleResponse(ctx, nil, nil)
}

func (as *AuthService) RemoveUserCacheInfo(ctx context.Context, accessToken string) (err error) {
    return as.authRepo.RemoveUserCacheInfo(ctx, accessToken)
}

func (ar *authRepo) RemoveUserCacheInfo(ctx context.Context, accessToken string) (err error) {
    err = ar.data.Cache.Del(ctx, constant.UserTokenCacheKey+accessToken)
    if err != nil {
        return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
    }
    return nil
}

在上面的代码中,不管函数本身是不是需要 context 这个参数,在调用的过程中都必须携带这个参数。