go语法大赏
前些日子单机房稳定性下降,找了好一会才找到真正的原因。这里面涉及到不少go语法细节,正好大家一起看一下。
一、仿真代码
这是仿真之后的代码
package mainimport ("fmt""go.uber.org/atomic""time"
)type StopSignal struct{}// RecvChannel is the wrapped channel for recv side.
type RecvChannel[T any] struct {// Data will be passed through the result channel.DataChannel <-chan T// Error will be passed through the error channel.ErrorChannel <-chan error// Stop signal will be passed through the stop signal channel,// when signal is sent or channel is closed, it means recv side requires send side to stop sending data.StopChannel chan<- StopSignalstopped *atomic.Bool
}// Close sends stop signal to the sender side.
func (c *RecvChannel[T]) Close() {if !c.stopped.CompareAndSwap(false, true) {return}close(c.StopChannel)
}// Stopped returns whether the stop signal has been sent.
func (c *RecvChannel[T]) Stopped() bool {return c.stopped.Load()
}// GetError returns the last error, it waits at most 1s if the error channel is not closed.
func (c *RecvChannel[T]) GetError() error {select {case err := <-c.ErrorChannel:return errcase <-time.After(time.Second):return nil}
}// SendChannel is the wrapped channel for sender side.
type SendChannel[T any] struct {// Data will be passed through the result channel.DataChannel chan<- T// Error will be passed through the error channel.ErrorChannel chan<- error// Stop signal will be passed through the stop signal channel,// when signal is sent or channel is closed, it means recv side requires send side to stop sending data.StopChannel <-chan StopSignalstopped *atomic.Bool
}// Close closes the result channel and error channel, so the recv will know the sending has been stopped.
func (c *SendChannel[T]) Close() {close(c.DataChannel)close(c.ErrorChannel)c.stopped = atomic.NewBool(true)
}// Stopped returns whether the stop signal has been sent.
func (c *SendChannel[T]) Stopped() bool {return c.stopped.Load()
}// Publish sends data to the data channel, does nothing if it is closed.
func (c *SendChannel[T]) Publish(t T) {if c.Stopped() {return}select {case <-c.StopChannel:case c.DataChannel <- t:}
}func (c *SendChannel[T]) PublishError(err error, close bool) {if c.Stopped() {return}select {case <-c.StopChannel:case c.ErrorChannel <- err:}if close {c.Close()}
}func NewChannel[T any](bufSize int) (*SendChannel[T], *RecvChannel[T]) {resultC := make(chan T, bufSize)errC := make(chan error, 1)stopC := make(chan StopSignal, 1)stopped := atomic.NewBool(false)sc := &SendChannel[T]{DataChannel: resultC,ErrorChannel: errC,StopChannel: stopC,stopped: stopped,}rc := &RecvChannel[T]{DataChannel: resultC,ErrorChannel: errC,StopChannel: stopC,stopped: stopped,}return sc, rc
}// SliceToChannel creates a channel and sends the slice's items into it.
// It ignores if the item in the slices is not a type T or error.
func SliceToChannel[T any](size int, s []any) *RecvChannel[T] {sc, rc := NewChannel[T](size)go func() {for _, item := range s {if sc.Stopped() {sc.Close()return}switch v := item.(type) {case T:sc.DataChannel <- vcase error:sc.ErrorChannel <- vdefault:continue}}sc.Close()}()return rc
}// /// 真正的处理逻辑
func Process(send *SendChannel[int]) {defer func() {if send != nil {fmt.Println("3 Process close defer")send.Close()}}()go func() {for {select {case <-send.StopChannel:fmt.Println("2 Process stop channel")send.Close()return}}}()send.ErrorChannel <- fmt.Errorf("0 Start error \n")fmt.Println("0 Start error")time.Sleep(1 * time.Second)
}func main() {send, recv := NewChannel[int](10)go func() {Process(send)}()for {fmt.Println("only once")select {case <-recv.ErrorChannel:fmt.Println("1 recv errorchannel ")recv.Close()break}break}//panic(1)time.Sleep(5 * time.Second)
}
执行结果如下:
➜ my go run main.go
only once
0 Start error
1 recv errorchannel
2 Process stop channel
3 Process close defer
panic: close of closed channelgoroutine 21 [running]:
main.(*SendChannel[...]).Close(...)/Users/bytedance/My/work/go/my/main.go:60
main.Process.func1()/Users/bytedance/My/work/go/my/main.go:147 +0x6c
main.Process(0x14000092020)/Users/bytedance/My/work/go/my/main.go:163 +0x118
main.main.func1()/Users/bytedance/My/work/go/my/main.go:168 +0x20
created by main.main in goroutine 1/Users/bytedance/My/work/go/my/main.go:167 +0x70
exit status 2
不知道大家是否能够比较快的看出来问题。
二、相关语法
2.1channel
知识点
在 Go 语言中,channel
是用于在多个goroutine
之间进行通信和同步的重要机制,以下是一些关于channel
的重要知识点:
1. 基本概念
- 定义:
channel
可以被看作是一个类型安全的管道,用于在goroutine
之间传递数据,遵循 CSP(Communicating Sequential Processes)模型,即 “通过通信来共享内存,而不是通过共享内存来通信”,从而避免了传统共享内存并发编程中的数据竞争等问题。 - 声明与创建:使用
make
函数创建,语法为make(chan 数据类型, 缓冲大小)
。缓冲大小是可选参数,省略时创建的是无缓冲channel
;指定大于 0 的缓冲大小时创建的是有缓冲channel
。例如:
unbufferedChan := make(chan int) // 无缓冲channel
bufferedChan := make(chan int, 10) // 有缓冲channel,缓冲大小为10
2. 操作方式
- 发送数据:使用
<-
操作符将数据发送到channel
中,语法为channel <- 数据
。例如:
ch := make(chan int)
go func() {ch <- 42 // 发送数据42到ch中
}()
- 接收数据:同样使用
<-
操作符从channel
中接收数据,有两种形式。一种是将接收到的数据赋值给变量,如数据 := <-channel
;另一种是只接收数据不赋值,如<-channel
。例如:
ch := make(chan int)
go func() {ch <- 42
}()
value := <-ch // 从ch中接收数据并赋值给value
- 关闭
channel
:使用内置的close
函数关闭channel
,关闭后不能再向其发送数据,但可以继续接收已发送的数据。接收完所有数据后,再接收将得到该类型的零值。例如:
ch := make(chan int)
go func() {for i := 0; i < 5; i++ {ch <- i}close(ch) // 关闭channel
}()
for {value, ok := <-chif!ok {break // 当ok为false时,表示channel已关闭}fmt.Println(value)
}
3. 缓冲与非缓冲channel
- 无缓冲
channel
:也叫同步channel
,数据的发送和接收必须同时准备好,即发送操作和接收操作会互相阻塞,直到对方准备好。只有当有对应的接收者在等待时,发送者才能发送数据;反之,只有当有发送者发送数据时,接收者才能接收数据。这确保了数据的同步传递。 - 有缓冲
channel
:内部有一个缓冲区,只要缓冲区未满,发送操作就不会阻塞;只要缓冲区不为空,接收操作就不会阻塞。当缓冲区满时,继续发送会阻塞;当缓冲区为空时,继续接收会阻塞。例如:
bufferedChan := make(chan int, 3)
bufferedChan <- 1
bufferedChan <- 2
bufferedChan <- 3
// 此时缓冲区已满,再发送会阻塞
// bufferedChan <- 4
4. 单向channel
- 单向
channel
只能用于发送或接收数据,分别为只写channel
(chan<- 数据类型
)和只读channel
(<-chan 数据类型
)。单向channel
主要用于函数参数传递,限制channel
的使用方向,增强代码的可读性和安全性。例如:
// 只写channel
func sendData(ch chan<- int) {ch <- 42
}// 只读channel
func receiveData(ch <-chan int) {data := <-chfmt.Println(data)
}
5. select
语句与channel
select
语句用于监听多个channel
的操作,它可以同时等待多个channel
的发送或接收操作。当有多个channel
准备好时,select
会随机选择一个执行。select
语句还可以结合default
分支实现非阻塞操作。例如:
ch1 := make(chan int)
ch2 := make(chan int)go func() {ch1 <- 1
}()select {
case data := <-ch1:fmt.Println("Received from ch1:", data)
case data := <-ch2:fmt.Println("Received from ch2:", data)
default:fmt.Println("No channel is ready")
}
6. channel
的阻塞与死锁
- 阻塞:发送和接收操作在
channel
未准备好时会阻塞当前goroutine
。无缓冲channel
在没有对应的接收者时发送会阻塞,没有发送者时接收会阻塞;有缓冲channel
在缓冲区满时发送会阻塞,缓冲区空时接收会阻塞。 - 死锁:如果在一个
goroutine
中,channel
的发送和接收操作相互等待,且没有其他goroutine
来打破这种等待,就会发生死锁。例如,一个goroutine
向无缓冲channel
发送数据,但没有其他goroutine
接收;或者一个goroutine
从无缓冲channel
接收数据,但没有其他goroutine
发送数据。运行时系统会检测到死锁并报错。
7. channel
的底层实现
channel
的底层实现基于一个名为hchan
的结构体,它包含了当前队列中元素数量、环形队列大小(缓冲容量)、指向环形队列的指针、元素大小、关闭标志、元素类型信息、发送索引、接收索引、等待接收的协程队列、等待发送的协程队列以及一个互斥锁等字段。- 发送操作时,如果接收队列非空,直接将数据拷贝给第一个等待的接收者并唤醒该
goroutine
;如果缓冲区未满,将数据存入缓冲区;如果缓冲区已满或无缓冲channel
,将当前goroutine
加入发送队列并挂起。接收操作时,如果发送队列非空,直接从发送者获取数据并唤醒发送者;如果缓冲区不为空,从缓冲区取出数据;如果缓冲区为空且无缓冲channel
,将当前goroutine
加入接收队列并挂起。
8. channel
误用导致的问题
在 Go 语言中,操作channel
时可能导致panic
或者死锁等:
- 多次关闭同一个
channel
使用内置的close
函数关闭channel
后,如果再次调用close
函数尝试关闭同一个channel
,就会引发panic
。这是因为channel
的关闭状态是一种不可逆的操作,重复关闭没有实际意义,并且可能会导致难以调试的问题。例如:
ch := make(chan int)
close(ch)
close(ch) // 这里会导致panic
- 向已关闭的
channel
发送数据
当一个channel
被关闭后,再向其发送数据会导致panic
。因为关闭channel
意味着不再有数据会被发送到该channel
中,继续发送数据违反了这种约定。示例如下:
ch := make(chan int)
close(ch)
ch <- 1 // 向已关闭的channel发送数据,会导致panic
- 关闭未初始化(
nil
)的channel
如果尝试关闭一个值为nil
的channel
,会引发panic
。nil
的channel
没有实际的底层数据结构来支持关闭操作。例如:
var ch chan int
close(ch) // 这里会导致panic,因为ch是nil
- 死锁导致的
panic
在操作channel
时,如果多个goroutine
之间的通信和同步设计不当,可能会导致死锁。死锁发生时,所有涉及的goroutine
都在互相等待对方,从而导致程序无法继续执行,运行时系统会检测到这种情况。例如:
func main() {ch := make(chan int)ch <- 1 // 没有其他goroutine从ch中接收数据,这里会阻塞,导致死锁fmt.Println("This line will never be executed")
}
➜ my go run main.go
fatal error: all goroutines are asleep - deadlock!goroutine 1 [chan send]:
main.main()/Users/bytedance/My/work/go/my/main.go:172 +0x54
exit status 2
- 不恰当的
select
语句使用
在select
语句中,如果没有default
分支,并且所有的case
对应的channel
操作都无法立即执行(阻塞),那么当前goroutine
会被阻塞。如果在主goroutine
中发生这种情况且没有其他goroutine
可以运行,就会导致死锁。例如:
func main() {ch1 := make(chan int)ch2 := make(chan int)select {case <-ch1:// 没有数据发送到ch1,这里会阻塞case <-ch2:// 没有数据发送到ch2,这里会阻塞}
}
要避免这些panic
情况,编写代码时需要仔细设计channel
的使用逻辑,合理处理channel
的关闭、数据的发送和接收,以及确保goroutine
之间的同步和通信正确无误。
解析
在NewChannel函数中,send和recv channel被赋值的是同一个ErrorChannel,而send和recv都是单向channel,一个只写,一个只读。
所以当Process里send.ErrorChannel <- fmt.Errorf(“0 Start error \n”)执行的时候,main中的case <-recv.ErrorChannel被立即触发,然后执行recv.Close()函数,该函数执行了close(c.StopChannel),又触发了Process中的case <-send.StopChannel,执行了send.Close()。对于Process退出的时候,有defer,再次执行send.Close(),导致channel被多次关闭。
2.2defer
知识点
以前写过Go defer的一些神奇规则,你了解吗?,这次主要关注
- defer(延迟函数)执行按后进先出顺序执行,即先出现的 defer最后执行。
- Process中的defer的执行顺序与Process中的goroutine里的defer(如果有的话)执行顺序无关。
解析
其实这两个Close位置都有可能panic,主要看谁被先执行到。我是为了演示让Process sleep了1s。
defer func() {if send != nil {fmt.Println("3 Process close defer")send.Close()}}()go func() {for {select {case <-send.StopChannel:fmt.Println("2 Process stop channel")send.Close()return}}}()
2.3recover
知识点
在 Go 语言中,recover
只能用于捕获当前goroutine
内的panic
,它的作用范围仅限于当前goroutine
。具体说明如下:
只能捕获当前goroutine
的panic
:当一个goroutine
发生panic
时,该goroutine
会沿着调用栈向上展开,执行所有已注册的defer
函数。如果在这些defer
函数中调用recover
,则可以捕获到该goroutine
内的panic
,并恢复正常执行流程。而对于其他goroutine
中发生的panic
,当前goroutine
无法通过recover
捕获。例如:
package mainimport ("fmt""time"
)func worker() {defer func() {if r := recover(); r != nil {fmt.Println("Recovered in worker:", r)}}()panic("Worker panicked")
}func main() {go worker()time.Sleep(1 * time.Second)fmt.Println("Main goroutine continues")
}
在上述代码中,worker
函数中的defer
语句里使用recover
捕获了该goroutine
内的panic
。main
函数中的goroutine
并不会受到影响,继续执行并打印出 “Main goroutine continues”。
解析
当时之所以查的比较困难,主要是发现Process中go func里配置了recover,报了很多错,但感觉没有大问题。加上代码不熟悉,没有发现有概率触发Process的defer中的panic。而且公司的监控没有监控到自建goroutine的panic情况。
三、解决方案
在Process中添加recover
defer func() {if r := recover(); r != nil {fmt.Println("Recovered in worker:", r)}}()
其实比较建议在涉及channel相关的地方,都加个recover,尤其是不太熟悉的时候。
相关文章:

go语法大赏
前些日子单机房稳定性下降,找了好一会才找到真正的原因。这里面涉及到不少go语法细节,正好大家一起看一下。 一、仿真代码 这是仿真之后的代码 package mainimport ("fmt""go.uber.org/atomic""time" )type StopSignal…...

软件工程各种图总结
目录 1.数据流图 2.N-S盒图 3.程序流程图 4.UML图 UML用例图 UML状态图 UML时序图 5.E-R图 首先要先了解整个软件生命周期: 通常包含以下五个阶段:需求分析-》设计-》编码 -》测试-》运行和维护。 软件工程中应用到的图全部有:系统…...
R-tree详解
R-tree 是一种高效的多维空间索引数据结构,专为快速检索空间对象(如点、线、区域)而设计。它广泛应用于地理信息系统(GIS)、计算机图形学、数据库等领域,支持范围查询、最近邻搜索等操作。以下是其核心原理…...

AAAI2024 | 基于特征多样性对抗扰动攻击 Transformer 模型
Attacking Transformers with Feature Diversity Adversarial Perturbation 摘要-Abstract引言-Introduction相关工作-Related Work方法-Methodology实验-Experiments结论-Conclusion 论文链接 本文 “Attacking Transformers with Feature Diversity Adversarial Perturbatio…...

关于数据湖和数据仓的一些概念
一、前言 随着各行业数字化发展的深化,数据资产和数据价值已越来越被深入企业重要发展的战略重心,海量数据已成为多数企业生产实际面临的重要问题,无论存储容量还是成本,可靠性都成为考验企业数据治理的考验。本文来看下海量数据存储的数据湖和数据仓,数据仓库和数据湖,…...
鸿蒙OSUniApp制作自定义的下拉菜单组件(鸿蒙系统适配版)#三方框架 #Uniapp
UniApp制作自定义的下拉菜单组件(鸿蒙系统适配版) 前言 在移动应用开发中,下拉菜单是一个常见且实用的交互组件,它能在有限的屏幕空间内展示更多的选项。虽然各种UI框架都提供了下拉菜单组件,但在一些特定场景下&…...
C++面试2——C与C++的关系
C与C++的关系及核心区别的解析 一、哲学与编程范式:代码组织的革命 过程式 vs 多范式混合 C语言是过程式编程的典范,以算法流程为中心,强调“怎么做”(How)。例如,实现链表操作需手动管理节点指针和内存。 C++则是多范式语言,支持面向对象(OOP)、泛型编程(模板)、函…...

常用的Java工具库
1. Collections 首先是 java.util 包下的 Collections 类。这个类主要用于操作集合,我个人非常喜欢使用它。以下是一些常用功能: 1.1 排序 在工作中,经常需要对集合进行排序。让我们看看如何使用 Collections 工具实现升序和降序排列&…...
基于LabVIEW的双音多频系统设计
目录 1 系统设计概述 双音多频(Dual-Tone Multi-Frequency, DTMF)信号是一种广泛应用于电话系统中的音频信号,通过不同的频率组合表示不同的按键。每个按键对应两个频率,一个低频和一个高频,共同组成独特的信号。在虚拟仪器技术快速发展的背景下,利用LabVIEW等图形化编程…...

R S的EMI接收机面板
图片摘自R & S官网。 根据您提供的第一张图(设备前面板带屏幕的图像),这是 Rohde & Schwarz ESRP7 EMI Test Receiver 的正面显示界面,我将对屏幕上显示的参数逐项进行解读: 🖥️ 屏幕参数解读 左…...

[ctfshow web入门] web122
信息收集 这一题把HOME开放了,把#和PWD给过滤了 <?php error_reporting(0); highlight_file(__FILE__); if(isset($_POST[code])){$code$_POST[code];if(!preg_match(/\x09|\x0a|[a-z]|[0-9]|FLAG|PATH|BASH|PWD|HISTIGNORE|HISTFILESIZE|HISTFILE|HISTCMD|US…...
Nginx+Lua 实战避坑:从模块加载失败到版本冲突的深度剖析
Nginx 集成 Lua (通常通过 ngx_http_lua_module 或 OpenResty) 为我们提供了在 Web 服务器层面实现动态逻辑的强大能力。然而,在享受其高性能和灵活性的同时,配置和使用过程中也常常会遇到各种令人头疼的问题。本文将结合实际案例,深入分析在 Nginx+Lua 环境中常见的技术问题…...
LangChain框架-Chain 链详解
摘要 本文基于源码分析与官方文档梳理,系统解析 LangChain 框架中的核心组件 Chain 链,旨在帮助开发者深入理解其设计原理、功能分类及实践应用场景。 作为 LangChain 的核心机制,Chain 链采用管道-过滤器(Pipe-Filter)…...

Java虚拟机 - JVM与Java体系结构
Java虚拟机 JVM与Java体系结构为什么要学习JVMJava与JVM简介Java 语言的核心特性JVM:Java 生态的基石JVM的架构模型基于栈的指令集架构(Stack-Based)基于寄存器的指令集架构(Register-Based)JVM生命周期 总结 JVM与Jav…...
elementUI调整滚动条高度后与固定列冲突问题解决
/* 1. 首先确保基础样式生效 */ .el-table.el-table–scrollable-x .el-table__body-wrapper { overflow-x: auto !important; } /* 2. 设置滚动条高度(对所有表格生效) */ .el-table__body-wrapper::-webkit-scrollbar { height: 10px !important; } …...
基于 nvitop+Prometheus+Grafana 的物理资源与 VLLM 引擎服务监控方案
一、方案背景与目标 在人工智能与高性能计算场景中,对物理资源(尤其是 GPU)的实时监控以及对 VLLM 引擎服务的性能追踪至关重要。本方案通过整合 nvitop、Prometheus 和 Grafana 三大组件,构建一套完整的监控体系,实现…...
互联网大厂Java求职面试:Spring AI与大模型交互在短视频平台中的应用
互联网大厂Java求职面试:Spring AI与大模型交互在短视频平台中的应用 面试场景设定 郑薪苦,一名有着丰富项目经验但总是能用奇葩比喻解释复杂技术的程序员,正在接受某知名互联网大厂技术总监的面试。 第一轮提问 面试官:假设我…...
【Lua】java 调用redis执行 lua脚本
【Lua】java 调用redis执行 lua脚本 public Object executeLuaScript(String script, List<String> keys, Object... args) {// 注意: 这里 Long.class 是返回值类型, 一定要指定清楚 不然会报错return this.redisTemplate.execute(RedisScript.of(j脚本, Long.class), k…...
【工奥阀门科技有限公司】签约智橙PLM
近日,工奥阀门科技有限公司正式签约了智橙泵阀行业版PLM。 忠于质量,臻于服务,精于研发 工奥阀门科技有限公司(以下简称工奥阀门)坐落于浙江永嘉,是一家集设计、开发、生产、销售、安装、服务为一体的阀门…...

灌区量测水自动化监测解决方案
一、方案背景 随着社会发展和人口增长,水资源需求不断增大。我国水资源总量虽然丰富,但时空分布不均,加之农业用水占比大且效率偏低,使得水资源短缺问题日益凸显。农业用水一直是我国的耗水大户,占全部耗水总量的60%以…...
SpringBoot整合MQTT实战:基于EMQX构建高可靠物联网通信,从零到一实现设备云端双向对话
一、引言 随着物联网(IoT)技术的快速发展,MQTT(Message Queuing Telemetry Transport)协议因其轻量级、低功耗和高效的特点,已成为物联网设备通信的事实标准。本文将详细介绍如何使用SpringBoot框架整合MQTT协议,基于开源MQTT代理EMQX实现设…...
AI与机器学习深度集成:从设备端能力爆发到开发工具智能化
简介 AI与机器学习技术正以惊人的速度在移动开发领域深入集成,设备端AI能力爆发与AI辅助开发工具的崛起,为开发者带来了前所未有的高效开发体验和应用创新机遇。本文将全面解析Google最新AI技术栈(包括ML Kit 2.0和Gemini Nano模型)的特性与应用场景,探索Android Studio …...

界面控件DevExpress WinForms v24.2 - 数据处理功能增强
DevExpress WinForms拥有180组件和UI库,能为Windows Forms平台创建具有影响力的业务解决方案。DevExpress WinForms能完美构建流畅、美观且易于使用的应用程序,无论是Office风格的界面,还是分析处理大批量的业务数据,它都能轻松胜…...

Linux的MySQL头文件和找不到头文件问题解决
头文件 #include <iostream> #include <mysql_driver.h> #include <mysql_connection.h> #include <cppconn/statement.h> #include <cppconn/resultset.h> #include <cppconn/prepared_statement.h> #include <cppconn/exception.h&g…...

wps excel将表格输出pdf时所有列在一张纸上
记录:wps excel将表格输出pdf时所有列在一张纸上 1,调整缩放比例,或选择将所有列打印在一页 2,将表格的所有铺满到这套虚线...

zabbix7.2最新版本 nginx自定义监控(三) 设置触发器
安装zabbix-get服务 在zabbix-server端口安装zabbix-get服务 [rootlocalhost ~]# dnf install -y zabbix-get Last metadata expiration check: 1:55:49 ago on Wed 14 May 2025 09:24:49 AM CST. Dependencies resolved. Package Architectur…...
CDN加速对云手机延迟的影响
一、CDN加速对云手机延迟的核心作用 缩短物理距离,降低网络延迟 CDN通过全球分布的节点,将云手机的服务内容(如应用数据、画面流)缓存至离用户最近的服务器,减少数据传输的物理距离。例如,用户在中国访问美…...
为什么 Docker 建议关闭 Swap
在使用 Docker 时,关闭系统 Swap(交换分区) 是一个常见的推荐做法,尤其是在生产环境中。虽然 Docker 不强制要求禁用 Swap,但出于性能、稳定性、可控性和资源管理的目的,通常建议这样做。 为什么 Docker 建…...

缓存的相关内容
缓存是一种介于数据永久存储介质与数据应用之间数据临时的存储介质 实用化保存可以有效地减少低俗数据读取的次数 (例如磁盘IO), 提高系统性能 缓存不仅可以用于提高永久性存储介质的数据读取效率,还可以提供临时的数据存储空间 spring boot中提供了缓存技术, 方便…...

[ctfshow web入门] web77
信息收集 上一题的读取flag方式不能用了,使用后的回显是:could not find driver 解题 同样的查目录方法 cvar_export(scandir("glob:///*"));die();cforeach(new DirectoryIterator("glob:///*") as $a){echo($a->__toString…...