异常处理
1 概述
本章节主要介绍了 GS 语言中异常处理的基本概念和使用方法。通过系统学习,您将掌握如何使用 try-catch语句块来捕获和处理程序运行时的各种错误,了解 error函数抛出错误方式,以及异常处理的底层机制和最佳实践 。
本章节面相初次学习GS异常处理的同学或需要简单回顾GS异常处理相关内容的同学。
阅读完本章后,应当能够通过异常处理机制编写更加健壮和可靠的代码,有效处理程序运行过程中可能出现的各类异常情况,提升程序的稳定性和可靠性。
2 异常处理概念
在GS程序的运行过程中,难免会出现一些非预期的情况,例如:打开一个不存在的文件、网络连接突然中断、传入函数的参数无效等。这些非预期的情况统称为异常或错误。
如果不对错误进行处理, 会打断我们的正常运行的逻辑,一路向上传递,直到第一个捕获的地方,或者到整个协程的入口为止。
-
抛出异常:当GS程序遇到无法处理的情况时,它通过抛出异常来向上层发送一个信号,表明遇到了非常规情况。
-
捕获异常:GS上层代码通过异常捕获机制来接收并处理这些信号,根据消息内容决定后续操作。
-
未捕获异常的全局处理:如果某个异常没有被显式捕获,它不会导致GS程序崩溃,而是会被传递到最外层的捕获位置,或者到整个协程的入口为止。
3 异常抛出
当GS中程序或函数遇到自身无法处理的情况时,它可以抛出一个错误,将问题上报。在GS语言中有多种方式可以抛出异常:
3.1 使用 error 函数
可以调用error(string format,...)函数显示的抛出错误并且指定错误信息,示例如下:
import gs.util.file;
// 抛出一个简单的错误信息
void create_a_error()
{
string file_name = "config_not_exits.json";
if(!file.exist(file_name))
error("File:'%s' does not exist", file_name);
}
create_a_error();
示例 3-1:使用error函数抛出错误示例
输出结果如下:
Error(-1): File:'config_not_exits.json' does not exist
Coroutine: co[440664:v0]boot
Domain: domain[2048:v0]zero [local call]
At /test/test.gs:7 (create_a_error) in object[2816337:v0]/test/test.gs
Variable file_name = (optimized)
Domain: domain[2048:v0]zero [local call]
At /test/test.gs:9 (::entry) in object[2816337:v0]/test/test.gs
At <Internal routine>
Error occurs when booting...
Error(-10009): Construct object object[2816337:v0](/test/test.gs) failed: File:'config_not_exits.json' does not exist
Coroutine: co[440664:v0]boot
At <Internal routine>
从示例的报错结果可以看到报错信息为 File:'config.json' does not exist。除此之外GS的报错信息中还有错误信息码Error(-1),调用栈信息,包含函数调用路径,代码执行位置,变量信息、协程信息、函数所在域等。
从上述的调用栈信息中我们可以看到我们的代码写在test.gs文件中,在::entry函数test.gs文件中第九行调用了下一层的create_a_error函数,并在test.gs文件中第七行抛出了错误。
3.2 使用 assert 断言
可以调用assert(condition) 或 assert(condition, format, ...)函数来检查条件并在条件为假 的情况下抛出错误,示例如下:
import gs.util.file;
// 抛出一个简单的错误信息
void assert_check_error()
{
string file_name = "config_not_exits.json";
assert(file.exist(file_name), "File:'%s' does not exist", file_name);
}
assert_check_error();
示例3-2:使用assert检查条件并在条件不成立时抛出错误
输出结果如下:
Error(-1): Assert Fail - File:'config_not_exits.json' does not exist.
Coroutine: co[440664:v0]boot
Domain: domain[2048:v0]zero [local call]
At unknown:0 (assert) in object[2877009:v0]/test/test.gs
Argument val = false
Argument $2 = "File:'%s' does not exist"
Argument $3 = "config_not_exits.json"
Domain: domain[2048:v0]zero [local call]
At /test/test.gs:6 (assert_check_error) in object[2877009:v0]/test/test.gs
Variable file_name = (optimized)
Domain: domain[2048:v0]zero [local call]
At /test/test.gs:8 (::entry) in object[2877009:v0]/test/test.gs
At <Internal routine>
Error occurs when booting...
Error(-10009): Construct object object[2877009:v0](/test/test.gs) failed: Assert Fail - File:'config_not_exits.json' does not exist.
Coroutine: co[440664:v0]boot
At <Internal routine>
在条件不满足的情况下错误被抛出,assert函数抛出的错误带有Assert Fail前缀字符串。
此处报错的调用栈信息中额外多了一层At unknow:0,该层实际是进入了assert的efun中。
Efun,是 External Function(外部函数)的缩写。它指的是GS内置提供的函数。这些函数是GS核心的一部分,用底层语言 C++ 实现,并直接暴露给上层脚本语言使用
3.3 使用 throw 函数
若要手动指定具体的错误信息码,则可以使用throw(int error_code,string format,...)来实现错误的抛出,如下示例所示:
import gs.util.file;
// 抛出一个简单的错误信息
void assert_check_error()
{
string file_name = "config_not_exits.json";
if(!file.exist(file_name))
throw(ErrorCode.IO_CAN_NOT_OPEN, "File:'%s' does not exist", file_name);
}
assert_check_error();
示例3-3:使用 throw 手动指定错误码
示例输出的结果如下:
Error(-3003): File:'config_not_exits.json' does not exist
Coroutine: co[440664:v0]boot
Domain: domain[2048:v0]zero [local call]
At unknown:0 (throw) in object[2817873:v0]/test/test.gs
Argument err_code = -3003
Argument format = "File:'%s' does not exist"
Argument $3 = "config_not_exits.json"
Domain: domain[2048:v0]zero [local call]
At /test/test.gs:7 (throw_a_error) in object[2817873:v0]/test/test.gs
Variable file_name = (optimized)
Domain: domain[2048:v0]zero [local call]
At /test/test.gs:9 (::entry) in object[2817873:v0]/test/test.gs
At <Internal routine>
Error occurs when booting...
Error(-10009): Construct object object[2817873:v0](/test/test.gs) failed: File:'config_not_exits.json' does not exist
Coroutine: co[440664:v0]boot
At <Internal routine>
从输出中首行接可以看到可以看到,报错的信息码由 ErrorCode.UNKNOWN_ERROR(-1)变为了Error(-3003)对应为ErrorCode.IO_CAN_NOT_OPEN。
在 driver shell中输入 man ErrorCode 即可查看错误码与数字的对应关系。
3.4 非法的操作
一些非法操作也会抛出错误,此时报错信息中除了有调用栈外还有非法操作的相关信息,一个很简单的示例如下:
void error_when_divide_zero(int arg = 0)
{
// 除零错误
int result = 10 / arg; // 自动抛出算术异常
write(result);
}
error_when_divide_zero();
示例3-4:非法操作导致异常的示例
输出的结果如下:
Error(-1): Integer division by zero
Coroutine: co[440664:v0]boot
Domain: domain[2048:v0]zero [local call]
At /test/test.gs:4 (error_when_divide_zero) in object[2816337:v0]/test/test.gs
Argument arg = 0
Variable result = nil
Domain: domain[2048:v0]zero [local call]
At /test/test.gs:7 (::entry) in object[2816337:v0]/test/test.gs
At <Internal routine>
Error occurs when booting...
Error(-10009): Construct object object[2816337:v0](/test/test.gs) failed: Integer division by zero
Coroutine: co[440664:v0]boot
At <Internal routine>
可以看到报错信息中明确表示了具体的非法操作为Error(-1): Integer division by zero ,可以很方便的查找到错误的位置及原因。
4 try-catch
我们手动调用一个error("err occurs")抛出一个错误,或者assert(false)产生断言,或者出现一个整数除0的算数运算,底层都会报错来触发异常。
如果错误不去捕获,会向上传递直到被捕获,或者被GS的全局隐含catch处理,确保程序不会崩溃。若我们通过 catch或 try-catch语句手动捕获并处理异常,代码就可以在异常被捕获及处理后正常继续执行。
GS中有两种捕获异常的方式:
4.1 catch 语句
直接使用catch是最简单的捕获但是不处理的方式,支持两种方式:
catch(表达式);catch { 代码块 }
int divide(int a, int b)
{
return a/b;
}
// 1. 捕获表达式可能抛出的错误
catch(error("xxx"));
// 2. 捕获代码块中的错误
catch {
writeln(divide(100, 0)); // 触发除0异常
}
writeln(HIG"Test output after catch."NOR);
示例4-1:使用catch语句捕获异常
输出结果如下:
Error(-1): xxx
Coroutine: co[440664:v0]boot
Domain: domain[2048:v0]zero [local call]
At /test/test.gs:7 (::entry) in object[2877009:v0]/test/test.gs
Variable $catch_ret_0 = nil
At <Internal routine>
Error(-1): Integer division by zero
Coroutine: co[440664:v0]boot
Domain: domain[2048:v0]zero [local call]
At /test/test.gs:3 (divide) in object[2877009:v0]/test/test.gs
Argument a(optimized) = "xxx"
Argument b(optimized) = { /* sizeof() == 2 */
"error" : -1,
"msg" : "xxx",
}
Domain: domain[2048:v0]zero [local call]
At /test/test.gs:11 (::entry) in object[2877009:v0]/test/test.gs
Variable $catch_ret_0 = { /* sizeof() == 2 */
"error" : -1,
"msg" : "xxx",
}
At <Internal routine>
Test output after catch.
从输出结果可以看到尽管 error或除以0非法操作出发了错误的抛出过程,但在被catch语句捕获抛出的异常后,后续的输出测试语句正常执行且输出了绿色的 Test output after catch.
4.2 try-catch 语句
若要在异常发生时,在捕获异常的基础上添加额外的异常处理逻辑可以使用try-catch语句,try catch的方式和C++类似,基本语法如下:
// 不指明ex的简单捕获
try
{
... //可能导致错误抛出的代码
}
catch
{
...//异常处理逻辑
}
// 指明ex,可以获得错误信息
try
{
... //可能导致错误抛出的代码
}
catch(ex) //在此处 GS 会将异常信息(错误码与错误描述) 放入ex 表变量中
{
...//异常处理逻辑
}
具体的示例代码如下:
// 不捕获异常对象的简单形式
try
{
error("hello");
}
catch
{
printf("Exception handling here!!!!!!!!!!!!!\n");
}
// 捕获异常对象,可以获取错误信息
try
{
error("hello");
}
catch(ex)
{
printf("Exception information: %O!!!!!!!!!!!!!\n", ex);
// 可以在这里进行错误恢复、重试、记录日志等操作
}
示例4-2:try-catch 捕获并处理异常
示例的输出如下:
Error(-1): hello
Coroutine: co[440664:v0]boot
Domain: domain[2048:v0]zero [local call]
At /test/test.gs:4 (::entry) in object[2877009:v0]/test/test.gs
At <Internal routine>
Exception handling here!!!!!!!!!!!!!
Error(-1): hello
Coroutine: co[440664:v0]boot
Domain: domain[2048:v0]zero [local call]
At /test/test.gs:14 (::entry) in object[2877009:v0]/test/test.gs
At <Internal routine>
Exception information: { /* sizeof() == 2 */
"error" : -1,
"msg" : "hello",
}!!!!!!!!!!!!!
从上出示例可以看到,异常首先被抛出捕获后,catch代码块中的异常处理逻辑才会执行。
可以尝试修改代码将error语句移除,此时再次运行就会发现catch代码块中的异常处理流程不再会被除法执行。
5 陷阱与调试
5.1 避免滥用
不应使用异常处理来代替正确的条件判断,示例如下:
array arr = array.allocate(10);
mixed value;
mixed default_value = 1;
// 不好的做法:用异常控制正常流程
try
{
value = arr[100]; // 可能越界
}
catch
{
value = default_value;
}
writeln(HIG,value,NOR);
// 好的做法:提前检查
if (100 < arr.length())
{
value = arr[100];
}
else
{
value = default_value;
}
writeln(HIG,value,NOR);
示例5-1:异常滥用示例
如上示例中数组的长度应提前进行逻辑判断,若索引超出数组长度范围赋默认值。而不是通过异常处理语句在catch代码块中处理。滥用异常处理会导致以下问题:
-
滥用异常处理会导致大量的非必要报错输出
-
滥用异常处理会导致程序运行效率显著降低
5.2 调试
通过 gslang 调试工具可以很直观的查看异常捕获过程以及异常处理流程中的变量内容,示例如下:
// 简单的调试示例
void main() {
write("=== Start test ===\n");
mixed data = []; //创建一个空数组数据
try
{
mixed val = data[0]; //访问超过范围的数组索引
writeln(val);
write("Mission complete!\n");
}
catch (ex)
{
writeln("\n[Erroe message] Got it");
writeln("Erroe info: ", ex); // 输出异常信息
writeln("Error type: " + ex.type_name()); // 输出异常数据名称 map
}
write("Keep running...\n");
}
main();
示例5-2:try-catch 调试示例代码
调试过程如下:

6 总结
本章节系统介绍了 GS 语言中的异常处理机制,涵盖了异常的基本概念,异常的抛出方式,异常的捕获处理,异常的陷阱与调试等内容。
异常处理是构建健壮、可靠应用程序的重要基础。通过合理的异常处理,GS 程序能够及时的应对各种意外情况,保证核心业务的连续性和稳定性。
异常处理机制通常需要与其他资源管理技术配合使用,以确保在发生异常时资源能够得到正确释放。在下一章节中,我们将学习 defer 语句——一种强大的资源管理工具,它能够与异常处理完美配合,确保无论是否发生异常,资源清理代码都能得到执行,进一步提升了代码的健壮性和可维护性。
掌握异常处理与 defer 语句的结合使用,将帮助您编写出更加安全、可靠的 GS 程序。