- VisualStudio2022插件的安装及使用-编程手把手系列文章
- pprof-在现网场景怎么用
- C#实现的下拉多选框,下拉多选树,多级节点
- 【学习笔记】基础数据结构:猫树
大家有没有遇到过,代码跑着跑着,线程突然抢资源抢疯了?其实,这都是“多线程同步”在作怪。多线程同步是个老生常谈的话题,可每次真正要处理时还是让人头疼。这篇文章,带你从头到尾掌握 Linux 的多线程同步,把概念讲成大白话,让你看了不再迷糊,还能拿出来装一装逼!不管是“锁”、“信号量”,还是“条件变量”,我们都一网打尽,赶紧点赞收藏,一文搞懂! 。
线程同步的核心,就是控制多个线程的访问顺序,让它们在访问共享资源时有序、稳定。你可以把它想象成大家排队进电影院,每个线程都是观众,排好队才能有序进场。如果大家一拥而上,不仅容易出事,还谁也看不成电影.
简单来说,线程同步就是一个“排队工具”,让线程们按顺序、按规则去操作资源,避免混乱、出错.
简单来说,多线程同步就是为了控制多个线程之间的访问顺序,保证数据的一致性,防止线程“打架”。 比如你有多个线程在“抢”同一个变量,它们随时会互相影响,最终导致程序结果错得一塌糊涂,甚至程序崩溃。这时候就像几个朋友围在一桌,大家都想夹最后一块肉,结果谁也夹不到,甚至还打起来了!在计算机中,这个场景会导致资源冲突或者死锁.
为什么多线程容易“打架”?因为线程是独立的执行单元,它们的执行顺序不确定。几个常见的问题:
所以,为了保证程序的正确性、数据一致性,Linux 提供了各种同步工具。可以理解为“排队工具”,让线程一个一个地来,用完再走,大家和平共处.
在 Linux 中,常用的同步工具主要有七类:
这些工具看起来好像有点复杂,但咱们一个一个来,保你一学就懂! 。
互斥锁是多线程同步的基础。顾名思义,互斥锁(mutex)是一种独占机制,即一次只允许一个线程访问共享资源。要理解互斥锁的作用,可以想象一下“厕所上锁”的场景:假设家里有一个卫生间,进门时必须锁上,完事出来再开锁,以防别人误闯.
在 POSIX 线程库中,互斥锁通过 pthread_mutex_t 类型实现,提供了以下常见接口:
pthread_mutex_init(&mutex, nullptr)
:初始化互斥锁pthread_mutex_lock(&mutex)
:加锁,若已被其他线程锁定,则阻塞等待pthread_mutex_trylock(&mutex)
:尝试加锁,若锁已被占用,则立即返回错误而不阻塞pthread_mutex_unlock(&mutex)
:解锁,释放互斥锁,允许其他线程加锁pthread_mutex_destroy(&mutex)
:销毁互斥锁,释放相关资源这段代码展示了如何使用互斥锁(mutex)来确保多个线程对共享变量 counter 的安全访问.
#include <pthread.h>
#include <iostream>
pthread_mutex_t mutex; // 声明互斥锁
int counter = 0;
void* increment(void* arg) {
pthread_mutex_lock(&mutex); // 加锁
counter++;
pthread_mutex_unlock(&mutex); // 解锁
return nullptr;
}
int main() {
pthread_t t1, t2;
pthread_mutex_init(&mutex, nullptr); // 初始化互斥锁
pthread_create(&t1, nullptr, increment, nullptr);
pthread_create(&t2, nullptr, increment, nullptr);
pthread_join(t1, nullptr);
pthread_join(t2, nullptr);
std::cout << "Final counter value: " << counter << std::endl;
pthread_mutex_destroy(&mutex); // 销毁互斥锁
return 0;
}
increment 函数:每个线程调用此函数,对 counter 变量进行加 1 操作。为了防止多个线程同时修改 counter,使用了互斥锁:
pthread_mutex_lock(&mutex)
:加锁,确保只有一个线程可以修改 counter
counter++
:增加 counter
的值pthread_mutex_unlock(&mutex)
:解锁,允许其他线程访问主函数 main:
pthread_mutex_init(&mutex, nullptr)
:初始化互斥锁increment
函数pthread_join
等待 t1 和 t2 结束counter
的最终值pthread_mutex_destroy(&mutex)
:销毁互斥锁,释放资源通过互斥锁的加锁和解锁,代码确保了两个线程不会同时修改 counter,从而保证数据安全.
优点:
缺点:
互斥锁适合那些需要独占资源访问的情况,比如多个线程同时需要修改同一个变量、更新配置文件、写文件等操作。互斥锁确保这些操作不会被打断,资源在操作时“锁”住,保证访问的有序和安全性.
条件变量有点像“等通知”。一个线程负责等信号,另一个线程发出信号。比如生产者和消费者,消费者要等到有货了才能继续;生产者一旦备好了货,就发个信号给消费者,“来吧,过来取,货到齐了!” 。
在 POSIX 线程库中,条件变量通过 pthread_cond_t 类型实现,配合互斥锁使用,常见接口包括以下几种:
pthread_cond_init(&cond, nullptr)
:初始化条件变量。pthread_cond_wait(&cond, &mutex)
:等待条件变量。需要持有互斥锁,当条件不满足时自动释放锁并进入等待状态,直到接收到信号或被唤醒。pthread_cond_signal(&cond)
:发送信号,唤醒一个正在等待的线程。适用于通知单个等待线程的情况。pthread_cond_broadcast(&cond)
:广播信号,唤醒所有正在等待的线程。pthread_cond_destroy(&cond)
:销毁条件变量,释放相关资源。这段代码展示了如何使用 条件变量(Condition Variable) 和 互斥锁(Mutex) 来协调两个线程之间的同步。代码中有两个线程,一个线程在等待信号,另一个线程发送信号.
#include <pthread.h>
#include <iostream>
pthread_mutex_t mutex; // 声明互斥锁
pthread_cond_t cond; // 声明条件变量
bool ready = false;
void* waitForSignal(void* arg) {
pthread_mutex_lock(&mutex);
while (!ready) {
pthread_cond_wait(&cond, &mutex); // 等待条件变量的信号
}
std::cout << "Signal received!" << std::endl;
pthread_mutex_unlock(&mutex);
return nullptr;
}
void* sendSignal(void* arg) {
pthread_mutex_lock(&mutex);
ready = true;
pthread_cond_signal(&cond); // 发送信号
pthread_mutex_unlock(&mutex);
return nullptr;
}
int main() {
pthread_t t1, t2;
pthread_mutex_init(&mutex, nullptr); // 初始化互斥锁
pthread_cond_init(&cond, nullptr); // 初始化条件变量
pthread_create(&t1, nullptr, waitForSignal, nullptr);
pthread_create(&t2, nullptr, sendSignal, nullptr);
pthread_join(t1, nullptr);
pthread_join(t2, nullptr);
pthread_mutex_destroy(&mutex); // 销毁条件变量
pthread_cond_destroy(&cond); // 销毁条件变量
return 0;
}
waitForSignal
函数:等待信号的线程,加锁后检查ready
的状态。如果ready
为false
,线程会调用pthread_cond_wait
进入等待状态,直到收到sendSignal
线程的信号才继续执行。sendSignal
函数:发送信号的线程,先加锁,将ready
设为true
,然后调用pthread_cond_signal
通知等待线程可以继续。最后解锁,让waitForSignal
线程继续执行。main
: 初始化互斥锁和条件变量,创建两个线程t1
和t2
,分别执行等待和发送信号的任务,最后等待线程完成并销毁互斥锁和条件变量。优点:
缺点:
pthread_cond_wait
可能会出现“虚假唤醒”情况,因此需要在循环中反复检查条件是否满足。条件变量适用于生产者-消费者模型等场景,非常适合一个线程需要等待另一个线程完成某些操作的情况,比如等待任务完成、资源释放、数据处理等。通过条件变量,一个线程可以在等待条件达成时自动暂停,等收到信号后再继续执行.
信号量就像门口的限流器。允许一定数量的线程同时进入“临界区”(共享资源区),超过这个数量的线程就得在门口等着。比如限量版奶茶店,一次只能进五个人,想喝就得排队! 。
在 POSIX 线程库中,信号量通过 sem_t 类型实现,接口主要包括:
sem_init(&semaphore, 0, count)
:初始化信号量,count
是信号量初始值,表示同时允许进入的线程数量。sem_wait(&semaphore)
:请求资源。当信号量大于零时,减一并进入临界区;若信号量为零,则线程阻塞,直到其他线程释放资源。sem_post(&semaphore)
:释放资源,增加信号量值,允许其他等待的线程继续。sem_destroy(&semaphore)
:销毁信号量,释放资源。下面的代码展示了如何使用信号量来控制多个线程对资源的访问权限。在这个例子中,信号量初始值为 1,确保同一时间只有一个线程能进入临界区.
#include <semaphore.h>
#include <pthread.h>
#include <iostream>
sem_t semaphore;
void* accessResource(void* arg) {
sem_wait(&semaphore); // 请求资源
std::cout << "Thread accessing resource!" << std::endl;
sem_post(&semaphore); // 释放资源
return nullptr;
}
int main() {
pthread_t t1, t2;
sem_init(&semaphore, 0, 1); // 初始化信号量,允许1个线程访问资源
pthread_create(&t1, nullptr, accessResource, nullptr);
pthread_create(&t2, nullptr, accessResource, nullptr);
pthread_join(t1, nullptr);
pthread_join(t2, nullptr);
sem_destroy(&semaphore); // 销毁信号量
return 0;
}
sem_wait(&semaphore);
:请求访问资源,信号量减一。如果信号量为零,线程将等待。sem_post(&semaphore);
:释放资源,信号量加一,让其他等待的线程可以进入。主函数中,两个线程 t1 和 t2 会分别调用 accessResource。信号量初始值设为 1,保证同一时刻只有一个线程访问资源,避免冲突.
优点:
缺点:
信号量在限制并发时非常实用,能够灵活控制线程数量,特别适合一些读写分离或限流场景,是多线程同步中的“好帮手”.
读写锁的作用顾名思义,就是让“读”操作更轻松。在多线程场景中,多个线程可以同时读取资源(共享查看),但写操作必须独占,确保不会在读取时被其他线程修改内容。这就像图书馆的书,大家可以一起看,但如果有人要修改书的内容,就得把书借走,防止其他人读到一半内容突然变了.
pthread_rwlock_init(&rwlock, nullptr)
:初始化读写锁。在使用读写锁之前必须初始化,可以选择设置锁的属性(用 nullptr
表示默认属性)。pthread_rwlock_rdlock(&rwlock)
:加读锁。多个线程可以同时持有读锁,但如果有线程持有写锁,调用线程会被阻塞,直到写锁释放。pthread_rwlock_wrlock(&rwlock)
:加写锁。加写锁时,线程需独占读写锁。持有写锁期间,所有其他的读锁或写锁请求都会被阻塞,直到写锁被释放。pthread_rwlock_unlock(&rwlock)
:解锁。无论是读锁还是写锁,都可以使用该接口解锁。若当前持有读锁,则释放一个读锁;若持有写锁,则释放写锁,允许其他线程加锁。pthread_rwlock_destroy(&rwlock)
:销毁读写锁。在不再需要使用读写锁时销毁它,释放相关的资源。这段代码展示了读写锁(rwlock)的基本用法,目的是让多个线程同时访问共享变量 counter,并确保读取和写入操作的安全性.
#include <pthread.h>
#include <iostream>
pthread_rwlock_t rwlock; // 声明读写锁
int counter = 0;
void* readCounter(void* arg) {
pthread_rwlock_rdlock(&rwlock); // 加读锁
std::cout << "Counter: " << counter << std::endl;
pthread_rwlock_unlock(&rwlock); // 解锁
return nullptr;
}
void* writeCounter(void* arg) {
pthread_rwlock_wrlock(&rwlock); // 加写锁
counter++;
pthread_rwlock_unlock(&rwlock); // 解锁
return nullptr;
}
int main() {
pthread_t t1, t2, t3;
pthread_rwlock_init(&rwlock, nullptr); // 初始化读写锁
pthread_create(&t1, nullptr, readCounter, nullptr);
pthread_create(&t2, nullptr, writeCounter, nullptr);
pthread_create(&t3, nullptr, readCounter, nullptr);
pthread_join(t1, nullptr);
pthread_join(t2, nullptr);
pthread_join(t3, nullptr);
pthread_rwlock_destroy(&rwlock); // 销毁读写锁
return 0;
}
readCounter
函数:获取读锁 pthread_rwlock_rdlock
,读取 counter
的值并打印,然后释放读锁。多个线程可以同时获取读锁,允许并发读取。writeCounter
函数:获取写锁 pthread_rwlock_wrlock
,增加 counter
的值,然后释放写锁。写锁是独占的,同一时间只有一个线程可以写入 counter
。main
函数:创建了三个线程 t1
、t2
和 t3
,两个线程进行读取操作(readCounter
),一个线程进行写入操作(writeCounter
)。读写锁 rwlock
确保了读取和写入时的线程安全。优点:
缺点:
自旋锁是种“忙等”锁,不获取到锁,它就原地打转,一直“自旋”等待。自旋锁适合短时间加锁的场景,时间一长就耗CPU了,所以常用于等待时间极短的资源。因此,自旋锁经常用于等待时间非常短的资源访问场景.
pthread_spin_init(pthread_spinlock_t* lock, int pshared)
pshared
指定自旋锁是否在进程间共享(0表示仅在进程内使用)。如果成功返回0,否则返回错误代码。pthread_spin_lock(pthread_spinlock_t* lock)
pthread_spin_unlock(pthread_spinlock_t* lock)
pthread_spin_destroy(pthread_spinlock_t* lock)
下面的代码展示了如何使用自旋锁在两个线程间进行资源访问控制,确保 counter 的安全递增.
#include <pthread.h>
#include <iostream>
pthread_spinlock_t spinlock; // 声明自旋锁
int counter = 0;
void* increment(void* arg) {
pthread_spin_lock(&spinlock); // 加锁
counter++;
pthread_spin_unlock(&spinlock); // 解锁
return nullptr;
}
int main() {
pthread_t t1, t2;
pthread_spin_init(&spinlock, 0); // 初始化自旋锁
// 创建两个线程,分别执行 increment 函数
pthread_create(&t1, nullptr, increment, nullptr);
pthread_create(&t2, nullptr, increment, nullptr);
// 等待两个线程执行完毕
pthread_join(t1, nullptr);
pthread_join(t2, nullptr);
std::cout << "Final counter value: " << counter << std::endl;
pthread_spin_destroy(&spinlock); // 销毁自旋锁
return 0;
}
increment 函数: 每个线程调用此函数,对counter进行加1操作。为了确保线程安全,使用了自旋锁spinlock:
pthread_spin_lock(&spinlock)
:加锁,使当前线程独占访问counter
。counter++
:增加counter
的值。pthread_spin_unlock(&spinlock)
:解锁,让其他线程可以访问counter
。主函数 main:
pthread_create(&t1, nullptr, increment, nullptr)
和 pthread_create(&t2, nullptr, increment, nullptr)
:创建两个线程t1
和t2
,分别执行increment
函数。pthread_join(t1, nullptr)
和 pthread_join(t2, nullptr)
:等待t1
和t2
执行完毕。通过自旋锁,这段代码确保了两个线程不会同时修改counter,保证了数据安全.
优点:
缺点:
适合短时、高频锁的情况:在多核 CPU 上,自旋锁非常适合那些“锁定时间极短但加锁频繁”的情况,比如快速更新某个标志位、计数器等。这种操作速度快、锁的持有时间短,因此用自旋锁可以减少阻塞带来的上下文切换开销.
屏障的作用是让一组线程都到达某个集合点,然后再一起继续。可以把它看作一个“集合点”,每个线程到这儿后必须等一等,直到所有线程都到齐,然后才能一起“放行”。这在需要同步的多线程任务中特别有用,比如并行的数据处理:每一阶段的数据处理需要多个线程完成,各自到达指定点后,才能一起进入下一个阶段.
在 POSIX 线程库中,屏障通过 pthread_barrier_t 类型实现,常用接口包括以下几个:
pthread_barrier_destroy(&barrier)
:销毁屏障,释放资源,通常在程序结束时调用。pthread_barrier_init(pthread_barrier_t* barrier, const pthread_barrierattr_t* attr, unsigned count)
:初始化屏障,count
参数指定屏障需要等待的线程数量。到达count
个线程后,屏障会放行所有等待的线程。pthread_barrier_wait(pthread_barrier_t* barrier)
:线程调用此函数后进入等待状态,直到所有线程都调用了这个函数,屏障才会释放线程进入下一步操作。pthread_barrier_destroy(pthread_barrier_t* barrier)
:销毁屏障,释放相关资源。下面的代码展示了如何使用屏障让三个线程同步等待,等到三个线程都到达屏障点后再继续执行。这样可以确保每个线程都在同一个步骤上同步.
#include <pthread.h>
#include <iostream>
pthread_barrier_t barrier; // 声明屏障
void* waitAtBarrier(void* arg) {
std::cout << "Thread waiting at barrier..." << std::endl;
pthread_barrier_wait(&barrier); // 等待屏障
std::cout << "Thread passed the barrier!" << std::endl;
return nullptr;
}
int main() {
pthread_t t1, t2, t3;
// 初始化屏障,3个线程需要同步
pthread_barrier_init(&barrier, nullptr, 3);
// 创建线程
pthread_create(&t1, nullptr, waitAtBarrier, nullptr);
pthread_create(&t2, nullptr, waitAtBarrier, nullptr);
pthread_create(&t3, nullptr, waitAtBarrier, nullptr);
// 等待线程结束
pthread_join(t1, nullptr);
pthread_join(t2, nullptr);
pthread_join(t3, nullptr);
pthread_barrier_destroy(&barrier); // 销毁屏障
return 0;
}
waitAtBarrier 函数:每个线程在此函数中执行,先打印“Thread waiting at barrier...”表示到达屏障,然后调用 pthread_barrier_wait(&barrier) 在屏障处等待,直到所有线程都到达,之后才继续执行并打印“Thread passed the barrier!”.
主函数 main:
pthread_barrier_init(&barrier, nullptr, 3);
:初始化屏障,要求 3 个线程同步到达。t1
, t2
, t3
),它们都调用 waitAtBarrier
函数。pthread_join
等待所有线程完成。pthread_barrier_destroy(&barrier);
:销毁屏障,释放资源。这段代码的效果是:3 个线程都会在屏障处等待,直到全部线程到达后再一起通过,确保同步执行.
优点:
缺点:
屏障适用于需要同步阶段的场合,尤其是以下几种:
原子操作是一种“小而快”的多线程操作。它直接对数据进行“独占式”的更新,操作不可分割,不需要加锁,因为它的操作是原子的:要么全做,要么全不做。适合用于快速更新小数据,比如计数、标志位等场景。在多线程环境中使用原子操作,可以避免加锁带来的性能开销,因此更新简单共享资源时,非常高效.
在C++的标准库中,原子操作接口非常简单,常用的有以下几种:
std::atomic<T>
T
,常用于简单数据类型,如int
、bool
等。std::atomic<int> counter(0);
表示一个整型原子变量counter
,初始值为0。fetch_add()
和 fetch_sub()
counter.fetch_add(1);
会安全地加1,同时返回旧值。load()
和 store()
load()
用于原子地读取变量值,store()
用于原子地存储值,确保数据的一致性。下面的代码展示了如何使用 std::atomic 来安全地对共享数据 counter 进行递增操作。此处无需加锁,原子操作自动确保了线程安全.
#include <atomic>
#include <pthread.h>
#include <iostream>
std::atomic<int> counter(0); // 使用原子类型
void* increment(void* arg) {
for (int i = 0; i < 100000; ++i) {
counter++; // 原子操作,自动保证线程安全
}
return nullptr;
}
int main() {
pthread_t t1, t2;
// 创建两个线程
pthread_create(&t1, nullptr, increment, nullptr);
pthread_create(&t2, nullptr, increment, nullptr);
// 等待两个线程执行完毕
pthread_join(t1, nullptr);
pthread_join(t2, nullptr);
std::cout << "Final counter value: " << counter << std::endl;
return 0;
}
std::atomic<int> counter(0);
:使用原子类型 std::atomic
声明计数器 counter
。所有对 counter
的操作都是线程安全的。counter++
:原子递增操作,无需加锁,在多线程环境下也能保证数据的一致性。通过原子操作,我们避免了加锁带来的性能开销,代码简洁、高效,特别适合对小数据的频繁更新.
优点:
std::atomic
可以直接更新共享数据,代码更简洁。缺点:
原子操作非常适合以下几种场合:
今天我们一起探讨了 Linux 中的多线程同步方式,从互斥锁到条件变量,再到信号量、读写锁以及自旋锁、还有屏障和原子操作,逐一解锁了每种同步方式的应用场景和优缺点。学会这些技巧后,写多线程程序就不再让人头疼了! 同步其实并不神秘,只要掌握好这些基础工具,你也能写出流畅又安全的多线程程序 .
如果觉得有帮助,别忘了点赞和分享,关注我,我们一起学更多有趣的编程知识!已经掌握了这些同步方式? 那恭喜你!如果还没完全弄明白,没关系,欢迎在评论区留言,我们一起讨论,确保你都能搞懂! 。
想更快找到我? 直接微信搜索 「跟着小康学编程」,或者扫描下方二维码关注,和一群爱学习的编程小伙伴一起成长吧! 。
这里分享 Linux C、C++、Go 开发、计算机基础知识 和 编程面试干货等,内容深入浅出,让技术学习变得轻松有趣.
无论您是备战面试,还是想提升编程技能,这里都致力于提供实用、有趣、有深度的技术分享。快来关注,让我们一起成长! 。
非常简单!扫描下方二维码即可一键关注.
此外,小康最近创建了一个技术交流群,专门用来讨论技术问题和解答读者的疑问。在阅读文章时,如果有不理解的知识点,欢迎大家加入交流群提问。我会尽力为大家解答。期待与大家共同进步! 。
最后此篇关于别再被多线程搞晕了!一篇文章轻松搞懂Linux多线程同步!的文章就讲到这里了,如果你想了解更多关于别再被多线程搞晕了!一篇文章轻松搞懂Linux多线程同步!的内容请搜索CFSDN的文章或继续浏览相关文章,希望大家以后支持我的博客! 。
HTTP缓存相关的问题好像是前端面试中比较常见的问题了,上来就会问什么cache-control字段有哪些,有啥区别啥的。嗯……说实话,我觉得至少在本篇来说,HTTP缓存还算不上复杂,只是字段稍
代理,其实全称应该叫做代理服务器,它是客户端与服务器之间得中间层,本质上来说代理就是一个服务器,在HTTP的链路中插入的一个中间环节,就是代理服务器啦。所谓的代理服务就是指:服务本身不生产内容,
我们在前两篇的内容中分别学习了缓存和代理,大致了解了缓存有哪些头字段,代理是如何服务于服务器和客户端的,那么把两者结合起来,代理缓存,也就是说代理服务器也可以缓存,当客户端请求数据的时候,未必一
在前面的章节,我们把HTTP/1.1的大部分核心内容都过了一遍,并且给出了基于Node环境的一部分示例代码,想必大家对HTTP/1.1已经不再陌生,那么HTTP/1.1的学习基本上就结束了。这两
我们前一篇学习了HTTP/2,相比于HTTP/1,HTTP/2在性能上有了大幅的改进,但是HTTP/2因为底层还是基于TCP协议的,虽然HTTP/2在应用层引入了流的概念,利用多路复用解决了队头
前面我们花了很大的篇幅来讲HTTP在性能上的改进,从1.0到1.1,再到2.0、3.0,HTTP通过替换底层协议,解决了一直阻塞性能提升的队头阻塞问题,在性能上达到了极致。 那么,接下
上一篇噢,我们搞明白了什么是安全的通信,这个很重要,特别重要,敲黑板!! 然后,我们还学了HTTPS到底是什么,以及HTTPS真正的核心SSL/TLS是什么。最后我们还聊了聊TLS的实
经过前两章的学习,我们知道了通信安全的定义以及TLS对其的实现~有了这些知识作为基础,我们现在可以正式的开始研究HTTPS和TLS协议了。嗯……现在才真正开始。 我记得之前大概聊过,当
这一篇文章,我们核心要聊的事情就是HTTP的对头阻塞问题,因为HTTP的核心改进其实就是在解决HTTP的队头阻塞。所以,我们会讲的理论多一些,而实践其实很少,要学习的头字段也只有一个,我会在最开始
我们在之前的文章中介绍HTTP特性的时候聊过,HTTP是无状态的,每次聊起HTTP特性的时候,我都会回忆一下从前辉煌的日子,也就是互联网变革的初期,那时候其实HTTP不需要有状态,就是个浏览页面
前面几篇文章,我从纵向的空间到横向的时间,再到一个具体的小栗子,可以说是全方位,无死角的覆盖了HTTP的大部分基本框架,但是我聊的都太宽泛了,很多内容都是一笔带过,再加上一句后面再说就草草结束了。
大家好,我是煎鱼。 在 Go 语言中总是有一些看上去奇奇怪怪的东西,咋一眼一看感觉很熟悉,但又不理解其在 Go 代码中的实际意义,面试官却爱问... 今天要给大家介绍的是 SliceHead
我是一名优秀的程序员,十分优秀!