- Published on
Golang 使用过程中遇到的一些问题(1)
- Authors
- Name
- Alex
- @adams6688
前段时间使用 golang 做了一些项目,总结一下遇到的问题。
没有枚举
在需要用到枚举的地方,Go 语言通常是用一组常量来模拟,这会带来几个问题:
类型安全问题
在 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,这不会有任何错误,但是在逻辑上是不允许这样比较的,所以在使用常量进行比较的时候要格外小心,这回带来额外的心智负担。
要特别注意声明的顺序
type Color int const ( Red Color = iota Green Blue )
在Go语言中,使用iota来模拟枚举时,值是顺序递增的。iota是Go语言的一个特殊常量,它在每当定义一个新的常量时会自动递增。它通常在const块中使用,用于生成一系列相关的值。
缺乏自描述性
我们在 Go 语言里打印这些枚举值的时候,打印出的都是基本值,看不出枚举值包含的枚举信息,这也降低了代码的可读性于可维护性。
不支持枚举方法
与一些其他语言不同,在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
这个参数,在调用的过程中都必须携带这个参数。