1.文档阅读 source code https://github.com/bigwhite/publication
04|初窥门径:一个Go程序的结构是怎样的?-极客时间
02|拒绝“Hello and Bye”:Go语言的设计哲学是怎么一回事?-极客时间
08|入口函数与包初始化:搞清Go程序的执行次序-极客时间
09|即学即练:构建一个Web服务就是这么简单-极客时间
36 | unicode与字符编码-极客时间
Go's Declaration Syntax - Go 语言博客
10|变量声明:静态语言有别于动态语言的重要特征-极客时间
2.整理输出 ------------------------------------------------------------分隔符------------------------------------------------------------ ------------------------------------------------------------分隔符------------------------------------------------------------ 04|初窥门径:一个Go程序的结构是怎样的?-极客时间 1. 网友-多选参数 go 引用了其他包的话,是将引用的包都编译进去。我用 ldd 才看几个 go 编译出来的二进制程序都是没有动态链接库的使用。但是,在看其他几个 go 编译出来的二进制程序时(比如 containerd、ctr,它们都是用 go 编写的),又有引用动态链接库,这个是为什么?
作者回复: go默认是开启CGO_ENABLED的,即CGO_ENABLED=1。但编译出来的二进制程序究竟有无动态链接,取决于你的程序使用了什么包。如果就是一个hello,world,那么你编译出来的将是一个纯静态程序。
如果你依赖了网络包或一些系统包,比如用http包编写了一个web server(见第9讲示例),那么你编译出来的二进制程序又会是一个包含动态链接的程序。
原因就在于目前的go标准库中,某些功能具有两份实现,一份是c语言实现的,一份是go语言实现的。在cgo_enable开启的情况下,go链接器会链接c语言的版本,于是就有了依赖动态链接库的情况。如果你将cgo_enabled置为0,你再重新编译链接,那么go链接器会使用go版本的实现,这样你将得到一个没有动态链接的纯静态二进制程序。
2. go的异常处理,使用起来简单,但是不方便,请问老师这是在践行go的简单设计哲学吗?
作者回复: 从go设计者的初衷来看(https://golang.google.cn/doc/faq#exceptions),go没有采用像java那样的结构化异常处理的确是出于对“简单”原则的考虑。
在java中错误处理与真正的“异常”是混杂在Try-catch机制中的,并没有明显的界限,无论是错误还是异常,一旦throw,方法的调用者就得负责处理它。
但在go中,错误处理与真正的异常处理是严格分开的,也就是说不要将panic掺和到错误处理中。
错误处理是常态,go中只有错误是返回给上层的。一旦出现panic,这意味着整个程序处于即将崩溃的状态,返回给上层几乎也是“无济于事”,所以在go中,一个常见的api设计思路是 不要向外部抛出panic(don't panic!)。如果api中存在panic的可能性,那么api自己要负责处理panic,并通过error将状态返回给上层。如果api无法处理panic,那程序就很大可能是要崩溃了,这种panic多是因为程序bug导致的。
3. 如何import自己在本地创建的module,在这个module还没有发布到GitHub的情况下?
作者回复: 非常好的问题。go module机制在您提到的工作场景下目前的体验做的还不够好。在Go 1.17版本及之前版本的解决方法是使用go mod的replace指示符(directive)。假如你的module a要import的module b将发布到github.com/user/repo中,那么你可以手动在module的go.mod中的require块中手工加上一条:
require github.com/user/repo v1.0.0
注意v1.0.0这个版本号是一个临时的版本号。
然后在module a的go.mod中使用replace将上面对module b的require替换为本地的module b:
replace github.com/user/repo v1.0.0 => module b本地路径
这样go命令就会使用你本地正在开发、尚未提交github的module b了。
你可以试试。
4. 网友-lesserror 感谢Tony Bai老师这么细致的讲解。能够基于最新的Go Module来讲解,正好是我想要的。 之前看了太多基于go path的教程。感觉Go Module比之前的go path清晰多了。
有几个问题想要了解下:
1. 老师这节课的源码目录为什么不是类似:src/github.com/xxx 这样的方式在本地构建呢? 我看很多地方,建议以这种方式来创建项目。
2. go mod tidy 这个命令中的 tidy 该如何翻译比较准确呢? 这个命令平时还挺常用的,希望结合中文翻译去加深记忆。
ps: 希望后面老师能够,结合Go项目的代码访问路径,来系统讲解一下,代码的访问流程。
作者回复: 问题1:go module与gopath的一个重要区别就是可以将项目放在任意路径下,而无需局限在$GOPATH/src下面。我之所以将一个module放在一个任意路径下,就是故意要与GOPATH模式区分开的。
问题2:有些时候,无论怎么翻译都不能很理想的呈现英文作者的原意,因此向go mod tidy我倾向于直接使用原文,而不要翻译为中文。
关于你的建议,我后续会重点考虑的,感谢。
5. 网友-william 前面能看懂,后面就开始迷了。。。 Go Module 的概念我还是不怎么理解, 很多教程都用github.com... 做例子, 你说的是”路径“,路径不应该是 (win10)"D:\goprojects\hellomodule" 吗?我自己自定义一个任意的名字也可以正常运行?我感觉很奇怪,为什么很多人用github.com/...做module呢? 还有go mod tidy 命令后,下载的包在哪?在Go语言安装路径的src文件夹中?还是在哪?
作者回复: 1. 使用github.com/...作为module path是因为多数实用级module多是上传到github上的。用这种示例便于后续与真实生产接驳。但对于本地开发使用的简单示例程序而言,你说的没错。module path可以任意起,比如:
module demo1
也是ok的。
第二个问题,go mod tidy下载的第三方包一般在$GOPATH/pkg/mod下面。如果没有设置GOPATH环境变量,其默认值为你的home路径下的go文件夹。这样第三方包就在go文件夹的pkg/mod下面。
另外网友-二的根比方 $GOPATH/pkg/mod :如果在本地开发,pkg体积已经很大了,几个G,只要开发用过且下载过的都在里面。 个人环境截图: ![](https://i-blog.csdnimg.cn/blog_migrate/6a4033328b76d384d5156fc1f5058553.png) 6. Rayjun 老师,提个问题,有了 Go Module 之后, GoPath 还有存在的必要么?
作者回复: go官方有移除gopath的打算。目前这个时间点,学习go基本不需要了解太多gopath了。
补充:网友-aoe Go Module看起来很好用啊! 作者回复: go module已经是go官方标准包依赖管理和构建模式了,所以从go入门开始就建议直接使用go module。gopath模式可以简单了解一下,然后忽略掉就好了。 7. Demon.Lee 如果与面向对象语言进行类比,go 语言中的包,是否可以类比成Java中的类呢?
作者回复: 如果从编译的角度来讲,go包是编译的基本单位,每个go包编译为一个.a文件,每个java public class编译为一个class文件。它们的确有些类似。但从内容结构上来说,go包是一个命名空间的概念,可以包含go所有语法元素。java class就是一个拥有多个属性+方法的抽象。
8. GAC·DU Go编译模式很好的解决了环境问题,就像docker镜像一样,一包在手,天下可跑,不再需要考虑环境的问题,实现了宏观上的标准化。不过有一个疑惑,如果一个大项目需要引用很多外部项目,编译的包会不会过大,因此不利于项目的分发部署?
作者回复: 无论任何静态编程语言,引用外部依赖越多,最终编译生成的可执行文件size都会越大。毕竟要把真正依赖的包的目标代码放入可执行文件中。这个没招啊。
9.网友-隰有荷 老师,我有些不理解的地方在于: 1、 1-1、go mod init 后面跟的路径规则是什么样的? 1-2、这个命令本身是什么含义? 1-3、文中所说的module,到底哪个是module,是go.mod文件是module,还是hellomodule这个文件夹是module?因为我看文章中说当前文件夹是module的根目录,所以这里不太理解。
2、我在执行go mod tidy 之后出现报错:go: warning: "all" matched no packages。但是我每一步都和文章中的步骤一样,请问是哪里出现了问题?
作者回复: 1-1. go mod init后面的路径就是go.mod中module后面的路径,代表的是module path。 1-2. go mod init命令的实际行为就是在当前目录下创建一个go.mod,而这个go.mod将当前目录转换为一个go module。 1-3. go module是一个逻辑概念。文中也说了,它更像一个命名空间的概念。它与文件夹名称无关。有了go.mod后,这个文件夹下的各个包就算是这个go module下面的包了。包的导入路径也是以module path为前缀的。
2. 确认一下你本地的go编译器版本?确认go module构建模式已开启。确认一下是否在go.mod所在目录执行的go mod tidy?
10. 网友-高远 老师好,学完这一节我有两个问题: 1. go module和go package有什么区别和联系? 2. go run只是go build和执行可执行文件这俩步的一个快捷方式吗?还是有一些特殊的处理?
作者回复: 1. 关于go module后面有详细讲解,这里简单说一下。go module是一个逻辑概念,它由一个或一组package组成,这组package的版本由module的版本决定。这组packet的导入路径也基于module path。实际我们在包中导入的包都是module下的某个包,我们在go.mod中的依赖则用module及module版本表述。 2. go run 就是build+run。
11. 网友-文经 go module管理,有了这种显式的依赖,是不是连makefile都不需要了,感觉go build可以实现类似make file的功能?
作者回复: 不用makefile等第三方构建辅助工具是Go核心团队的目标。
但说实话,在实际生产项目中,项目不仅仅只是构建(go build)与跑测试(go test)操作,很可能还有制作镜像,上传镜像等,这些操作go命令是“鞭长莫及”的,这时候,我们还是需要依赖make等第三方的项目管理工具。
我个人在生产项目中就一直在使用make工具。
12. 网友-凉友 为什么我的执行完 go mod tidy 之后 还是找不到依赖 还需要再执行go mod vendor -v才可以呢
作者回复: go mod tidy没有自动分析和下载依赖么?
13. 网友-水到渠成 go mod init XXXXX 后面这部分 XXXXX 可以是任意的吧?应该只是作为文件的标识吧
作者回复: XXXXX将作为go.mod中module后面的module path。
通常用类似github.com/user/repo的形式标识一个唯一的module path。
但是如果本地学习之用,用任何字符串都可,比如demo1。
14. 网友-小宝 关于go module 想问下老师: 1. 多数module引用都是基于 github.com/a-module/xxx, 这个在企业实际中是否会存在去构建私域的moudule Sever? 2. 举个例子 require( github.com/go-redis/redis/v7 v7.4.0 ) 这个v7是不是一个虚拟目录,我在本地没有看到这个目录,没想明白这个module在本地怎么区分版本兼容? 3. v7.4.0 这个具体版本通常就是去github上找吗?
上面问题,类比Java的maven仓库,如果理解不正确,请老师指正。
作者回复: 1. 会的。很多企业会建立自己的module proxy,拉取内部的private module 2. v7是go module的语义导入版本机制,在06,07讲会有详细讲解。 3. 具体版本go 命令会自动到module所在vcs server(比如github.com)上去search。
15. 网友-Grocker 请问下tony老师,用go module的话项目根目录下还需要 src,pkg,bin这三个目录吗?
作者回复: Go应用的项目的根目录下不需要src, pkg, bin。
------------------------------------------------------------分隔符------------------------------------------------------------ ------------------------------------------------------------分隔符------------------------------------------------------------ ------------------------------------------------------------分隔符------------------------------------------------------------ 08|入口函数与包初始化:搞清Go程序的执行次序-极客时间 1. 文章中一段话 大多 Go 程序都是并发程序,程序会启动多个 Goroutine 并发执行程序逻辑,这里你一定要注意主 Goroutine 的优雅退出,也就是主 Goroutine 要根据实际情况来决定,是否要等待其他子 Goroutine 做完清理收尾工作退出后再行退出。 2. 当思考题 init 函数在检查包数据初始状态时遇到失败或错误的情况,我们该如何处理呢? 网友-andox 分情况而定 1. 初始化失败的是必要的数据 panic处理 结束进程 1. 初始化失败的是对业务没影响,可成功可失败的 输出warn或error日志 方便定位
作者回复: 手动点赞!
另外网友-python玩家一枚 init失败的话,我感觉一般init中要完成的内容好像都偏向资源属性,如果有必然能成功的默认属性则走默认值并警告,如果是必要资源则不成功会影响后续的运行,这时候应该要直接严重错误告警并终止程序吧
作者回复: 正确✅
另外网友2-进化菌 真棒,相当于了解go代码的执行生命周期。 当 init 函数在检查包数据初始状态时遇到失败或错误的情况,我们该如何处理呢?直接返回异常吗?在go里面,异常一般会当成第二个返回值吧。
作者回复: init函数没有返回值,异常是通过panic机制传导的,通常导致程序退出。当 init 函数在检查包数据初始状态时遇到失败或错误的情况,通过panic退出是一个多数的选择。
作者回复: 记录错误日志并退出是目前选择最多的方案。
网友3-歪文 当 init 函数在检查包数据初始状态时遇到失败或错误的情况,我们该如何处理呢 1. 由于是程序启动,可以直接panic,让开发者解决问题。 2. 如果不影响整个程序运行,打印warn警告,然后使用默认值代替,继续运行下去。
作者回复: 正确✅
3. 网友-一步 go 循环依赖是怎么处理的?
作者回复: go不允许循环依赖。编译器会检测并报错。
4.网友-Calvin 简单做个笔记: - Go 包的初始化次序: 1)依赖包按“深度优先”的次序进行初始化; 2)每个包内按以“常量 -> 变量 -> init 函数”(main.main 函数前)的顺序进行初始化; 3)包内的多个 init 函数按出现次序进行自动调用。 - init 函数常见用途: 1)重置包级变量值; 2)实现对包级变量的复杂初始化; 3)在 init 函数中实现“注册模式”(工厂设计模式)- 空导入。 - init 函数具备的几种行为特征(init 函数十分适合做一些包级数据初始化工作以及包级数据初始状态的检查工作): 1)执行顺位排在包内其他语法元素的后面; 2)每个 init 函数在整个 Go 程序生命周期内仅会被执行一次; 3)init 函数是顺序执行的,只有当一个 init 函数执行完毕后,才会去执行下一个 init 函数。
作者回复: 手动点赞!
5.网友-二的根比方 老师,请问下“GO入口文件包名称一定要是main,func名称一定要是main,(满足2个条件)文件名或者文件夹名字不一定是main。”这样的说法对吗
作者回复: 对的。
个人补充: package name与file name不一定相同,但是推荐相同。 ------------------------------------------------------------分隔符------------------------------------------------------------- 09|即学即练:构建一个Web服务就是这么简单-极客时间 1. 网友-尧九之阳 Go现在有流行的web服务器框架么?
作者回复:
go最初有很多web框架,经过多年演进,目前gin这个web框架似乎成为了go社区的首选。
2. 网友-snow 我看这里使用了mux包,我只用过gin包,请问这两个老师更喜欢哪个?以及这里选择mux的原因。
作者回复:
原则就是如果标准库中的mux可以满足,尽量不引用第三方包。
如果标准库无法满足,尽量引用规模较小的包。gin是更大的web框架。除了mux,还有很多其他功能。如果不是项目必需,使用更“专一”的包可能更好。
3. 网友-jc9090kkk 这节课的项目内容理解起来不困难,但是对于实际的生产项目而言,尤其是对第三方的中间件有依赖的前提下,大都会针对的将配置单独存储为配置文件以方便维护,我想问下老师go项目针对配置文件有什么最佳实践方式吗? 我自己本地写了一个小项目,用的是以下方式来配置MySQL连接的,我总觉得不太优雅,大多项目会根据开发环境的不同选取不同的配置文件作为配置项加载,但是如果通过硬编码的方式添加进去会让项目变得很奇怪。。。
package config
// GetDBConfig 数据库配置 func GetDBConfig() map[string]string {
// 初始化数据库配置map dbConfig := make(map[string]string)
dbConfig["DB_HOST"] = "127.0.0.1" dbConfig["DB_PORT"] = "3306" dbConfig["DB_NAME"] = "test" dbConfig["DB_USER"] = "root" dbConfig["DB_PWD"] = "123456" dbConfig["DB_CHARSET"] = "utf8"
// 连接池最大连接数 dbConfig["DB_MAX_OPEN_CONNECTS"] = "20" // 连接池最大空闲数 dbConfig["DB_MAX_IDLE_CONNECTS"] = "10" // 连接池链接最长生命周期 dbConfig["DB_MAX_LIFETIME_CONNECTS"] = "7200"
return dbConfig }
作者回复:
生产项目一般不会将配置“硬编码”进去的。一般而言,配置项会存储在配置文件中,也有通过命令行参数传入程序的,也可以通过环境变量传入程序。Go标准库没有内置配置读写框架,目前go社区应用较多的第三方库是Go核心团队成员开发的viper(github.com/spf13/viper)。
对于一些更大的平台,常常有很多服务,这些服务的配置一般存储在专门的配置中心中,由配置中心管理与分发。
4. 网友-xsgzh 老师请教个文件 store/memstore.go文件中第29 - 30行,直接赋值不可以么,ms.books[book.id] = book? nBook := *book ms.books[book.Id] = &nBook
作者回复: 如果ms.books[book.id] = book,那么由于存储的value是一个指针,当外部的book发生变化时,map中存储的value实际上也会跟着变化。
我的代码中通过“clone”,实际上是将存储在map中的book与外部传入的book分离开来,它们是两块不同的存储区域。
------------------------------------------------------------分隔符------------------------------------------------------------ 10|变量声明:静态语言有别于动态语言的重要特征-极客时间 文章片段截取 ... Go 语言提供了一种通用变量声明形式以及两种变量声明“语法糖”形式,而且 Go 包级变量和局部变量会根据具体情况选择不同的变量声明形式,这里我们用一幅图来做个形象化的小结: ![](https://i-blog.csdnimg.cn/blog_migrate/3815afdfca6e8cc6581edea58d5e3b98.png) 你可以看到,良好的变量声明实践需要我们考虑多方面因素,包括明确要声明的变量是包级变量还是局部变量、是否要延迟初始化、是否接受默认类型、是否是分支控制变量并结合聚类和就近原则等。 说起来,Go 语言崇尚“做一件事只用一种方法”,但变量声明却似乎是一个例外。如果让 Go 语言的设计者重新来设计一次变量声明语法,我觉得他们很大可能不会给予开发们这么大的变量声明灵活性。作为开发者,我们要注意的是,在统一项目范围内,我们选择的变量声明的形式应该是一致的。 思考题 今天,我们的思考题是:与主流静态语言不同,在 Go 语言变量声明中,类型是放在变量名的后面的,你认为这样做有什么好处?欢迎在留言区给我留言。 1. 网友-珅珅君 Go's Declaration Syntax - Go 语言博客 https://blog.go-zh.org/gos-declaration-syntax 其实官方有给出解释,原因简单来说就是和C相比,在当参数是指针的复杂情况下,这种声明格式会相对好理解一点,链接里面有详细解释
作者回复: 正解。
2. 网友-jc9090kkk 我认为类型放在变量名的后面是为了提高代码可读性的,我个人认为golang的设计者在设计go语言的时候大概率参考了c语言的语法,针对存在大量变量需要声明的场景下,go的变量类型放在变量名的后面可读性会高很多,不容易引起类型定义的混乱。
比如: c: int (*fp)(int a, int b); int (*fp)(int (*ff)(int x, int y), int b)
go: f func(func(int,int) int, int) int f func(func(int,int) int, int) func(int, int) int
相比之下:go的清晰易懂!而c的层层嵌套,难以直接看出是什么类型的指针
另外的一点我认为也是大多编程语言开发者的习惯,语法定义后紧接着就是变量名应该是很多编程者的开发习惯,如果var后面跟着int类型这样的语法会让人很别扭,至少对我来讲是这样。
作者回复: ✅。答案非常细致。刚开始学go时,也不适应,后来适应后,就像你说的,在一些复杂函数/方法的声明中,类型在后面的方式更容易理解。
3. 网友-hhzzer 想问一下老师,为啥go产生了这么多种变量声明方式?依照一个问题只用一种方法解决的原则,如果你重新设计,会选择保留哪一种申明方式呢?
作者回复:
1. 保留这么多变量声明方式的原因,Go语言设计者们并未在公开场合说明过。这个我也无不能不负责任的揣测^_^。 2. rob pike只是说过,如果有机会从头设计go,他不会再提供这么多变量声明方式了。但没说要保留哪一种。如果是我设计,我可能会保留var a int以及短变量声明。
4. 网友-修文 就近原则介绍的例子中提到,ErrNoCookie 只被用在了 Cookie 方法中。有一个疑问是,为什么 ErrNoCookie 不定义在方法中,而是定义在方法外呢?
作者回复: 后面会讲到,go的错误处理是基于值比较的。之所以将ErrNoCookie放在包级而不是函数/方法内部,是因为需要将ErrNoCookie暴露给包的使用者。这样包的使用者才能通过ErrNoCookie与方法返回的错误值进行比较以判断是否是这个错误。
补充
Ransang:
貌似源码中以err开头的变量都被定义为包级变量了,可能是一种约定或者默认行为
5. 网友-罗杰 C 和 C++ 都是类型在前,变量名在后,记得非常清楚 CCMouse 老师在视频课上提到这才是正确的方法,但到现在其实我还没太明白原因。不过在 Rust 中采用了和 Go 相同的方式。所以希望老师解答一下原因
作者回复: 其实官方早有正解,https://go.dev/blog/declaration-syntax 。我个人以前是C程序员,在读懂那些复杂函数的原型时,颇为费力。换做go之后,再未被这类问题困扰过:)
6. 网友-AlexWillBeGood 老师好,请问“静态语言明确声明类型”会在编译阶段或运行阶段提高效率吗?
作者回复: 我觉得与效率无关。静态语言的类型声明是静态语言的机制所要求的。静态语言原本运行效率就是要比动态语言要高的,因为它已经编译为机器码了,无需开销较大的解释过程就可以运行了。
7. 网友-Goopand 请教:关于包级变量 (package varible),只要被导入(import)过一次,这个变量就会一直常驻在内存中,直到main函数执行结束、退出,这样理解对吗?
作者回复: 正确
8. 网友-Geek_bb6b26 局部变量特指仅在语句块(if,for)内定义使用的变量吗?还是仅在方法内定义使用的就是局部变量?
作者回复: 每个大括号括起的代码块中都可以定义局部变量。
9. 网友-Vfeelit a, e := a() 然后 b, e := b() 这里的 e 算不算声明两次?
作者回复: 同一代码块内先后放置上面两个语句,e不算声明两次。你可以输出e的地址证明这点。两个e的地址是相同的。
补充
ning先森
验证
func justtest(){ a, e := "william", "ning" println(a, e, &e)
b, e := "william1", "ning1" println(a, e, b, &e) }
输出 ➜ hellomodule go run . william ning 0xc0000c9e28 william ning1 william1 0xc0000c9e28 ➜ hellomodule
可知:不算声明两次,后一次相当于赋值. 另外,变量e的内存地址是相同的。
------------------------------------------------------------分隔符------------------------------------------------------------ 36 | unicode与字符编码-极客时间 1. 网友-lesserror 郝林老师,请问一下:“基于混合线程的并发编程模型”。这句话该怎么理解呢?
作者回复:
这里所谓的混合线程就是:OS内核外的线程/协程 + OS内核里的系统线程。
它牵扯到了两个程序级别,一个是用户级,一个是内核级,所以也叫“混合”或“两级”的线程模型。从数量对应的角度讲,它也叫M:N的线程模型(M指核外线程数量,N指核内线程数量),即两者之间是动态匹配的。你想想看,goroutine和系统线程是不是动态匹配的。Go语言的并发模型中不止有混合线程,但底层是基于此的。
相对应的,Java用的是1:1的纯内核级线程模型,也就是说JVM中的一个线程就等同于内核中的一个系统线程,一一对应。
还有就是,Python的绿色线程,是M:1的纯用户级线程模型。同进程内的多个绿色线程实际上共用一个线程,它们并不是真的并发执行(共用一个线程不可能出现并发执行的情况),只是通过调度看起来像并发执行而已。
2. 网友-党 一个unicode字符点都是由两个字节存储,为什么在go语言中 type rune = int32 四个字节 而不是 type rune=16 两个字节啊
作者回复: Unicode 代码点是一个抽象的概念,并且没有规定占用的字节数,只是说以“U+XXXX”的形式来表示。“X”还可能有 5 个或 6 个。
Unicode 代码点是 Unicode 编码标准的一部分。但你要分清楚编码标准和编码格式(像 UTF-8、UTF-16 等等)的区别。前者是概念和表示法,后者是真正的落地格式。编码格式才是真正与存储占用(比如一个字符占用多少个字节)有关的东西。
rune 类型的宽度是 4 个字节,那是因为 Go 语言使用 UTF-8 编码格式。这种格式编出来的码最多占用 4 个字节。
------------------------------------------------------------分隔符------------------------------------------------------------ ------------------------------------------------------------分隔符------------------------------------------------------------ ------------------------------------------------------------分隔符------------------------------------------------------------ ------------------------------------------------------------分隔符------------------------------------------------------------ 后续补充 ... |