C语言 - goto关键词

一、背景  

  长期以来,goto 的使用一直存在较大的争议。goto 语句的主要问题并不在于语法本身,而在于不受约束的跳转容易破坏程序原有的控制流结构。当一个函数中存在大量跨区域、双向或互相交叉的跳转时,代码的执行路径会变得难以追踪,最终形成难以阅读、调试和维护的“面条代码”。

  随着结构化编程思想在20世纪60至70年代逐渐普及,越来越多的开发者开始倡导使用 if-else、for、while、switch等结构化语句代替 goto。时至今日,许多编程规范仍然会限制甚至禁止使用 goto。这一争论中较为著名的文章是 Edsger W.Dijkstra 于1968年发表的 《Go To Statement Considered Harmful》。文章指出,过度使用 goto 会使程序的控制流程难以建立清晰的逻辑结构,因此应尽量避免使用。不过,Donald Ervin Knuth 在他的 《Structured Programming with go to Statements 》文章中也分析了许多常见的编程任务,发现其中的一些任务使用 goto语句可以得到最理想的结构。

  所以,goto 本身并不能简单地定义为“好”或“坏”。goto语句更像是一种需要严格限制使用范围的控制流工具,开发者不仅要了解它的语法,还要能够判断跳转是否会破坏函数的整体结构。哪怕具备足够的使用经验,也更推荐优先考虑结构化语句,只有在确定能够提升代码清晰度、减少重复逻辑时,才推荐谨慎使用 goto。毕竟企业工程的开发是团队协作的过程,必须要考虑到团队其他开发者的工程开发体验。

  如果希望结合实际工程源码理解 goto 的合理用法,可以阅读 Linux 内核源码。Linux 内核中存在较多 goto 的使用场景,主要用于错误处理、资源释放以及多阶段初始化失败后的统一退出流程。

Linux 内核源码: https://github.com/torvalds/linux/tree/master

二、语法格式

  goto 是C语言中的无条件跳转语句。执行 goto 后,程序会直接跳转到当前函数内部指定标签(label)的位置,并从该位置继续顺序执行

goto label;

/* 其它代码 */

label:
    printf("Hello\n");

  goto 是执行跳转动作,label 是定义跳转目标位置。printf("Hello\n") 是标签(label)后的第一条执行语句。这里需要注意,label 本身不执行任何操作,只是用来定义 goto 要跳转的目标位置

三、用法示例及注意事项

(一)跳转至标签后继续顺序执行程序

int Device_Init(void)
{
    int ret = -1;

    ret = Clock_Init();
    if(ret != 0)
    {
        goto err_clock;
    }

    ret = DMA_Init();
    if(ret != 0)
    {
        goto err_dma;
    }

    ret = Task_Create();
    if(ret != 0)
    {
        goto err_task;
    }
    return 0;

err_task:
    UART_DeInit();

err_dma:
    Clock_DeInit();

err_clock:
    return -1;
}

  如果跳转至 err_task 标签处,程序会依照 UART_DeInit() -> Clock_DeInit() -> return -1 的顺序执行,并不会跳转执行一条语句即结束或执行其他的顺序,除非跳转到 err_task 标签后又执行了 goto 跳转到其它地方。

(二)label 命名

    label 命名的基本规则如下:

  • 只能由字母、数字、下划线组成
  • 不能以数字开头
  • 不能使用C语言关键字
  • 大小写敏感
/* 合法命名 */
error:
Error_Handler:
err_dma:
cleanup1:
exit:

/* 非法命名 */
1error:        //不能以数字开头
err-dma:       //不能包含非法字符'-'
goto:          //goto 是关键字

(三)goto 只能在同一函数内部执行

  goto 语句的执行只能在同一函数内跳转,无法执行跨函数跳转。比如:

#include <stdio.h>

void Device_Init(void)
{
    goto err;

    printf("normal code\n");

err:
    printf("init error\n");
}

  这个属于正常的调用,在函数执行到 goto err 时,下面的 printf("normal code\n") 不会执行,而是会跳到 err: 处直接执行 printf("init error\n") 。此时 goto 和 err 属于同一个函数内。但是当跨函数执行时:

#include <stdio.h>

void Other(void)
{
error:
    printf("error\n");
}

void Device(void)
{
    goto error;
}

  goto error 是在 Device() 函数中执行,标签 error 在另一个函数 Other() 中,此时编译就会直接报错

image

  这里是因为 goto 查找 label 时,只会在当前函数内部寻找标签,虽然代码中存在 error: 标签,但它属于 Other() 函数,而 goto error 位于 Device() 函数中,因此对 Device() 来说,该标签并不存在。

  goto 不能跨函数跳转,主要是因为函数之间拥有独立的执行上下文。每个函数都有自己的栈帧、局部变量生命周期、参数空间以及返回地址。如果允许 Device() 直接跳转到 Other() 内部的标签,那么 Other() 并没有经过正常调用流程,相关栈帧没有创建。局部变量也看没有初始化,最终会破坏C语言函数调用机制。

  从 goto 的作用域看,goto 只是修改当前函数内部的程序执行位置,而 label 也只是当前函数内部某个代码位置的标记,它并不是一个全局符号。所以无法支持不同函数之间的流程控制。

(四)label 属于函数作用域,不属于 if/for 等代码块

   label 的作用域与函数中普通变量的作用域不同,label 的作用域是整个函数,不受代码块(block)的限制,但是普通变量的作用域会受到代码块(block)的限制。比如:

void Test(void)
{
    int a = 10;

    goto error;

    if(0)
    {
        error:

        printf("error\n");

        int b = 20;

        printf("%d\n", a);
        printf("%d\n", b);
    }
        printf("%d\n", a);
        printf("%d\n", b);//错误
        printf("error\n");
}

  执行此函数会报错“error: 'b' undeclared (first use in this function)”这个就是变量 'b' 只在 if(0) 的代码块中生效但是在代码块也用了此变量造成的错误。而 label 则不受 if(0) 的代码块的限制。

(五)不要使用 goto 跳入局部变量定义区域,避免绕过变量初始化

   goto 的时候要注意不能跳过局部变量的初始化,否则对应变量会变成一个不确定的数。比如:

void Test(void)
{
    goto error;

    if(1)
    {
        int a = 10;
        
        error:
        printf("%d\n", a);
        
        int b = 20;
        printf("%d\n", b);
    }
}

  输出的结果是 0  20 ,这个 0 是一个随机数,并不代表 a 的值就是 0。此时变量 a 的初始化被 goto 跳过,a 就变成了一个未定义行为。在工程中一旦出现这种情况可能会直接 HardFault。

(六)goto 最好不用于正常业务流程,只用于异常处理、资源回滚

   这条注意事项更多的还是工程经验的总结,并不是 goto 的语法规则。本意是在说明 goto 并不建议用于组织正常的业务流程,更适合把它限制在异常退出、错误处理和资源回滚这类“非正常路径”中。比如:

int main(void)
{
    int state = 0;

start:
    if(state == 0)
    {
        state = 1;
        goto process;
    }

process:
    if(state == 1)
    {
        state = 2;
        goto finish;
    }

finish:
    printf("done\n");

    return 0;
}

  在这个业务中,虽然使用 goto 语法上完全合法,但是本质上还是用 goto 来人工组织 start -> process -> finish 这一流程。现在这段业务写的极其简略所以阅读完感觉还好,但是一旦复杂起来再继续使用 goto 去组织流程,我们看到的往往是一个执行路径到处跳转的业务流程,对于阅读者而言并不友好,对后续的维护者来说也是一种灾难。如果我们使用 if 去写这段业务:

int main(void)
{
    int state = 0;

    if(state == 0)
    {
        state = 1;
    }

    if(state == 1)
    {
        state = 2;
    }

    printf("done\n");

    return 0;
}

  现在看上去是不是就感觉很清晰了。但是 goto 用在异常路径处理的时候也会有意想不到的效果:

int Device_Init(void)
{
    int ret;

    ret = Clock_Init();
    if(ret != 0)
    {
        goto exit;
    }

    ret = DMA_Init();
    if(ret != 0)
    {
        goto err_dma;
    }

    ret = UART_Init();
    if(ret != 0)
    {
        goto err_uart;
    }

    return 0;

err_uart:
    DMA_DeInit();

err_dma:
    Clock_DeInit();

exit:
    return ret;
}

  正常的执行路径就是 Clock_Init -> DMA_Init -> UART_Init -> return 0,但是当进入异常路径的时候就会执行 UART_Init 失败 -> goto err_uart -> DMA_DeInit -> Clock_DeInit -> return 这套流程,这样就将异常状态流程与正常流程分的很清晰了。

(七)资源释放顺序应与申请顺序相反

  参照(六)中异常处理流程中的正常执行流程是 Clock -> DMA -> UART,这个初始化流程往往意味着后申请的资源会依赖前面已经存在的资源,比如 DMA 可能依赖 Clock,UART可能依赖 Clock、DMA 等基础资源。所以在释放的时候就必须要反过来变成 UART -> DMA -> Clock。总结下来就是先释放“上层依赖资源”,再释放“底层被依赖资源”,核心就是要理解流程之间的资源依赖关系,资源依赖关系理清了,释放顺序自然就明白了。这个规则并不是 goto 的自带规则,而是实践经验的总结,如果不按照这个顺序来,可能会造成 UART、DMA 还没完全停止时,Clock 就被关掉了,后续清理的话就可能访问已经失效的硬件资源。goto 这种申请与释放资源的关系可以类比 LIFO(Last In, First Out,后进先出)。

(八)goto 不替代循环

   循环结构本身会包含明确的控制语义,比如循环条件、循环次数、循环范围和退出条件,但 goto 只是简单地改变程序的执行位置,并不附带这些信息。下面用一段程序说明

int i = 0;

loop:

printf("%d\n", i);

i++;

if(i < 5)
{
    goto loop;
}

  这段最后执行结果虽然没有问题,但是在阅读时需要自己分析这个循环是从哪里开始的、什么时候结束以及 i 在哪里变化,不够清晰。但是如果直接使用循环结构来表达同样的意思,

for(int i = 0; i < 5; i++)
{
    printf("%d\n", i);
}

  阅读的时候就会很明确,开始、结束、i 什么时候增加一眼就能看出来。所以非特殊场景下极不推荐使用 goto 来替代循环。

posted @ 2026-08-20 20:29  。叁焑港  阅读(6)  评论(0)    收藏  举报