我一直有一个印象,如果可能的话,永远不要使用goto。

然而,在前几天阅读libavcodec(它是用C编写的)时,我惊讶地注意到它的多种用法。

在支持循环和函数的语言中使用goto是否有优势?如果有,为什么?请提供一个具体的例子,清楚地说明使用goto的理由。


当前回答

我发现有趣的是,有些人会给出一个可以接受goto的例子列表,说所有其他的用法都是不可接受的。你真的认为你知道每种情况下goto是表达算法的最佳选择吗?

为了说明这一点,我将给你一个还没有人展示过的例子:

今天我在写代码,在哈希表中插入一个元素。哈希表是以前计算的缓存,可以随意重写(影响性能但不影响正确性)。

哈希表的每个桶都有4个槽,当桶满时,我有一堆标准来决定覆盖哪个元素。现在,这意味着在一个桶中最多要经过三次,就像这样:

// Overwrite an element with same hash key if it exists
for (add_index=0; add_index < ELEMENTS_PER_BUCKET; add_index++)
  if (slot_p[add_index].hash_key == hash_key)
    goto add;

// Otherwise, find first empty element
for (add_index=0; add_index < ELEMENTS_PER_BUCKET; add_index++)
  if ((slot_p[add_index].type == TT_ELEMENT_EMPTY)
    goto add;

// Additional passes go here...

add:
// element is written to the hash table here

如果不使用goto,代码会是什么样子?

就像这样:

// Overwrite an element with same hash key if it exists
for (add_index=0; add_index < ELEMENTS_PER_BUCKET; add_index++)
  if (slot_p[add_index].hash_key == hash_key)
    break;

if (add_index >= ELEMENTS_PER_BUCKET) {
  // Otherwise, find first empty element
  for (add_index=0; add_index < ELEMENTS_PER_BUCKET; add_index++)
    if ((slot_p[add_index].type == TT_ELEMENT_EMPTY)
      break;
  if (add_index >= ELEMENTS_PER_BUCKET)
   // Additional passes go here (nested further)...
}

// element is written to the hash table here

如果添加更多的遍数,它看起来会越来越糟,而带有goto的版本始终保持相同的缩进级别,并避免使用虚假的if语句,其结果由前一个循环的执行暗示。

所以在另一种情况下,goto使代码更清晰,更容易编写和理解……我相信还有更多的例子,所以不要假装知道所有goto有用的例子,而轻视任何你想不到的好例子。

其他回答

我发现有趣的是,有些人会给出一个可以接受goto的例子列表,说所有其他的用法都是不可接受的。你真的认为你知道每种情况下goto是表达算法的最佳选择吗?

为了说明这一点,我将给你一个还没有人展示过的例子:

今天我在写代码,在哈希表中插入一个元素。哈希表是以前计算的缓存,可以随意重写(影响性能但不影响正确性)。

哈希表的每个桶都有4个槽,当桶满时,我有一堆标准来决定覆盖哪个元素。现在,这意味着在一个桶中最多要经过三次,就像这样:

// Overwrite an element with same hash key if it exists
for (add_index=0; add_index < ELEMENTS_PER_BUCKET; add_index++)
  if (slot_p[add_index].hash_key == hash_key)
    goto add;

// Otherwise, find first empty element
for (add_index=0; add_index < ELEMENTS_PER_BUCKET; add_index++)
  if ((slot_p[add_index].type == TT_ELEMENT_EMPTY)
    goto add;

// Additional passes go here...

add:
// element is written to the hash table here

如果不使用goto,代码会是什么样子?

就像这样:

// Overwrite an element with same hash key if it exists
for (add_index=0; add_index < ELEMENTS_PER_BUCKET; add_index++)
  if (slot_p[add_index].hash_key == hash_key)
    break;

if (add_index >= ELEMENTS_PER_BUCKET) {
  // Otherwise, find first empty element
  for (add_index=0; add_index < ELEMENTS_PER_BUCKET; add_index++)
    if ((slot_p[add_index].type == TT_ELEMENT_EMPTY)
      break;
  if (add_index >= ELEMENTS_PER_BUCKET)
   // Additional passes go here (nested further)...
}

// element is written to the hash table here

如果添加更多的遍数,它看起来会越来越糟,而带有goto的版本始终保持相同的缩进级别,并避免使用虚假的if语句,其结果由前一个循环的执行暗示。

所以在另一种情况下,goto使代码更清晰,更容易编写和理解……我相信还有更多的例子,所以不要假装知道所有goto有用的例子,而轻视任何你想不到的好例子。

当然,可以使用GOTO,但是有一件事比代码风格更重要,或者在使用它时,您必须考虑到代码是否可读:其中的代码可能不像您想象的那样健壮。

例如,看看下面的两个代码片段:

If A <> 0 Then A = 0 EndIf
Write("Value of A:" + A)

GOTO的等效代码

If A == 0 Then GOTO FINAL EndIf
   A = 0
FINAL:
Write("Value of A:" + A)

我们首先想到的是这两段代码的结果将是“Value of A: 0”(当然,我们假设执行没有并行性)

这是不正确的:在第一个示例中,A将始终为0,但在第二个示例中(使用GOTO语句)A可能不是0。为什么?

原因是,从程序的另一点,我可以插入一个GOTO FINAL而不控制a的值。

这个例子非常明显,但是随着程序变得越来越复杂,看到这些东西的难度也增加了。

相关材料可以在Dijkstra先生的著名文章“反对GO TO声明的案例”中找到

它有时在按字符进行字符串处理时很方便。

想象一下这样一个printf式的例子:

for cur_char, next_char in sliding_window(input_string) {
    if cur_char == '%' {
        if next_char == '%' {
            cur_char_index += 1
            goto handle_literal
        }
        # Some additional logic
        if chars_should_be_handled_literally() {
            goto handle_literal
        }
        # Handle the format
    }
    # some other control characters
    else {
      handle_literal:
        # Complicated logic here
        # Maybe it's writing to an array for some OpenGL calls later or something,
        # all while modifying a bunch of local variables declared outside the loop
    }
}

您可以将goto handle_literal重构为一个函数调用,但如果它修改了几个不同的局部变量,则必须将引用传递给每个局部变量,除非您的语言支持可变闭包。如果您的逻辑使else case不起作用,那么您仍然必须在调用之后使用continue语句(可以说是goto的一种形式)以获得相同的语义。

我还在lexer中明智地使用了gotos,通常用于类似的情况。大多数时候你不需要它们,但在一些奇怪的情况下有它们很好。

我在以下情况下使用goto: 当需要从不同位置的函数返回时,并且在返回之前需要进行一些初始化:

non-goto版本:

int doSomething (struct my_complicated_stuff *ctx)    
{
    db_conn *conn;
    RSA *key;
    char *temp_data;
    conn = db_connect();  


    if (ctx->smth->needs_alloc) {
      temp_data=malloc(ctx->some_size);
      if (!temp_data) {
        db_disconnect(conn);
        return -1;      
        }
    }

    ...

    if (!ctx->smth->needs_to_be_processed) {
        free(temp_data);    
        db_disconnect(conn);    
        return -2;
    }

    pthread_mutex_lock(ctx->mutex);

    if (ctx->some_other_thing->error) {
        pthread_mutex_unlock(ctx->mutex);
        free(temp_data);
        db_disconnect(conn);        
        return -3;  
    }

    ...

    key=rsa_load_key(....);

    ...

    if (ctx->something_else->error) {
         rsa_free(key); 
         pthread_mutex_unlock(ctx->mutex);
         free(temp_data);
         db_disconnect(conn);       
         return -4;  
    }

    if (ctx->something_else->additional_check) {
         rsa_free(key); 
         pthread_mutex_unlock(ctx->mutex);
         free(temp_data);
         db_disconnect(conn);       
         return -5;  
    }


    pthread_mutex_unlock(ctx->mutex);
    free(temp_data);    
    db_disconnect(conn);    
    return 0;     
}

goto版本:

int doSomething_goto (struct my_complicated_stuff *ctx)
{
    int ret=0;
    db_conn *conn;
    RSA *key;
    char *temp_data;
    conn = db_connect();  


    if (ctx->smth->needs_alloc) {
      temp_data=malloc(ctx->some_size);
      if (!temp_data) {
            ret=-1;
           goto exit_db;   
          }
    }

    ...

    if (!ctx->smth->needs_to_be_processed) {
        ret=-2;
        goto exit_freetmp;      
    }

    pthread_mutex_lock(ctx->mutex);

    if (ctx->some_other_thing->error) {
        ret=-3;
        goto exit;  
    }

    ...

    key=rsa_load_key(....);

    ...

    if (ctx->something_else->error) {
        ret=-4;
        goto exit_freekey; 
    }

    if (ctx->something_else->additional_check) {
        ret=-5;
        goto exit_freekey;  
    }

exit_freekey:
    rsa_free(key);
exit:    
    pthread_mutex_unlock(ctx->mutex);
exit_freetmp:
    free(temp_data);        
exit_db:
    db_disconnect(conn);    
    return ret;     
}

当您需要更改释放语句中的某些内容时(每个语句在代码中使用一次),第二个版本使它变得更容易,并减少了在添加新分支时跳过其中任何一个语句的机会。在函数中移动它们在这里不会有帮助,因为可以在不同的“级别”进行释放。

这些年来,我写了不少汇编语言。最终,每一种高级语言都被编译成gotos。好吧,叫它们“分支”或“跳跃”或其他什么,但它们是gotos。有人能写无goto汇编器吗?

当然,你可以向Fortran、C或BASIC程序员指出,gotos的泛滥就像意大利肉酱面一样。然而,答案不是避免它们,而是小心地使用它们。

刀可以用来准备食物,解救某人,或者杀死某人。我们会因为害怕后者而没有刀吗?同样,“后向”:不小心使用它会碍事,小心使用它会有所帮助。