我的编程空间,编程开发者的网络收藏夹
学习永远不晚

Golang如何实现一个不可复制类型

短信预约 -IT技能 免费直播动态提醒
省份

北京

  • 北京
  • 上海
  • 天津
  • 重庆
  • 河北
  • 山东
  • 辽宁
  • 黑龙江
  • 吉林
  • 甘肃
  • 青海
  • 河南
  • 江苏
  • 湖北
  • 湖南
  • 江西
  • 浙江
  • 广东
  • 云南
  • 福建
  • 海南
  • 山西
  • 四川
  • 陕西
  • 贵州
  • 安徽
  • 广西
  • 内蒙
  • 西藏
  • 新疆
  • 宁夏
  • 兵团
手机号立即预约

请填写图片验证码后获取短信验证码

看不清楚,换张图片

免费获取短信验证码

Golang如何实现一个不可复制类型

这篇文章主要讲解了“Golang如何实现一个不可复制类型”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“Golang如何实现一个不可复制类型”吧!

    如何复制一个对象

    不考虑IDE提供的代码分析和go vet之类的静态分析工具,golang里几乎所有的类型都能被复制。

    // 基本标量类型和指针var i int = 1iCopy := istr := "string"strCopy := str pointer := &ipointerCopy := pointeriCopy2 := *pointer // 解引用后进行复制 // 结构体和数组arr := [...]int{1, 2, 3}arrCopy := arr type Obj struct {    i int}obj := Obj{}objCopy := obj

    除了这些,golang还有函数和引用类型(slice、map、interface),这些类型也可以被复制,但稍有不同:

    func f() {...}f1 := ff2 := f1 fmt.Println(f1, f2) // 0xabcdef 0xabcdef 打印出来的值是一样的fmt.Println(&f1 == &f2) // false 虽然值一样,但确实是两个不同的变量

    这里并没有真正复制处三份f的代码,f1和f2均指向f,f的代码始终只会有一份。map、slice和interface与之类似:

    m := map[int]string{    0: "a",    1: "b",}mCopy := m // 两者引用同样的数据mCopy[0] := "unknown"m[0] == "unknown" // True// slice的复制和map相同

    interface是比较另类的,它的行为要分两种情况:

    s := "string"var i1 any = svar i2 any = s// 当把非指针和接口类型的值赋值给interface,会导致原来的对象被复制一份 s := "string"var i1 any = svar i2 any = i2// 当把接口赋值给接口,底层引用的数据不会被复制,i1会复制s,i2此时和i1共有一个s的副本 ss := "string but pass by pointer"var i3 any = &ssvar i4 any = i3// i3和i4均引用ss,此时ss没有被复制,但指向ss的指针的值被复制了两次

    上面的结果会一定程度上被编译优化干扰,比如少数情况下编译器可以确认赋值给接口的值从来没被修改并且生命周期不比源对象长,则可能不会进行复制。

    所以这里有个小提示:如果要赋值给接口的数据比较大,那么最好以指针的形式赋值给接口,复制指针比复制大量的数据更高效。

    为什么要禁止复制

    从上一节可以看到,允许复制时会在某些情况下“闯祸”。比如:

    浅拷贝的问题很容易出现,比如例子里的map和slice的浅拷贝问题,这可能会导致数据被意外修改

    意外复制了大量数据,导致性能问题

    在需要共享状态的地方错误的使用了副本,导致状态不一致从而产生严重问题,比如sync.Mutex,复制一个锁并使用其副本会导致死锁

    根据业务或者其他需求,某类型的对象只允许存在一个实例,这时复制显然是被禁止的

    显然在一些情况下禁止复制是合情合理的,这也是为什么我会写这篇文章。

    但具体情况具体分析,不是说复制就是万恶之源,什么时候该支持复制,什么时候应该禁止,应该结合自己的实际情况。

    运行时检测实现禁止复制

    想在别的语言中禁止某个类型被复制,方法有很多,用c++举一例:

    struct NoCopy {    NoCopy(const NoCopy &) = delete;    NoCopy &operator=(const NoCopy &) = delete;};

    可惜在golang里不支持这么做。

    另外,因为golang没有运算符重载,所以很难在赋值的阶段就进行拦截,所以我们的侧重点在于“复制之后可以尽快检测到”。

    所以我们先实现在对象被复制后报错的功能。虽然不如c++编译期就可以禁止复制那样优雅,但也算实现了功能,至少不什么都没有要强一些。

    初步尝试

    那么如何直到对象是否被复制了?很简单,看它的地址就行了,地址一样那必然是同一个对象,不一样了那说明复制出一个新的对象了。

    顺着这个思路,我们需要一个机制来保存对象第一次创建时的地址,并在后续进行比较,于是第一版代码诞生了:

    import "unsafe" type noCopy struct {    p uintptr} func (nc *noCopy) check() {    if uintptr(unsafe.Pointer(nc)) != nc.p {        panic("copied")    }}

    逻辑比较清晰,每次调用check来检查当前的调用者的地址和保存地址是否相同,如果不同就panic。

    为什么没有创建这个类型的方法?因为我们没法得知自己被其他类型创建时的地址,所以这块得让其他使用noCopy的类型代劳。

    使用的时候需要把noCopy嵌入自己的struct,注意不能以指针的形式嵌入:

    type SomethingCannotCopy struct {    noCopy    ...} func (s *SomethingCannotCopy) DoWork() {    s.check()    fmt.Println("do something")} func NewSomethingCannotCopy() *SomethingCannotCopy {    s := &SomethingCannotCopy{        // 一些初始化    }    // 绑定地址    s.noCopy.p = unsafe.Pointer(&s.noCopy)    return s}

    注意初始化部分的代码,在这里我们需要把noCopy对象的地址绑定进去。现在可以实现运行时检测了:

    func main() {    s1 := NewSomethingCannotCopy()    pointer := s1    s1Copy := *s1 // 这里实际上进行了复制,但需要调用方法的时候才能检测到    pointer.DoWork() // 正常打印出信息    s1Copy.DoWork() // panic}

    解释下原理:当SomethingCannotCopy被复制的时候,noCopy也会被复制,因此复制出来的noCopy的地址和原先的那个是不一样的,但他们内部记录的p是一样的,这样当被复制出来的noCopy对象调用check方法的时候就会触发panic。这也是为什么不要用指针形式嵌入它的原因。

    功能实现了,但代码实在是太丑,而且耦合严重:只要用了noCopy,就必须在创建对象的同时初始化noCopy的实例,noCopy的初始化逻辑会侵入到其他对象的初始化逻辑中,这样的设计是不能接受的。

    更好的实现

    那么有没有更好的实现?答案是有的,而且在标准库里。

    标准库的信号量sync.Cond是禁止复制的,而且比Mutex更为严格,因为复制它比复制锁更容易导致死锁和崩溃,所以标准库加上了运行时的动态检查。

    主要代码如下:

    type Cond struct {    // L is held while observing or changing the condition    L Locker    ...    // 复制检查    checker copyChecker} // NewCond returns a new Cond with Locker l.func NewCond(l Locker) *Cond {        return &Cond{L: l}} func (c *Cond) Signal() {    // 检查自己是否被复制    c.checker.check()    runtime_notifyListNotifyOne(&c.notify)}

    checker实现了运行时检测是否被复制,但初始化的时候并不需要特殊处理这个checker,这是用了什么手法做到的呢?

    看代码:

    type copyChecker uintptr func (c *copyChecker) check() {    if uintptr(*c) != uintptr(unsafe.Pointer(c)) && // step 1            !atomic.CompareAndSwapUintptr((*uintptr)(c), 0, uintptr(unsafe.Pointer(c))) && // step 2            uintptr(*c) != uintptr(unsafe.Pointer(c)) { //step 3        panic("sync.Cond is copied")    }}

    看着很复杂,连原子操作都来了,这都是啥啊。但别怕,我给你捋一捋就明白了。

    首先是checker初始化之后第一次调用:

    • 当check第一次被调用,c的值肯定是0,而这时候c是有真实的地址的,所以step 1失败,进入step 2;

    • 用原子操作把c的值设置成自己的地址值,注意只有c的值是0的时候才能完成设置,因为这里c的值是0,所以交换成功,step 2是False,判断流程直接结束;

    • 因为不排除还有别的goroutine拿着这个checker在做检测,所以step 2是会失败的,这是要进入step 3;

    • step 3再次比较c的值和它自己的地址是否相同,相同说明多个goroutine共用了一个checker,没有发生复制,所以检测通过不会panic。

    • 如果step 3的比较发现不相等,那么说明被复制了,直接panic

    然后我们再看其他情况下checker的流程:

    • 这时候c的值不是0,如果没发生复制,那么step 1的结果是False,判断流程结束,不会panic;

    • 如果c的值和自己的地址不一样,会进入step 2,因为这里c的值不为0,所以表达式结果一定是True,所以进入step 3;

    • step 3和step 1一样,结果是True,地址不同说明被复制,这时候if里面的语句会执行,因此panic。

    搞得这么麻烦,其实就是为了能干干净净地初始化。这样任何类型都只需要带上checker作为自己的字段就行,不用关心它是这么初始化的。

    还有个小问题,为什么设置checker的值需要原子操作,但读取就不用呢?

    因为读取一个uintptr的值,在现代的x86和arm处理器上只要一个指令,所以要么读到过时的值要么读到最新的值,不会读到错误的或者写了一半的不完整的值,对于读到旧值的情况(主要出现在第一次调用check的时候),还有step 3做进一步的检查,因此不会影响整个检测逻辑。而“比较并交换”显然一条指令做不完,如果在中间步骤被打断那么整个操作的结果很可能就是错的,从而影响整个检测逻辑,所以必须要用原子操作才行。

    那么在读取的时候也使用atomic.Load行吗?当然行,但一是这么做仍然避免不了step 3的检测,可以思考下是为什么;二是原子操作相比直接读取会带来性能损失,在这里不使用原子操作也能保证正确性的情况下这是得不偿失的。

    性能

    因为是运行时检测,所以我们得看看会对性能带来多少影响。我们使用改进版的checker。

    type CheckBench struct {    num uint64    checker copyChecker} func (c *CheckBench) CheckCopy() {    c.checker.check()    c.num++} // 不进行检测func (c *CheckBench) NoCheck() {    c.num++} func BenchmarkCheckBench_NoCheck(b *testing.B) {    c := CheckBench{}    for i := 0; i < b.N; i++ {        for j := 0; j < 50; j++ {            c.NoCheck()        }    }} func BenchmarkCheckBench_WithCheck(b *testing.B) {    c := CheckBench{}    for i := 0; i < b.N; i++ {        for j := 0; j < 50; j++ {            c.CheckCopy()        }    }}

    测试结果如下:

    cpu: Intel(R) Core(TM) i5-10200H CPU @ 2.40GHz
    BenchmarkCheckBench_NoCheck-8           17689137                68.36 ns/op
    BenchmarkCheckBench_WithCheck-8         17563833                66.04 ns/op

    几乎可以忽略不计,因为我们这里没有发生复制,所以几乎每次检测都是通过的,这对cpu的分支预测非常友好,所以性能损耗几乎可以忽略。

    所以我们给cpu添点堵,让分支预测没那么容易:

    func BenchmarkCheckBench_WithCheck(b *testing.B) {    for i := 0; i < b.N; i++ {        c := &CheckBench{}        for j := 0; j < 50; j++ {            c.CheckCopy()        }    }} func BenchmarkCheckBench_NoCheck(b *testing.B) {    for i := 0; i < b.N; i++ {        c := &CheckBench{}        for j := 0; j < 50; j++ {            c.NoCheck()        }    }}

    现在分支预测没那么容易了而且要多付出初始化时使用atomic的代价,测试结果会变成这样:

    cpu: Intel(R) Core(TM) i5-10200H CPU @ 2.40GHz
    BenchmarkCheckBench_WithCheck-8         15552717                74.84 ns/op
    BenchmarkCheckBench_NoCheck-8           26441635                44.74 ns/op

    差不多会慢40%。当然,实际的代码不会有这么极端,所以最坏可能也只会产生20%的影响,通常不太会成为性能瓶颈,运行时检测是否有影响还需结核profile。

    优点和缺点

    优点:

    • 只要调用check,肯定能检查出是否被复制

    • 简单

    缺点:

    • 所有的方法里都需要调用check,新加方法忘了调用的话就无法检测

    • 只能在被复制出来的新对象那检测到复制操作,原先那个对象上check始终是没问题的,这样不是严格禁止了复制,但大多数时间没问题,可以接受

    • 如果只复制了对象没调用任何对象上的方法,也无法检测到复制,这种情况比较少见

    • 有潜在性能损耗,虽然很多时候可以得到充分优化损耗没那么夸张

    静态检测实现禁止复制

    动态检测的缺点不少,能不能像c++那样编译期就禁止复制呢?

    利用Locker接口不可复制实现静态检测

    也可以,但得配合静态代码检测工具,比如自带的go vet。看下代码:

    // 实现sync.Locker接口type noCopy struct{}func (*noCopy) Lock() {}func (*noCopy) Unlock() {} type SomethingCannotCopy struct {    noCopy}

    这样就行了,不需要再添加其他的代码。解释下原理:任何实现了sync.Locker的类型都不应该被拷贝,静态代码检测会检测出这些情况并报错。

    所以类似下边的代码都是无法通过静态代码检测的:

    func f(s SomethingCannotCopy) {    // 报错,因为参数会导致复制    // 返回SomethingCannotCopy也是不行的} func (s SomethingCannotCopy) Method() {    // 报错,因为非指针类型接收器会导致复制} func main() {    s := SomethingCannotCopy{}    sCopy := s // 报错    sInterface := any(s) // 报错    sPointer := &s // OK    sCopy2 := *sPointer // 报错    sInterface2 := any(sPointer) // OK    sCopy3 := *(sInterface2.(*SomethingCannotCopy)) // 报错}

    基本上涵盖了所以会产生复制操作的地方,基本能在编译期完成检测。

    如果跳过go vet,直接使用go run或者go build,那么上面的代码可以正常编译并运行。

    优点和缺点

    因为只有静态检测,因此没有什么运行时开销,所以性能这节就不需要费笔墨了。主要来看下这种方案的优缺点。

    优点:

    • 实现非常简单,代码很简练,基本无侵入性

    • 依赖静态检测,不影响运行时性能

    • golang自带检测工具:go vet

    • 可检测到的case比运行时检测多

    缺点:

    • 最大的缺点,尽管静态检测会报错,但仍然可以正常编译执行

    • 不是每个测试环境和CI都配备了静态检测,所以很难强制保证类型没有被复制

    • 会导致类型实现sync.Locker,然而很多时候我们的类型并不是类似锁的资源,使用这个接口只是为了静态检测,这会带来代码被误用的风险

    标准库也使用的这套方案,建议仔细阅读这个issue里的讨论。

    更进一步

    看过运行时检测和静态检测两种方案之后,我们会发现这些做法多少都有些问题,不尽如人意。

    所以我们还是要追求一种更好用的,更符合golang风格的做法。幸运的是,这样的做法是存在的。

    利用package和interface进行封装

    首先我们创建一个worker包,里面定义一个Worker接口,包中的数据对外以Worker接口的形式提供:

    package worker import (    "fmt") // 对外只提供接口来访问数据type Worker interface {    Work()} // 内部类型不导出,以接口的形式供外部使用type normalWorker struct {    // data members}func (*normalWorker) Work() {    fmt.Println("I am a normal worker.")}func NewNormalWorker() Worker {    return &normalWorker{}} type specialWorker struct {    // data members}func (*specialWorker) Work() {    fmt.Println("I am a special worker.")}func NewSpecialWorker() Worker {    return &specialWorker{}}

    worker包对外只提供Worker接口,用户可以使用NewNormalWorker和NewSpecialWorker来生成不同种类的worker,用户不需要关心具体的返回类型,只要使用得到的Worker接口即可。

    这么做的话,在worker包之外是看不到normalWorker和specialWorker这两个类型的,所以没法靠反射和类型断言取出接口引用的数据;因为我们传给接口的是指针,因此源数据不会被复制;同时我们在第一节提到过,把一个接口赋值给另一个接口(worker包之外你只能这么做),底层被引用的数据不会被复制,因此在包外始终不会在这两个类型上产生复制的行为。

    因此下面这样的代码是不可能通过编译的:

    func main() {    w := worker.NewSpecialWorker()    // worker.specialWorker 在worker包以外不可见,因此编译错误    wCopy := *(w.(*worker.specialWorker))    wCopy.Work()}

    优点和缺点

    这样就实现了worker包之外的禁止复制,下面来看看优缺点。

    优点:

    • 不需要额外的静态检查工具在编译代码前执行检查

    • 不需要运行时动态检测是否被复制

    • 不会实现自己不需要的接口类型导致污染方法集

    • 符合golang开发中的习惯做法

    缺点:

    • 并没有让类型本身不可复制,而是靠封装屏蔽了大部分可能导致复制的情况

    • 这些worker类型在包内是可见的,如果在包内修改代码时不注意可能会导致复制这些类型的值,所以要么包内也都用Woker接口,要么参考上一节添加静态检查

    • 有些场景下不需要接口或者因为性能要求苛刻而使用不了接口,这种做法就行不通了,比如标准库sync里的类型为了性能大部分都是暴露出来给外部直接使用的

    综合来说,这种方案是实现成本最低的。

    感谢各位的阅读,以上就是“Golang如何实现一个不可复制类型”的内容了,经过本文的学习后,相信大家对Golang如何实现一个不可复制类型这一问题有了更深刻的体会,具体使用情况还需要大家实践验证。这里是编程网,小编将为大家推送更多相关知识点的文章,欢迎关注!

    免责声明:

    ① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

    ② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

    Golang如何实现一个不可复制类型

    下载Word文档到电脑,方便收藏和打印~

    下载Word文档

    猜你喜欢

    Golang如何实现一个不可复制类型

    这篇文章主要讲解了“Golang如何实现一个不可复制类型”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“Golang如何实现一个不可复制类型”吧!如何复制一个对象不考虑IDE提供的代码分析和g
    2023-07-05

    Golang拾遗之实现一个不可复制类型详解

    在这篇文章中我们将实现一个无法被复制的类型,顺便加深对引用类型、值传递以及指针的理解。文中的示例代码讲解详细,感兴趣的可以了解一下
    2023-02-20

    利用golang怎么实现一个强制类型转换功能

    这篇文章给大家介绍利用golang怎么实现一个强制类型转换功能,内容非常详细,感兴趣的小伙伴们可以参考借鉴,希望对大家能有所帮助。golang的优点有哪些golang是一种编译语言,可以将代码编译为机器代码,编译后的二进制文件可以直接部署到
    2023-06-06

    Golang如何实现不被复制的结构体

    这篇文章主要介绍“Golang如何实现不被复制的结构体”的相关知识,小编通过实际案例向大家展示操作过程,操作方法简单快捷,实用性强,希望这篇“Golang如何实现不被复制的结构体”文章能帮助大家解决问题。不允许复制的结构体sync包中的许多
    2023-07-05

    Vue如何实现一个可复用组件

    本篇内容主要讲解“Vue如何实现一个可复用组件”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“Vue如何实现一个可复用组件”吧!构成组件组件,是一个具有一定功能,且不同组件间功能相对独立的模块。组
    2023-07-04

    详解如何实现一个Kotlin函数类型

    这篇文章主要为大家详细介绍了如何实现一个Kotlin函数类型,文中的实现方法讲解详细,具有一定的借鉴价值,需要的小伙伴可以跟随小编一起学习一下
    2022-11-13

    Golang中怎么实现一个不可重入函数

    Golang中怎么实现一个不可重入函数,相信很多没有经验的人对此束手无策,为此本文总结了问题出现的原因和解决方法,通过这篇文章希望你能解决这个问题。场景用例某个服务是对某些条件进行轮询,每秒监视一些状态。我们希望每个状态都可以独立地检查,而
    2023-06-04

    3dmax如何复制模型到另一个文件

    这篇文章主要介绍了3dmax如何复制模型到另一个文件的相关知识,内容详细易懂,操作简单快捷,具有一定借鉴价值,相信大家阅读完这篇3dmax如何复制模型到另一个文件文章都会有所收获,下面我们一起来看看吧。3dma
    2023-03-15

    如何使用TypeScript实现一个类型安全的EventBus

    这篇文章主要介绍“如何使用TypeScript实现一个类型安全的EventBus”,在日常操作中,相信很多人在如何使用TypeScript实现一个类型安全的EventBus问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对
    2023-07-02

    java如何实现类型转换与强制类型转换

    这篇文章主要介绍了java如何实现类型转换与强制类型转换,具有一定借鉴价值,感兴趣的朋友可以参考下,希望大家阅读完这篇文章之后大有收获,下面让小编带着大家一起了解一下。java类型转换与强制类型转换如果你以前有编程经验,那么你已经知道把一种
    2023-06-03

    Vue如何用枚举类型实现一个HTML下拉框

    本篇内容主要讲解“Vue如何用枚举类型实现一个HTML下拉框”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“Vue如何用枚举类型实现一个HTML下拉框”吧!第一步: 编写下拉框需要的枚举类型 S
    2023-07-04

    如何用Vue3实现可复制表格

    这篇“如何用Vue3实现可复制表格”文章的知识点大部分人都不太理解,所以小编给大家总结了以下内容,内容详细,步骤清晰,具有一定的借鉴价值,希望大家阅读完这篇文章能有所收获,下面我们一起来看看这篇“如何用Vue3实现可复制表格”文章吧。最基础
    2023-07-04

    如何使用 Golang 函数类型实现回调?

    是的,在 go 中可以使用函数类型实现回调功能,具体步骤如下:声明一个函数类型,指定回调函数的签名。定义一个接受函数类型作为参数的函数。将需要回调的函数传递给该函数。使用 Golang 函数类型实现回调在 Go 中,函数类型允许您将函数声
    如何使用 Golang 函数类型实现回调?
    2024-04-22

    PHP一个类调用另一个类的方法如何实现

    本篇内容主要讲解“PHP一个类调用另一个类的方法如何实现”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“PHP一个类调用另一个类的方法如何实现”吧!在面向对象编程中,类与类之间的依赖关系很常见。当
    2023-07-06

    利用SpringMVC如何实现一个自定义类型转换器

    这篇文章将为大家详细讲解有关利用SpringMVC如何实现一个自定义类型转换器,文章内容质量较高,因此小编分享给大家做个参考,希望大家阅读完这篇文章后对相关知识有一定的了解。我们在使用SpringMVC时,常常需要把表单中的参数映射到我们对
    2023-05-31

    golang函数中的类型断言是如何实现的?

    类型断言通过以下步骤实现:编译器生成包含类型信息和方法表的 runtime.type 结构体。对一个值进行类型断言时,编译器检查其 runtime.type 结构体是否与目标类型匹配。匹配成功时,类型断言成功,ok 为 true,并提取值。
    golang函数中的类型断言是如何实现的?
    2024-05-23

    编程热搜

    • Python 学习之路 - Python
      一、安装Python34Windows在Python官网(https://www.python.org/downloads/)下载安装包并安装。Python的默认安装路径是:C:\Python34配置环境变量:【右键计算机】--》【属性】-
      Python 学习之路 - Python
    • chatgpt的中文全称是什么
      chatgpt的中文全称是生成型预训练变换模型。ChatGPT是什么ChatGPT是美国人工智能研究实验室OpenAI开发的一种全新聊天机器人模型,它能够通过学习和理解人类的语言来进行对话,还能根据聊天的上下文进行互动,并协助人类完成一系列
      chatgpt的中文全称是什么
    • C/C++中extern函数使用详解
    • C/C++可变参数的使用
      可变参数的使用方法远远不止以下几种,不过在C,C++中使用可变参数时要小心,在使用printf()等函数时传入的参数个数一定不能比前面的格式化字符串中的’%’符号个数少,否则会产生访问越界,运气不好的话还会导致程序崩溃
      C/C++可变参数的使用
    • css样式文件该放在哪里
    • php中数组下标必须是连续的吗
    • Python 3 教程
      Python 3 教程 Python 的 3.0 版本,常被称为 Python 3000,或简称 Py3k。相对于 Python 的早期版本,这是一个较大的升级。为了不带入过多的累赘,Python 3.0 在设计的时候没有考虑向下兼容。 Python
      Python 3 教程
    • Python pip包管理
      一、前言    在Python中, 安装第三方模块是通过 setuptools 这个工具完成的。 Python有两个封装了 setuptools的包管理工具: easy_install  和  pip , 目前官方推荐使用 pip。    
      Python pip包管理
    • ubuntu如何重新编译内核
    • 改善Java代码之慎用java动态编译

    目录