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出版。