嵌入式中程序错误如何处理?_16 1 c users a desktop 嵌入式软件 sy2 main

typedef enum{
S_OK, //成功
S_ERROR, //失败(原因未明确),通用状态

S_NULL_POINTER, //入参指针为NULL
S_ILLEGAL_PARAM, //参数值非法,通用
S_OUT_OF_RANGE, //参数值越限
S_MAX_STATUS //不可作为返回值状态,仅作枚举最值使用
}FUNC_STATUS;

#define RC_NAME(eRetCode)
((eRetCode) == S_OK ? “Success” :
((eRetCode) == S_ERROR ? “Failure” :
((eRetCode) == S_NULL_POINTER ? “NullPointer” :
((eRetCode) == S_ILLEGAL_PARAM ? “IllegalParas” :
((eRetCode) == S_OUT_OF_RANGE ? “OutOfRange” :
“Unknown”)))))

当返回值错误码来自下游模块时,可能与本模块错误码冲突。此时,建议不要将下游错误码直接向上传递,以免引起混乱。若允许向终端或文件输出错误信息,则可详细记录出错现场(如函数名、错误描述、参数取值等),并转换为本模块定义的错误码再向上传递。

2.2 全局状态标志(errno)

Unix系统调用或某些C标准库函数出错时,通常返回一个负值,并设置全局整型变量errno为一个含有错误信息的值。例如,open函数出错时返回-1,并设置errno为EACESS(权限不足)等值。

C标准库头文件<errno.h>中定义errno及其可能的非零常量取值(以字符’E’开头)。在ANSI C中已定义一些基本的errno常量,操作系统也会扩展一部分(但其对错误描述仍显匮乏)。Linux系统中,出错常量在errno(3)手册页中列出,可通过man 3 errno命令查看。除EAGAIN和EWOULDBLOCK取值相同外,POSIX.1指定的所有出错编号取值均不同。

Posix和ISO C将errno定义为一个可修改的整型左值(lvalue),可以是包含出错编号的一个整数,或是一个返回出错编号指针的函数。以前使用的定义为:

extern int errno;

但在多线程环境中,多个线程共享进程地址空间,每个线程都有属于自己的局部errno(thread-local)以避免一个线程干扰另一个线程。例如,Linux支持多线程存取errno,将其定义为:

extern int __errno_location(void);
#define errno (
__errno_location())

函数__ errno_location在不同的库版本下有不同的定义,在单线程版本中,直接返回全局变量errno的地址;而在多线程版本中,不同线程调用__errno_location返回的地址则各不相同。

C运行库中主要在math.h(数学运算)和stdio.h(I/O操作)头文件声明的函数中使用errno。

使用errno时应注意以下几点:

  1. \1. 函数返回成功时,允许其修改errno。

例如,调用fopen函数新建文件时,内部可能会调用其他库函数检测是否存在同名文件。而用于检测文件的库函数在文件不存在时,可能会失败并设置errno。这样, fopen函数每次新建一个事先并不存在的文件时,即使没有任何程序错误发生(fopen本身成功返回),errno也仍然可能被设置。

因此,调用库函数时应先检测作为错误指示的返回值。仅当函数返回值指明出错时,才检查errno值:

//调用库函数
if(返回错误值)
//检查errno

  1. \1. 库函数返回失败时,不一定会设置errno,取决于具体的库函数。
  2. \2. errno在程序开始时设置为0,任何库函数都不会将errno再次清零。

因此,在调用可能设置errno的运行库函数之前,最好先将errno设置为0。调用失败后再检查errno的值。

  1. \1. 使用errno前,应避免调用其他可能设置errno的库函数。如:

if (somecall() == -1)
{
printf(“somecall() failed\n”);
if(errno == …) { … }
}

somecall()函数出错返回时设置errno。但当检查errno时,其值可能已被printf()函数改变。

若要正确使用somecall()函数设置的errno,须在调用printf()函数前保存其值:

if (somecall() == -1)
{
int dwErrSaved = errno;
printf(“somecall() failed\n”);
if(dwErrSaved == …) { … }
}

类似地,当在信号处理程序中调用可重入函数时,应在其前保存其后恢复errno值。

  1. \1. 使用现代版本的C库时,应包含使用<errno.h>头文件;在非常老的Unix 系统中,可能没有该头文件,此时可手工声明errno(如extern int errno)。

C标准定义strerror和perror两个函数,以帮助打印错误信息。

#include <string.h>
char *strerror(int errnum);

该函数将errnum(即errno值)映射为一个出错信息字符串,并返回指向该字符串的指针。可将出错字符串和其它信息组合输出到用户界面,或保存到日志文件中,如通过fprintf(fp, “somecall failed(%s)”, strerror(errno))将错误消息打印到fp指向的文件中。

perror函数将当前errno对应的错误消息的字符串输出到标准错误(即stderr或2)上。

#include <stdio.h>
void perror(const char *msg);

该函数首先输出由msg指向的字符串(用户自己定义的信息),后面紧跟一个冒号和空格,然后是当前errno值对应的错误类型描述,最后是一个换行符。未使用重定向时,该函数输出到控制台上;若将标准错误输出重定向到/dev/null,则看不到任何输出。

注意,perror()函数中errno对应的错误消息集合与strerror()相同。但后者可提供更多定位信息和输出方式。

两个函数的用法示例如下:

int main(int argc, char** argv)
{
errno = 0;
FILE *pFile = fopen(argv[1], “r”);
if(NULL == pFile)
{
printf(“Cannot open file ‘%s’(%s)!\n”, argv[1], strerror(errno));
perror(“Open file failed”);
}
else
{
printf(“Open file ‘%s’(%s)!\n”, argv[1], strerror(errno));
perror(“Open file”);
fclose(pFile);
}

return 0;
}

执行结果为:

[wangxiaoyuan_@localhost test1]$ ./GlbErr /sdb1/wangxiaoyuan/linux_test/test1/test.c
Open file ‘/sdb1/wangxiaoyuan/linux_test/test1/test.c’(Success)!
Open file: Success
[wangxiaoyuan_@localhost test1]$ ./GlbErr NonexistentFile.h
Cannot open file ‘NonexistentFile.h’(No such file or directory)!
Open file failed: No such file or directory
[wangxiaoyuan_@localhost test1]$ ./GlbErr NonexistentFile.h > test
Open file failed: No such file or directory
[wangxiaoyuan_@localhost test1]$ ./GlbErr NonexistentFile.h 2> test
Cannot open file ‘NonexistentFile.h’(No such file or directory)!

也可仿照errno的定义和处理,定制自己的错误代码:

int *_fpErrNo(void)
{
static int dwLocalErrNo = 0;
return &dwLocalErrNo;
}

#define ErrNo (*_fpErrNo())
#define EOUTOFRANGE 1
//define other error macros…

int Callee(void)
{
ErrNo = 1;
return -1;
}

int main(void)
{
ErrNo = 0;
if((-1 == Callee()) && (EOUTOFRANGE == ErrNo))
printf(“Callee failed(ErrNo:%d)!\n”, ErrNo);
return 0;
}

借助全局状态标志,可充分利用函数的接口(返回值和参数表)。但与返回值一样,它隐含地要求调用者在调用函数后检查该标志,而这种约束同样脆弱。

此外,全局状态标志存在重用和覆盖的风险。而函数返回值是无名的临时变量,由函数产生且只能被调用者访问。调用完成后即可检查或拷贝返回值,然后原始的返回对象将消失而不能被重用。又因为无名,返回值不能被覆盖。

2.3 局部跳转(goto)

使用goto语句可直接跳转到函数内的错误处理代码处。以除零错误为例:

double Division(double fDividend, double fDivisor)
{
return fDividend/fDivisor;
}
int main(void)
{
int dwFlag = 0;
if(1 == dwFlag)
{
RaiseException:
printf(“The divisor cannot be 0!\n”);
exit(1);
}
dwFlag = 1;

double fDividend = 0.0, fDivisor = 0.0;
printf(“Enter the dividend: “);
scanf(”%lf”, &fDividend);
printf(“Enter the divisor : “);
scanf(”%lf”, &fDivisor);
if(0 == fDivisor) //不太严谨的浮点数判0比较
goto RaiseException;
printf(“The quotient is %.2lf\n”, Division(fDividend, fDivisor));

return 0;
}

执行结果如下:

[wangxiaoyuan_@localhost test1]$ ./test
Enter the dividend: 10
Enter the divisor : 0
The divisor cannot be 0!
[wangxiaoyuan_@localhost test1]$ ./test
Enter the dividend: 10
Enter the divisor : 2
The quotient is 5.00

虽然goto语句会破坏代码结构性,但却非常适用于集中错误处理。伪代码示例如下:

CallerFunc()
{
if((ret = CalleeFunc1()) < 0);
goto ErrHandle;
if((ret = CalleeFunc2()) < 0);
goto ErrHandle;
if((ret = CalleeFunc3()) < 0);
goto ErrHandle;
//…

return;

ErrHandle:
//Handle Error(e.g. printf)
return;
}

2.4 非局部跳转(setjmp/longjmp)

局部goto语句只能跳到所在函数内部的标号上。若要跨越函数跳转,需要借助标准C库提供非局部跳转函数setjmp()和longjmp()。它们分别承担非局部标号和goto的作用,非常适用于处理发生在深层嵌套函数调用中的出错情况。“非局部跳转”是在栈上跳过若干调用帧,返回到当前函数调用路径上的某个函数内。

#include <setjmp.h>
int setjmp(jmp_buf env);
void longjmp(jmp_buf env,int val);

函数setjmp()将程序运行时的当前系统堆栈环境保存在缓冲区env结构中。初次调用该函数时返回值为0。longjmp()函数根据setjmp()所保存的env结构恢复先前的堆栈环境,即“跳回”先前调用setjmp时的程序执行点。

此时,setjmp()函数返回longjmp()函数所设置的参数val值,程序将继续执行setjmp调用后的下一条语句(仿佛从未离开setjmp)。参数val为非0值,若设置为0,则setjmp()函数返回1。

可见,setjmp()有两类返回值,用于区分是首次直接调用(返回0)和还是由其他地方跳转而来(返回非0值)。对于一个setjmp可有多个longjmp,因此可由不同的非0返回值区分这些longjmp。

举个简单例子说明 setjmp/longjmp的非局部跳转:

jmp_buf gJmpBuf;
void Func1(){
printf(“Enter Func1\n”);
if(0)longjmp(gJmpBuf, 1);
}
void Func2(){
printf(“Enter Func2\n”);
if(0)longjmp(gJmpBuf, 2);
}
void Func3(){
printf(“Enter Func3\n”);
if(1)longjmp(gJmpBuf, 3);
}

int main(void)
{
int dwJmpRet = setjmp(gJmpBuf);
printf(“dwJmpRet = %d\n”, dwJmpRet);
if(0 == dwJmpRet)
{
Func1();
Func2();
Func3();
}
else
{
switch(dwJmpRet)
{
case 1:
printf(“Jump back from Func1\n”);
break;
case 2:
printf(“Jump back from Func2\n”);
break;
case 3:
printf(“Jump back from Func3\n”);
break;
default:
printf(“Unknown Func!\n”);
break;
}
}
return 0;
}

执行结果为:

dwJmpRet = 0
Enter Func1
Enter Func2
Enter Func3
dwJmpRet = 3
Jump back from Func3

当setjmp/longjmp嵌在单个函数中使用时,可模拟PASCAL语言中嵌套函数定义(即函数内中定义一个局部函数)。当setjmp/longjmp跨越函数使用时,可模拟面向对象语言中的异常(exception) 机制。

模拟异常机制时,首先通过setjmp()函数设置一个跳转点并保存返回现场,然后使用try块包含那些可能出现错误的代码。可在try块代码中或其调用的函数内,通过longjmp()函数抛出(throw)异常。

抛出异常后,将跳回setjmp()函数所设置的跳转点并执行catch块所包含的异常处理程序。

以除零错误为例:

jmp_buf gJmpBuf;
void RaiseException(void)
{
printf("Exception is raised: ");
longjmp(gJmpBuf, 1); //throw,跳转至异常处理代码
printf(“This line should never get printed!\n”);
}
double Division(double fDividend, double fDivisor)
{
return fDividend/fDivisor;
}
int main(void)
{
double fDividend = 0.0, fDivisor = 0.0;
printf(“Enter the dividend: “);
scanf(”%lf”, &fDividend);
printf(“Enter the divisor : “);
if(0 == setjmp(gJmpBuf)) //try块
{
scanf(”%lf”, &fDivisor);
if(0 == fDivisor) //也可将该判断及RaiseException置于Division内
RaiseException();
printf(“The quotient is %.2lf\n”, Division(fDividend, fDivisor));
}
else //catch块(异常处理代码)
{
printf(“The divisor cannot be 0!\n”);
}

return 0;
}

执行结果为:

Enter the dividend: 10
Enter the divisor : 0
Exception is raised: The divisor cannot be 0!

通过组合使用setjmp/longjmp函数,可对复杂程序中可能出现的异常进行集中处理。根据longjmp()函数所传递的返回值来区分处理各种不同的异常。

使用setjmp/longjmp函数时应注意以下几点:

  1. \1. 必须先调用setjmp()函数后调用longjmp()函数,以恢复到先前被保存的程序执行点。若调用顺序相反,将导致程序的执行流变得不可预测,很容易导致程序崩溃。
  2. \2. longjmp()函数必须在setjmp()函数的作用域之内。在调用setjmp()函数时,它保存的程序执行点环境只在当前主调函数作用域以内(或以后)有效。若主调函数返回或退出到上层(或更上层)的函数环境中,则setjmp()函数所保存的程序环境也随之失效(函数返回时堆栈内存失效)。这就要求setjmp()不可该封装在一个函数中,若要封装则必须使用宏(详见《C语言接口与实现》“第4章 异常与断言”)。
  3. \3. 通常将jmp_buf变量定义为全局变量,以便跨函数调用longjmp。
  4. \4. 通常,存放在存储器中的变量将具有longjmp时的值,而在CPU和浮点寄存器中的变量则恢复为调用setjmp时的值。因此,若在调用setjmp和longjmp之间修改自动变量或寄存器变量的值,当setjmp从longjmp调用返回时,变量将维持修改后的值。若要编写使用非局部跳转的可移植程序,必须使用volatile属性。
  5. \5. 使用异常机制不必每次调用都检查一次返回值,但因为程序中任何位置都可能抛出异常,必须时刻考虑是否捕捉异常。在大型程序中,判断是否捕捉异常会是很大的思维负担,影响开发效率。

相比之下,通过返回值指示错误有利于调用者在最近出错的地方进行检查。此外,返回值模式中程序的运行顺序一目了然,对维护者可读性更高。因此,应用程序中不建议使用setjmp/longjmp“异常处理”机制(除非库或框架)。

2.5 信号(signal/raise)

在某些情况下,主机环境或操作系统可能发出信号(signal)事件,指示特定的编程错误或严重事件(如除0或中断等)。这些信号本意并非用于错误捕获,而是指示与正常程序流不协调的外部事件。

为处理信号,需要使用以下信号相关函数:

#include <signal.h>
typedef void (*fpSigFunc)(int);
fpSigFunc signal(int signo, fpSigFunc fpHandler);
int raise(int signo);

其中,参数signo是Unix系统定义的信号编号(正整数),不允许用户自定义信号。参数fpHandler是常量SIG_DFL、常量SIG_IGN或当接收到此信号后要调用的信号处理函数(signal handler)的地址。若指定SIG_DFL,则接收到此信号后调用系统的缺省处理函数;若指定SIG_ IGN,则向内核表明忽略此信号(SIGKILL和SIGSTOP不可忽略)。

某些异常信号(如除数为零)不太可能恢复,此时信号处理函数可在程序终止前正确地清理某些资源。信号处理函数所收到的异常信息仅是一个整数(待处理的信号事件),这点与setjmp()函数类似。

signal()函数执行成功时返回前次挂接的处理函数地址,失败时则返回SIG_ERR。信号通过调用raise()函数产生并被处理函数捕获。

以除零错误为例:

void fphandler(int dwSigNo)
{
printf(“Exception is raised, dwSigNo=%d!\n”, dwSigNo);
}
int main(void)
{
if(SIG_ERR == signal(SIGFPE, fphandler))
{
fprintf(stderr, “Fail to set SIGFPE handler!\n”);
exit(EXIT_FAILURE);
}

double fDividend = 10.0, fDivisor = 0.0;
if(0 == fDivisor)
{
raise(SIGFPE);
exit(EXIT_FAILURE);
}
printf(“The quotient is %.2lf\n”, fDividend/fDivisor);

return 0;
}

执行结果为"Exception is raised, dwSigNo=8!"(0.0不等同于0,因此系统未检测到浮点异常)。

若将被除数(Dividend)和除数(Divisor)改为整型变量:

int main(void)
{
if(SIG_ERR == signal(SIGFPE, fphandler))
{
fprintf(stderr, “Fail to set SIGFPE handler!\n”);
exit(EXIT_FAILURE);
}

int dwDividend = 10, dwDivisor = 0;
double fQuotient = dwDividend/dwDivisor;
printf(“The quotient is %.2lf\n”, fQuotient);

return 0;
}

则执行后循环输出"Exception is raised, dwSigNo=8!"。这是因为进程捕捉到信号并对其进行处理时,进程正在执行的指令序列被信号处理程序临时中断,它首先执行该信号处理程序中的指令。若从信号处理程序返回(未调用exit或longjmp),则继续执行在捕捉到信号时进程正在执行的正常指令序列。

因此,每次系统调用信号处理函数后,异常控制流还会返回除0指令继续执行。而除0异常不可恢复,导致反复输出异常。

规避方法有两种:

  1. \1. 将SIGFPE信号变成系统默认处理,即signal(SIGFPE, SIG_DFL)。

此时执行输出为"Floating point exception"。

  1. \1. 利用setjmp/longjmp跳过引发异常的指令:

jmp_buf gJmpBuf;
void fphandler(int dwSigNo)
{
printf(“Exception is raised, dwSigNo=%d!\n”, dwSigNo);
longjmp(gJmpBuf, 1);
}
int main(void)
{
if(SIG_ERR == signal(SIGFPE, SIG_DFL))
{
fprintf(stderr, “Fail to set SIGFPE handler!\n”);
exit(EXIT_FAILURE);
}

int dwDividend = 10, dwDivisor = 0;
if(0 == setjmp(gJmpBuf))
{
double fQuotient = dwDividend/dwDivisor;
printf(“The quotient is %.2lf\n”, fQuotient);
}
else
{
printf(“The divisor cannot be 0!\n”);
}

return 0;
}

注意,在信号处理程序中还可使用sigsetjmp/siglongjmp函数进行非局部跳转。相比setjmp函数,sigsetjmp函数增加一个信号屏蔽字参数。

三 错误处理

3.1 终止(abort/exit)

致命性错误无法恢复,只能终止程序。例如,当空闲堆管理程序无法提供可用的连续空间时(调用malloc返回NULL),用户程序的健壮性将严重受损。若恢复的可能性渺茫,则最好终止或重启程序。

标准C库提供exit()和abort()函数,分别用于程序正常终止和异常终止。两者都不会返回到调用者中,且都导致程序被强行结束。

exit()及其相似函数原型声明如下:

#include <stdlib.h>
void exit(int status);
void _Exit(int status);
#include <unistd.h>
void _exit(int status);

其中,exit和_Exit由ISO C说明,而_exit由Posix.1说明。因此使用不同的头文件。

ISO C定义_ Exit旨在为进程提供一种无需运行终止处理程序(exit handler)或信号处理程序(signal handler)而终止的方法,是否冲洗标准I/O流则取决于实现。Unix系统中_ Exit 和_ exit同义,两者均直接进入内核,而不冲洗标准I/O流。_exit函数由exit调用,处理Unix特定的细节。

exit()函数首先调用执行各终止处理程序,然后按需多次调用fclose函数关闭所有已打开的标准I/O流(将所有缓冲的输出数据冲洗写到文件上),然后调用_exit函数进入内核。

标准函数库中有一种“缓冲I/O(buffered I/O)”机制。该机制对于每个打开的文件,在内存中维护一片缓冲区。每次读文件时会连续读出若干条记录,下次读文件时就可直接从内存缓冲区中读取;每次写文件时也仅仅写入内存缓冲区,等满足一定条件(如缓冲区填满,或遇到换行符等特定字符)时再将缓冲区内容一次性写入文件。

通过尽可能减少read和write调用的次数,该机制可显著提高文件读写速度,但也给编程带来某些麻烦。例如,向文件内写入一些数据时,若未满足特定条件,数据会暂存在缓冲区内。开发者并不知晓这点,而调用_ _ exit()函数直接关闭进程,导致缓冲区数据丢失。

因此,若要保证数据完整性,必须调用exit()函数,或在调用_ _ exit()函数前先通过fflush()函数将缓冲区内容写入指定的文件。

例如,调用printf函数(遇到换行符’\n’时自动读出缓冲区中内容)函数后再调用exit:

int main(void)
{
printf(“Using exit…\n”);
printf(“This is the content in buffer”);
exit(0);
printf(“This line will never be reached\n”);
}

执行输出为:

Using exit…
This is the content in buffer(结尾无换行符)

调用printf函数后再调用_exit:

int main(void)
{
printf(“Using _exit…\n”);
printf(“This is the content in buffer”);
fprintf(stdout, “Standard output stream”);
fprintf(stderr, “Standard error stream”);
//fflush(stdout);
_exit(0);
}

执行输出为:

Using _exit…
Standard error stream(结尾无换行符)

若取消fflush句注释,则执行输出为:

Using _exit…
Standard error streamThis is the content in bufferStandard output stream(结尾无换行符)

通常,标准错误是不带缓冲的,打开至终端设备的流(如标准输入和标准输出)是行缓冲的(遇换行符则执行I/O操作);其他所有流则是全缓冲的(填满标准I/O缓冲区后才执行I/O操作)。

三个exit函数都带有一个整型参数status,称之为终止状态(或退出状态)。该参数取值通常为两个宏,即EXIT_SUCCESS(0)和EXIT_FAILURE(1)。大多数Unix shell都可检查进程的终止状态。

若(a)调用这些函数时不带终止状态,或(b)main函数执行了无返回值的return语句,或© main函数未声明返回类型为整型,则该进程的终止状态未定义。但若main函数的返回类型为整型,且执行到最后一条语句时返回(隐式返回),则该进程的终止状态为0。

exit系列函数是最简单直接的错误处理方式,但程序出错终止时无法捕获异常信息。ISO C规定一个进程可以注册32个终止处理函数。这些函数可编写为自定义的清理代码,将由exit()函数自动调用,并可使用atexit()函数进行注册。

#include <stdlib.h>
int atexit(void (*func)(void));

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Go语言工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Go语言全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
img
img
img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Golang知识点,真正体系化!

由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新

如果你觉得这些内容对你有帮助,可以添加V获取:vip1024b (备注Go)
img

一个人可以走的很快,但一群人才能走的更远。不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎扫码加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

8年进入阿里一直到现在。**

深知大多数Go语言工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Go语言全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
[外链图片转存中…(img-Mqmh8Mq8-1713027933509)]
[外链图片转存中…(img-Eb2GxaiR-1713027933511)]
[外链图片转存中…(img-NgTIzUxw-1713027933512)]
[外链图片转存中…(img-u3dRh06K-1713027933512)]
[外链图片转存中…(img-xYaWna3C-1713027933513)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Golang知识点,真正体系化!

由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新

如果你觉得这些内容对你有帮助,可以添加V获取:vip1024b (备注Go)
[外链图片转存中…(img-Wcqn6fpd-1713027933513)]

一个人可以走的很快,但一群人才能走的更远。不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎扫码加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

  • 18
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值