为什么我喜欢Go的错误处理
#go #errors

GO语言中最具争议的问题之一是我们如何处理错误。我记得在使用PHP几年之后,当我开始使用该语言时,我很惊讶它没有使用著名的 try/catch

第一次影响通过后,我试图理解原因,官方答案是:“在GO中,错误是一流的公民。”因此,您有责任明确处理它们。

我将展示一个示例来说明这一点。我最近在Java中遇到了以下代码:

private JsonNode get(String query) {
    try {
        SimpleHttp.Response response = SimpleHttp.doGet(query, this.session)
                .connectionRequestTimeoutMillis(Integer.parseInt(model.get(ConsumerProviderConstants.TIMEOUT_CONSUMER)))
                .connectTimeoutMillis(Integer.parseInt(model.get(ConsumerProviderConstants.CONN_TIMEOUT_CONSUMER)))
                .socketTimeOutMillis(Integer.parseInt(model.get(ConsumerProviderConstants.SOCKET_TIMEOUT_CONSUMER)))
                .header(HttpHeaders.CONTENT_TYPE, "application/json; charset=utf-8")
                .asResponse();
        switch (response.getStatus()) {
            case HttpStatus.SC_OK:
                return response.asJson();
            default:
                logger.errorf("private get(%s) - ResponseBody: {%s} - StatusCode(%d)", query, response.asString(),
                        response.getStatus());
                return null;
        }
    } catch (Exception e) {
        throw new ServiceException("Error on retrieve data");
    }
}

找到了一些“检索数据上的错误” 出现在应用程序日志中。

中的出现。

上面的代码有什么问题?查找代码的哪一部分导致错误很棘手。它可以在 simplehttp.doget model.get integer.parseint

进行了重构,以便我们可以使代码更强大。结果,看起来像这样(我们可以做更多的重构,但是在此示例中,就足够了):

public int tryParseInt(String stringToParse, int defaultValue, String varName) {
    try {
        return Integer.parseInt(stringToParse);
    } catch (NumberFormatException ex) {
        logger.errorf("tryParseInt(String %s, varName %s)", stringToParse, varName);
        return defaultValue;
    }
}

private JsonNode get(String query) {
    try {
        SimpleHttp.Response response = SimpleHttp.doGet(query, this.session)
                .connectionRequestTimeoutMillis(
                        this.tryParseInt(model.get(ConsumerProviderConstants.TIMEOUT_CONSUMER), 1000,
                                ConsumerProviderConstants.TIMEOUT_CONSUMER))
                .connectTimeoutMillis(this.tryParseInt(model.get(ConsumerProviderConstants.CONN_TIMEOUT_CONSUMER),
                        1000, ConsumerProviderConstants.CONN_TIMEOUT_CONSUMER))
                .socketTimeOutMillis(this.tryParseInt(model.get(ConsumerProviderConstants.SOCKET_TIMEOUT_CONSUMER),
                        5000, ConsumerProviderConstants.SOCKET_TIMEOUT_CONSUMER))
                .header(HttpHeaders.CONTENT_TYPE, "application/json; charset=utf-8")
                .asResponse();

        if (response == null) {
            logger.errorf("Response was return null");
            throw new ServiceException("Error response null");
        }

        switch (response.getStatus()) {
            case HttpStatus.SC_OK:
                return response.asJson();
            default:
                logger.errorf("private get(%s) - ResponseBody: {%s} - StatusCode(%d)", query, response.asString(),
                        response.getStatus());
                return null;
        }
    } catch (Exception e) {
        logger.errorf("ConsumerService Response error (%s)", e);
        throw new ServiceException("Error on retrieve data");
    }
}

好多了,因为现在我们已经确定了当将 timeout_consumer 环境变量转换为整数时,由于它是空的,因此发生了错误。关于此新代码的关注点是,现在 tryparseint 函数知道并处理 numberformatexception 。新代码之间生成了它们之间的耦合。如果在某个时候,此例外的名称更改或类开始抛出新异常,我们可能需要更改代码以适应此更改。

但是这与去有什么关系?此处介绍的问题不仅是Java发生的,否则罪魁祸首是 try/catch 的概念,更不用说撰写代码或审查了代码的人(顺便说一句,我是一个人审查此代码的人)。我的观点是Java(和其他语言)允许此结构。

同时,作为一种强烈的语言,GO使这种情况变得更加困难。因此,这是我对上述代码的解释:

package main

import (
    "encoding/json"
    "fmt"
    "io/ioutil"
    "net/http"
    "strconv"
    "time"
)

const (
    timeoutConsumer = "timeout.consumer"
)

type model struct{}

// just to emulate the model
func (m model) get(name string) string {
    return "0"
}

func main() {
    json, err := getJSON("http://url_to_be_consumed")
    if err != nil {
        panic(err) // handle error
    }
    fmt.Println(json)
}

func getJSON(query string) (string, error) {
    m := model{}
    tc, err := strconv.Atoi(m.get(timeoutConsumer))
    if err != nil {
        return "", fmt.Errorf("error converting timeout: %w", err)
    }
    req, err := http.NewRequest(http.MethodGet, query, nil)
    if err != nil {
        return "", fmt.Errorf("error creating request: %w", err)
    }
    req.Header.Set("Content-Type", "application/json; charset=utf-8")

    client := http.Client{
        Timeout: time.Duration(tc) * time.Second,
    }
    res, err := client.Do(req)
    if err != nil {
        return "", fmt.Errorf("error executing request: %w", err)
    }
    if res == nil {
        return "", fmt.Errorf("empty response")
    }
    if res.StatusCode != http.StatusOK {
        return "", fmt.Errorf("expected %d received %d", http.StatusOK, res.StatusCode)
    }

    resBody, err := ioutil.ReadAll(res.Body)
    if err != nil {
        return "", fmt.Errorf("error reading body: %w", err)
    }
    var result map[string]any
    err = json.Unmarshal(resBody, &result)
    if err != nil {
        return "", fmt.Errorf("error parsing json: %w", err)
    }
    jsonStr, err := json.Marshal(result)
    if err != nil {
        return "", fmt.Errorf("error converting json: %w", err)
    }
    return string(jsonStr), nil
}

代码变得更加详细。编写相同功能的更多行和“ OMG,那一堆If/else!”。我同意这个论点。确实,该代码变得更长,但其读取变得更加明显。每个函数总是返回错误力量的事实,无论谁编写代码来处理或忽略它,这将是非常明显的。例如,可以更改摘要:

tc, err := strconv.Atoi(m.get(timeoutConsumer))
if err != nil {
  return "", fmt.Errorf("error converting timeout: %w", err)
}

作者:

tc, _ := strconv.Atoi(m.get(timeoutConsumer))

但是,在这种情况下,任何人都可以在《代码审查》中进行观察:

嘿,您是否忽略了转换错误?您确定可以这样做吗?

正如我们在此示例中看到的,转换错误可能发生:)

这是我为什么我喜欢如何处理错误的说明。当然,它可能会像其他语言一样更好。它是冗长的,但我宁愿写更多的行并具有更易于维护的代码。

最初于2023年3月27日在https://eltonminetto.dev出版。