Go 100 常见错误 2: 不必要的代码嵌套

274 阅读3分钟

一个应用于软件的心理模型是系统行为的内部表示。在编程时,我们需要维护心理模型(例如,关于整体代码交互和函数实现)。根据命名、一致性、格式化等多重标准,代码被认为具有可读性。可读性高的代码需要较少的认知努力来维持心理模型;因此,它更容易阅读和维护。

可读性的关键方面之一是嵌套层次的数量。我们来做个练习。假设我们正在开展一个新项目,需要理解以下 join 函数的作用:

func join(s1, s2 string, max int) (string, error) {
  if s1 == "" {
    return "", errors.New("s1 is empty")
  } else {
    if s2 == "" {
      return "", errors.New("s2 is empty")
    } else {
      concat, err := concatenate(s1, s2) // 调用一个连接函数来执行特定的连接操作,但可能会返回错误
      if err != nil {
        return "", err
      } else {
        if len(concat) > max {
          return concat[:max], nil
        } else {
          return concat, nil
        }
      }
    }
  }
}
func concatenate(s1 string, s2 string) (string, error) {
    // ...
}

这个 join 函数将两个字符串连接起来,如果长度大于 max,则返回一个子字符串。同时,它还处理对 s1s2 的检查,以及调用连接时是否返回了错误。

从实现的角度来看,这个函数是正确的。然而,构建一个包含所有不同情况的心理模型可能并不是一项简单的任务。为什么?因为嵌套层次的数量。

现在,让我们再次尝试这个练习,但这次使用相同函数的不同实现方式:

func join(s1, s2 string, max int) (string, error) {
    if s1 == "" {
        return "", errors.New("s1 is empty")
    }
    if s2 == "" {
        return "", errors.New("s2 is empty")
    }
    concat, err := concatenate(s1, s2)
    if err != nil {
        return "", err
    }
    if len(concat) > max {
        return concat[:max], nil
    }
    return concat, nil
}
func concatenate(s1 string, s2 string) (string, error) {
    // ...
}

您可能已经注意到,构建这个新版本的心智模型所需的认知负荷较小,尽管它与之前做的工作相同。在这里,我们只保留了两个嵌套层次。正如《Go Time》播客的嘉宾 Mat Ryer 所提到的(medium.com/@matryer/li…):

将主要执行路径(快乐路径)左对齐;你应该能够迅速地沿一列向下扫描,以查看预期的执行流程。

第一个版本由于嵌套的 if/else 语句,很难区分预期的执行流程。

相反,第二个版本只需要扫描一列就能看到预期的执行流程,通过扫描第二列则能看到如何处理边缘情况,如图 2.1 所示。

image-20240509171139631.png

一般来说,一个函数所需的嵌套层次越多,它就越难阅读和理解。让我们看看这个规则的一些不同应用,以优化我们的代码可读性:

复制再试一次分享:

  • 当一个 if 代码块使用了 return 语句时,我们应该在所有情况下省略 else 代码块。例如,我们不应该这样写:

    if foo() {
        // ...
        return true
    } else {
        // ... 
    }
    

    相反,我们应该像这样省略 else 代码块:

    if foo() {
        // ...
        return true 
    }
    // ...
    

    使用这个新版本,之前位于 else 代码块中的代码被移动到了最外层,这样做使得代码更易于阅读。

  • 我们也可以将这个逻辑应用于非主要执行路径:

    if s != "" {
        // ...
    } else {
        return errors.New("empty string")
    }
    

    在这里,一个空的 s 表示非主要执行路径。因此,我们应该像这样翻转条件:

    if s == "" { // 翻转 if 条件
        return errors.New("empty string") 
    }
    // ...
    

    这个新版本更容易阅读,因为它将主要执行路径保持在左侧边缘,并减少了代码块的数量。

    编写可读性强的代码是每位开发者面临的重要挑战。努力减少嵌套代码块的数量、将主要执行路径左对齐,并且尽可能早地返回都是提高代码可读性的具体方法。

    在下一节中,我们将讨论 Go 项目中常见的一个误用:初始化(init)函数。