20

在我看来,谷歌的例外替代方案是

  • GO:多值返回“return val, err;”
  • GO,C++:无检查(提前返回)
  • GO,C++:“处理该死的错误”(我的术语)
  • C++:断言(表达式)

  • GO:defer/panic/recover 是在提出此问题后添加的语言功能

多值回报是否足以作为替代品?为什么“断言”被认为是替代品?如果发生错误处理不正确,Google 是否认为程序停止是可以的?

有效 GO:多个返回值

Go 的一个不同寻常的特性是函数和方法可以返回多个值。这可以用来改进 C 程序中的几个笨拙的习惯用法:带内错误返回(例如 EOF 的 -1)和修改参数。

在 C 语言中,写入错误由负计数表示,错误代码隐藏在易失性位置。在 Go 中,Write 可以返回一个计数和一个错误:“是的,你写了一些字节,但不是全部,因为你填满了设备”。os 包中 *File.Write 的签名为:

func (file *File) Write(b []byte) (n int, err Error)

正如文档所说,它返回写入的字节数和当 n != len(b) 时的非零错误。这是一种常见的风格;有关更多示例,请参见错误处理部分。

有效 GO:命名结果参数

Go 函数的返回或结果“参数”可以指定名称并用作常规变量,就像传入参数一样。当命名时,它们在函数开始时被初始化为它们的类型的零值;如果函数执行没有参数的 return 语句,则使用结果参数的当前值作为返回值。

这些名称不是强制性的,但它们可以使代码更短更清晰:它们是文档。如果我们命名 nextInt 的结果,很明显哪个返回的 int 是哪个。

func nextInt(b []byte, pos int) (value, nextPos int) {

因为命名结果被初始化并与一个简单的返回相关联,所以它们可以简化和澄清。这是一个很好地使用它们的 io.ReadFull 版本:

func ReadFull(r Reader, buf []byte) (n int, err os.Error) {
  for len(buf) > 0 && err == nil {
    var nr int;
    nr, err = r.Read(buf);
    n += nr;
    buf = buf[nr:len(buf)];
  }
  return;
}

为什么 Go 没有异常?

例外是一个类似的故事。已经提出了许多异常设计,但每一种都大大增加了语言和运行时的复杂性。就其本质而言,异常跨越函数,甚至可能跨越 goroutine。它们具有广泛的影响。人们还担心它们会对图书馆产生影响。根据定义,它们是卓越的,但使用其他支持它们的语言的经验表明它们对库和接口规范具有深远的影响。如果能找到一种设计,让它们真正出类拔萃,而又不会鼓励常见错误变成需要每个程序员进行补偿的特殊控制流,那就太好了。

像泛型一样,异常仍然是一个悬而未决的问题。

Google C++ 风格指南:例外

决定:

从表面上看,使用例外的好处大于成本,尤其是在新项目中。但是,对于现有代码,异常的引入对所有依赖代码都有影响。如果异常可以传播到新项目之外,那么将新项目集成到现有的无异常代码中也会出现问题。由于 Google 现有的大多数 C++ 代码都没有准备好处理异常,因此采用会产生异常的新代码相对困难。

鉴于 Google 现有的代码不能容忍异常,使用异常的成本比新项目的成本要高一些。转换过程会很慢并且容易出错。我们不认为异常的可用替代方法(例如错误代码和断言)会带来很大的负担。

我们反对使用例外的建议不是基于哲学或道德依据,而是基于实际的依据。因为我们想在 Google 使用我们的开源项目,如果这些项目使用异常就很难做到,所以我们也需要建议不要在 Google 开源项目中使用异常。如果我们不得不从头开始重新做一遍,事情可能会有所不同。

GO:推迟、恐慌和恢复

Defer 语句允许我们考虑在打开每个文件后立即关闭它,从而保证无论函数中返回语句的数量如何,文件都将被关闭。

defer 语句的行为是直接且可预测的。有三个简单的规则:

1. 当 defer 语句被评估时,延迟函数的参数被评估。

在这个例子中,当 Println 调用被延迟时,表达式“i”被计算。函数返回后,延迟调用将打印“0”。

    func a() {
         i := 0
         defer fmt.Println(i)
         i++
         return    
    }

2. 延迟函数调用在周围函数返回后按后进先出的顺序执行。此函数打印“3210”:

     func b() {
        for i := 0; i < 4; i++ {
            defer fmt.Print(i)
        }   
     }

3. 延迟函数可以读取并分配给返回函数的命名返回值。

在此示例中,延迟函数在周围函数返回后递增返回值 i。因此,此函数返回 2:

    func c() (i int) {
        defer func() { i++ }()
        return 1 
    }

这样方便修改函数的错误返回值;我们很快就会看到一个例子。

Panic 是一个内置函数,它可以停止普通的控制流程并开始恐慌。当函数 F 调用 panic 时,F 的执行停止,F 中的所有延迟函数都正常执行,然后 F 返回其调用者。对调用者来说,F 的行为就像是对恐慌的调用。该进程继续向上堆栈,直到当前 goroutine 中的所有函数都返回,此时程序崩溃。恐慌可以通过直接调用恐慌来启动。它们也可能是由运行时错误引起的,例如越界数组访问。

Recover 是一个内置函数,可以重新控制恐慌的 goroutine。恢复仅在延迟函数中有用。在正常执行期间,recover 调用将返回 nil 并且没有其他效果。如果当前的 goroutine 正在恐慌,对 recovery 的调用将捕获为 panic 提供的值并恢复正常执行

这是一个演示恐慌和延迟机制的示例程序:

<snip>

有关恐慌和恢复的真实示例,请参阅 Go 标准库中的 json 包。它使用一组递归函数对 JSON 编码的数据进行解码。当遇到格式错误的 JSON 时,解析器调用 panic 是将堆栈展开到顶层函数调用,该函数调用会从 panic 中恢复并返回适当的错误值(参见 decode.go 中的 'error' 和 'unmarshal' 函数) . 在 regexp 包的 Compile 例程中有一个类似的技术示例。Go 库中的约定是,即使包在内部使用了 panic,它的外部 API 仍然会显示显式的错误返回值。

defer 的其他用途(除了前面给出的 file.Close() 示例之外)包括释放互斥锁:

mu.Lock()  
defer mu.Unlock
4

7 回答 7

14

多重返回并不是 Go 独有的,它们也不能替代异常。在 C(或 C++)术语中,它们是返回包含多个值的结构(对象)的简洁且用户友好的替代品。

它们确实提供了一种方便的方法来指示错误,如果这就是你的意思的话。

为什么“断言”被认为是替代品?

断言最初用于调试。他们在程序处于“不可能”状态的情况下停止程序,设计认为不应该发生,但无论如何都会发生。返回错误不太可能有太大帮助。代码库显然还不能正常工作,那么它到底如何才能成功恢复呢?当有一个需要注意的错误时,你为什么还要它呢?

在生产代码中使用断言有点不同 - 显然存在性能和代码大小问题,因此通常的方法是在您的代码分析和测试使您确信“不可能”的情况确实是不可能的时将其删除。但是,如果你在这种偏执的水平上运行代码,它正在审计自己,那么你可能也会偏执,如果你让它继续在“不可能”的状态下运行,那么它可能会做一些危险的破坏:破坏有价值的数据,超出堆栈分配并可能造成安全漏洞。再说一次,您只想尽快关闭。

你使用断言的东西实际上和你使用异常的东西是不一样的:当像 C++ 和 Java 这样的编程语言为“不可能”的情况(logic_errorArrayOutOfBoundsException)提供异常时,它们无意中鼓励一些程序员认为他们的程序应该尝试从真正失控的情况中恢复过来。有时这是合适的,但 Java 建议不要捕获 RuntimeExceptions 是有充分理由的。偶尔抓住一个是个好主意,这就是它们存在的原因。几乎总是抓住它们不是一个好主意,这意味着它们无论如何都等于停止程序(或至少是线程)。

于 2009-12-05T13:30:18.507 回答
4

您应该阅读几篇关于异常的文章,以了解返回值不是异常。不是以 C“带内”方式或任何其他方式。

无需深入讨论,异常应该在发现错误条件的地方抛出,并在可以有意义地处理错误条件的地方捕获。返回值仅在层次结构堆栈的第一个函数中处理,该函数可能或不能如何处理问题。一个简单的例子是一个配置文件,它可以将值作为字符串检索,并且还支持处理成类型化的返回语句:

class config {
   // throws key_not_found
   string get( string const & key );
   template <typename T> T get_as( string const & key ) {
      return boost::lexical_cast<T>( get(key) );
   }
};

现在的问题是,如果找不到密钥,您将如何处理。如果您使用返回码(比如在 go-way 中),问题是get_as必须处理错误代码get并采取相应措施。由于它真的不知道该怎么做,唯一明智的做法是手动向上游传播错误:

class config2 {
   pair<string,bool> get( string const & key );
   template <typename T> pair<T,bool> get_as( string const & key ) {
      pair<string,bool> res = get(key);
      if ( !res.second ) {
          try {
             T tmp = boost::lexical_cast<T>(res.first);
          } catch ( boost::bad_lexical_cast const & ) {
             return make_pair( T(), false ); // not convertible
          }
          return make_pair( boost::lexical_cast<T>(res.first), true );
      } else {
          return make_pair( T(), false ); // error condition
      }
   }
}

类的实现者必须添加额外的代码来转发错误,并且该代码与问题的实际逻辑混杂在一起。在 C++ 中,这可能比在为多重赋值设计的语言中更繁重a,b=4,5lexical_cast无论如何进入变量。

于 2009-12-05T13:42:25.967 回答
3

这不是 Go,但在 Lua 中,多次返回是处理异常的非常常见的习惯用法。

如果你有一个像

function divide(top,bottom)
   if bottom == 0 then 
        error("cannot divide by zero")
   else
        return top/bottom
   end
end

然后当bottom为 0 时,将引发异常并且程序的执行将停止,除非您将函数包装dividepcall(或受保护的调用)中。

pcall总是返回两个值:第一个是结果是一个布尔值,告诉函数是否成功返回,第二个结果是返回值或错误消息。

以下(人为的)Lua 代码段显示了它的使用情况:

local top, bottom = get_numbers_from_user()
local status, retval = pcall(divide, top, bottom)
if not status then
    show_message(retval)
else
    show_message(top .. " divided by " .. bottom .. " is " .. retval)
end

当然pcall,如果你调用的函数已经以status, value_or_error.

多年来,多次返回对于 Lua 来说已经足够好,所以虽然这并不能确保它对于 Go 来说足够好,但它支持这个想法。

于 2009-12-05T13:54:33.230 回答
2

是的,错误返回值很好,但没有捕捉到异常处理的真正含义……这是对通常不打算发生的异常情况的能力和管理。

Java(ie)设计认为异常 IMO 是有效的工作流场景,并且它们对必须声明和版本这些抛出的异常的接口和库的复杂性有一点看法,但可惜异常在堆栈多米诺骨牌中起着重要作用。

想想另一种情况,在几十个方法调用中,有条件地处理异常返回代码。根据违规行号的位置,堆栈跟踪会是什么样子?

于 2009-12-05T13:40:09.563 回答
2

这个问题很难客观地回答,对异常的看法可能会有很大的不同。

但如果我推测一下,我认为 Go 中不包含异常的主要原因是因为它使编译器复杂化,并且在编写库时可能导致不平凡的影响。例外情况很难解决,他们优先考虑让某些东西正常工作。

通过返回值和异常处理错误的主要区别在于异常迫使程序员处理异常情况。除非您明确地捕获异常并且在 catch 块中什么都不做,否则您永远不会有“静默错误”。另一方面,您在函数内部随处可见隐式返回点,这可能导致其他类型的错误。这在 C++ 中尤其普遍,您在其中显式管理内存并需要确保永远不会丢失指向已分配内容的指针。

C++ 中的危险情况示例:

struct Foo {
    // If B's constructor throws, you leak the A object.
    Foo() : a(new A()), b(new B()) {}
    ~Foo() { delete a; delete b; }

    A *a;
    B *b;
};

多个返回值可以更轻松地实现基于返回值的错误处理,而不必依赖函数的输出参数,但它不会从根本上改变任何东西。

某些语言同时具有多个返回值和异常(或类似机制)。一个例子是Lua

于 2009-12-05T13:53:23.280 回答
1

这是一个示例,说明多个返回值如何在 c++ 中工作。我自己不会编写这段代码,但我认为使用这种方法并不是完全不可能的。

#include <iostream>
#include <fstream>
#include <string>
using namespace std;

// return value type
template <typename T> 
struct RV {
    int mStatus;
    T mValue;

    RV( int status, const T & rv ) 
        : mStatus( status ), mValue( rv ) {}
    int Status() const { return mStatus; }
    const T & Value() const {return mValue; }
};

// example of possible use
RV <string> ReadFirstLine( const string & fname ) {
    ifstream ifs( fname.c_str() );
    string line;
    if ( ! ifs ) {
        return RV <string>( -1, "" );
    }
    else if ( getline( ifs, line ) ) {
        return RV <string>( 0, line );
    }
    else {
        return RV <string>( -2, "" );
    }
}

// in use
int main() {
    RV <string> r = ReadFirstLine( "stuff.txt" );
    if ( r.Status() == 0 ) {
        cout << "Read: " << r.Value() << endl;
    }
    else {
        cout << "Error: " << r.Status() << endl;
    }
}
于 2009-12-05T13:56:08.937 回答
-2

如果您需要 C++ 方法来执行“可空”对象,请使用 boost::optional< T >。您将其作为布尔值进行测试,如果它评估为真,则将其取消引用为有效的 T。

于 2011-08-02T20:58:16.610 回答