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

Golang协程泄露怎么预防

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

北京

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

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

看不清楚,换张图片

免费获取短信验证码

Golang协程泄露怎么预防

这篇文章主要介绍了Golang协程泄露怎么预防,具有一定借鉴价值,感兴趣的朋友可以参考下,希望大家阅读完这篇文章之后大有收获,下面让小编带着大家一起了解一下。

泄露案例

关于协程泄露很多时候我们往往会忽略它,直到机器资源负载异常才引起重视。之前排除生产环境异常的时候,曾经遇到过go程序内存泄露的场景,内存泄漏和协程泄露有很大关系,本质上都是资源不回收导致的。

这里列举一个典型的泄露案例:

func JumpForSignal() int {    ch := make(chan int)    go func() {        ch <- bizMtx    }()    go func() {        ch <- bizMtx    }()    go func() {        ch <- bizMtx    }()    //一有输入立刻返回    return <-ch}func main() {    // ...    JumpForSignal()    // ...}

事后分析这个demo可以得知,这个函数调用会阻塞两个子协程,预期只有一个协程会正常退出。

获取协程信息

既然存在协程泄露,我们在日常工作怎么避免或者发现它呢?下面我们列举几个思路。

遵守准则

由于Go是自带GC的语言,很多时候写代码不需要关心变量的资源释放,不像C程序员变量申请之后需要在结束处释放。但是Go的chan在使用时候是有一些准则的,当确定chan不再使用时候,可以在输出方进行close,避免其他协程还在等待该chan的输出。

协程数量

找到泄露的协程,第一个能够想到的是协程数量,当你的函数处理逻辑比较简单,除了主协程之外,预期协程应该都在结束前返回,可以在main函数结束处调用runtime包的函数:

// NumGoroutine returns the number of goroutines that currently exist.func NumGoroutine() int {    return int(gcount())}

通过它可以返回当前协程总数量:

func Count()  {    fmt.Printf("Number of goroutines:%d\n", runtime.NumGoroutine())}func main() {    defer Count()    Count()    JumpForSignal()}

输出:

Number of goroutines:1Number of goroutines:3

协程函数栈

还有一种比较常见定位协程的形式,在Go里面,可以用于分析协程函数的上下文,常见的比如go自带的pprof也是通过这种方式获取,实际案例中,条件允许的情况可以开启pprof方便分析。

下面来看一个示例,我们在上面的例子加一个http端口监听,用于接入go自带的pprof分析工具。

随后在浏览器输入:

http://localhost:8899/debug/pprof/goroutine?debug=1

可以得到整个程序的协程列表:

goroutine profile: total 71 @ 0x165eb6 0x126465 0x126235 0x29341e 0x19de01#0x29341dpixelgo/leak.JumpForSignal.func1+0x3dF:/code/pixelGo/class="lazy" data-src/pix-demo/leak/leak.go:241 @ 0x165eb6 0x126465 0x126235 0x29347e 0x19de01#0x29347dpixelgo/leak.JumpForSignal.func2+0x3dF:/code/pixelGo/class="lazy" data-src/pix-demo/leak/leak.go:281 @ 0x165eb6 0x15bb3d 0x1975a5 0x228d05 0x229d8d 0x22c40d 0x321765 0x33437c 0x447c89 0x285239 0x285606 0x4493f3 0x450da8 0x19de01#0x1975a4internal/poll.runtime_pollWait+0x64D:/dev/go1.16/class="lazy" data-src/runtime/netpoll.go:227#0x228d04internal/poll.(*pollDesc).wait+0xa4D:/dev/go1.16/class="lazy" data-src/internal/poll/fd_poll_runtime.go:87#0x229d8cinternal/poll.execIO+0x2acD:/dev/go1.16/class="lazy" data-src/internal/poll/fd_windows.go:175#0x22c40cinternal/poll.(*FD).Read+0x56c// ...

结论是:当前程序一共有7个协程,可以看出分别有1个协程分配在F:/code/pixelGo/class="lazy" data-src/pix-demo/leak/leak.go:24F:/code/pixelGo/class="lazy" data-src/pix-demo/leak/leak.go:28,正是上文泄露的代码块。

有时候还可以多维度去分析,比如输入:

http://localhost:8899/debug/pprof/goroutine?debug=2

可以通过协程后面的标签,看到当前协程的不同状态,running/io wait/chan send

goroutine 9 [running]:runtime/pprof.writeGoroutineStacks(0x7f7d00, 0xc0000aa000, 0x0, 0x0)D:/dev/go1.16/class="lazy" data-src/runtime/pprof/pprof.go:693 +0xc5net/http/pprof.handler.ServeHTTP(0xc000094011, 0x9, 0x7fba40, 0xc0000aa000, 0xc000092000)    //..goroutine 1 [IO wait]:internal/poll.runtime_pollWait(0x223debb10d8, 0x72, 0xc000152f48)D:/dev/go1.16/class="lazy" data-src/runtime/netpoll.go:227 +0x65internal/poll.(*pollDesc).wait(0xc0001530b8, 0x72, 0x93b400, 0x0, 0x0)    //...goroutine 6 [chan send]:pixelgo/rout.JumpForSignal.func1(0xc000053800)F:/code/pixelGo/class="lazy" data-src/pix-demo/rout/leak.go:25 +0x10ecreated by pixelgo/rout.JumpForSignalF:/code/pixelGo/class="lazy" data-src/pix-demo/rout/leak.go:23 +0x71goroutine 7 [chan send]:pixelgo/rout.JumpForSignal.func2(0xc000053800)F:/code/pixelGo/class="lazy" data-src/pix-demo/rout/leak.go:30 +0x10ecreated by pixelgo/rout.JumpForSignalF:/code/pixelGo/class="lazy" data-src/pix-demo/rout/leak.go:28 +0x93

协程id

接下来我们来探索协程标识:协程id,在Go中,每个运行的协程都会分配一个协程id,一个常见的方式是从函数运行栈获取,引用之前网上其他同学的写法:

func main() {    fmt.Println(getGID())}func getGID() uint64 {    b := make([]byte, 64)    b = b[:runtime.Stack(b, false)]    b = bytes.TrimPrefix(b, []byte("goroutine "))    b = b[:bytes.IndexByte(b, ' ')]    n, _ := strconv.ParseUint(string(b), 10, 64)    return n}

我们来看看runtime.stack() 会返回什么呢,其中真实内容是这样的:

goroutine 21 [running]:leaktest.interestingGoroutines(0xdb9980, 0xc00038e018, 0x0, 0x0, 0x0)F:/code/pixelGo/class="lazy" data-src/leaktest/leaktest.go:81 +0xbfleaktest.CheckContext(0xdbe398, 0xc000108040, 0xdb9980, 0xc00038e018, 0x0)F:/code/pixelGo/class="lazy" data-src/leaktest/leaktest.go:141 +0x6eleaktest.CheckTimeout(0xdb9980, 0xc00038e018, 0x3b9aca00, 0x0)F:/code/pixelGo/class="lazy" data-src/leaktest/leaktest.go:127 +0xe5leaktest.TestCheck.func8(0xc000384780)F:/code/pixelGo/class="lazy" data-src/leaktest/leaktest_test.go:122 +0xaftesting.tRunner(0xc000384780, 0xc000100050)D:/dev/go1.16/class="lazy" data-src/testing/testing.go:1193 +0x1a3created by testing.(*T).RunD:/dev/go1.16/class="lazy" data-src/testing/testing.go:1238 +0x63cgoroutine 1 [chan receive]:testing.(*T).Run(0xc000037080, 0xd8486a, 0x9, 0xd9ebc8, 0x304bd824304bd800)D:/dev/go1.16/class="lazy" data-src/testing/testing.go:1239 +0x66atesting.runTests.func1(0xc000036f00)D:/dev/go1.16/class="lazy" data-src/testing/testing.go:1511 +0xbdtesting.tRunner(0xc000036f00, 0xc00008fc00)D:/dev/go1.16/class="lazy" data-src/testing/testing.go:1193 +0x1a3testing.runTests(0xc0000040d8, 0xf40460, 0x5, 0x5, 0x0, 0x0, 0x0, 0x21cbf1c0100)D:/dev/go1.16/class="lazy" data-src/testing/testing.go:1509 +0x448testing.(*M).Run(0xc0000c0000, 0x0)D:/dev/go1.16/class="lazy" data-src/testing/testing.go:1417 +0x514main.main()_testmain.go:51 +0xc8

可以发现这个栈和我们运行panic抛出的信息非常类似,需要注意的是,通过这种方式获取协程id并不是一个高效的方式。
实际生产使用过程并不提倡,值得一提的是,为了方便我们更好的定位问题上下文,有时候日志框架又需要我们打印出当前协程id。

比如这是一个生产案例日志输出:

// gid-1号协程用于初始化资源[0224/162532.310:INFO:gid-1:yx_trace.go:66] cfg:&{ false false [] 0xc000295140 0xc0001d4e00 <nil> <nil> <nil>}[0224/162532.320:INFO:gid-1:main.go:50] GameRoom Startup->[0224/162532.320:INFO:gid-1:config_manager.go:107] configManager SetHttpListenAddr:8080[0224/162532.320:INFO:gid-1:room_manager.go:57] roomManager Startup[0224/162532.323:INFO:gid-1:room_manager.go:72] roomManager initPrx.[0224/162532.330:INFO:gid-1:bootstrap.go:153] GameRoom START ok.// gid-60号协程分配用于启动HTTP Server[0224/162533.277:INFO:gid-60:expose.go:36] Start for HTTP server...[0224/162533.277:INFO:gid-60:expose.go:39] register for debug server...

往往日志框架是力求对业务性能影响最低的,既然有性能顾虑,那么它是怎么获取协程id的呢?只能曲线救国了。
还有一个解法,其实在Go中,每个协程绑定的系统线程结构中,有一个g指针,拿到g指针的信息之后,根据g指针结构的偏移量(注意不同go版本可能不同),指定获取id。

汇编获取

通过协程绑定的g指针,这里参考《Go高级编程》的做法

// 记录各个版本的偏移量var offsetDictMap = map[string]int64{        "go1.12":    152,        "go1.12.1":  152,        "go1.12.2":  152,        "go1.12.3":  152,        "go1.12.4":  152,        "go1.12.5":  152,        "go1.12.6":  152,        "go1.12.7":  152,        "go1.13":    152,        "go1.14":    152,        "go1.16.12":    152,}// offset for go1.12var goid_offset uintptr = 152//go:nosplitfunc getG() interface{}func GoId() int64// 部分汇编代码// func getGptr() unsafe.PointerTEXT ·getGptr(SB), NOSPLIT, $0-8    MOVQ (TLS), BX    MOVQ BX, ret+0(FP)    RETTEXT ·GoId(SB),NOSPLIT,$0-8    NO_LOCAL_POINTERS    MOVQ ·goid_offset(SB),AX    // get runtime.g    MOVQ (TLS),BX    ADDQ BX,AX    MOVQ (AX),BX    MOVQ BX,ret+0(FP)    RET

这里点到为止,大概思路是这样。

性能比较:

我们来简单测试下两种获取go协程id方式性能差距:

// BenchmarkGRtId-8   1000000000         0.0005081 ns/opfunc BenchmarkGRtId(b *testing.B) {    for n := 0; n < 1000000000; n++ {        // runtime获取协程id        getGID()    }}// BenchmarkGoId-8   1000000000         0.05731 ns/opfunc BenchmarkGoId(b *testing.B) {    for n := 0; n < 1000000000; n++ {        // 汇编方式获取        GoId()    }}

可以看到通过汇编方式获取协程id的方式性能更优,相差几个数量级。


限制协程

上面列举了几个定位协程信息的方法,那么在协程泄露之前有没有其他方式对程序的go协程进行管控呢,有个做法是使用强大的channel坐下限制。

抛砖引玉

这里先提供一个简单的思路,即再包装一层channel进行保护,

// 限制数量var LIMIT_G_NUM = make(chan struct{}, 100)// 需要自定义的处理逻辑type HandleFun func()func AsyncGoForHandle(fn HandleFun)  {    // 计数加一    LIMIT_G_NUM <- struct{}{}    go func() {        defer func() {            if err := recover(); err != nil {                log.Fatalf("AsyncGoForHandle recover from err: %v", err)            }            // 回收计数            <-LIMIT_G_NUM        }()        // 处理逻辑        fn()    }()}

上面的思路比较简单,相信大家能看懂,每次需要异步创建协程只要调用AsyncGoForHandle()函数即可,不足之处可能是处理逻辑HandleFun()不够通用,需要自己定义具体实现。

还有一种方式,就是引入协程池的概念,这里的池子和数据库连接池有点像,即一开始就预创建好,业务层只要负责提交数据,业界已经有不少成熟的封装。

成熟方案:tunny

之前看到社区有一个封装得比较完善的协程池tunny,代码行数不多,我们来试着拆解分析一下代码,项目地址:https://github.com/Jeffail/tunny

定义处理逻辑接口:

type Worker interface {    // 自定义逻辑实现,开发者只需要关心入参和出参    Process(interface{}) interface{}}

包装worker的输入源workRequest

type workerWrapper struct {    // 注入内部实现逻辑    worker        Worker    interruptChan chan struct{}    // 请求来源workRequest    reqChan chan<- workRequest    // ...}

输入源结构

type workRequest struct {    // 输入    jobChan chan<- interface{}    // 处理结果,即worker.Process()的返回值    retChan <-chan interface{}    // ...}

编写实现类:

我们知道Go的接口遵循鸭子模型: 只要它表现得像个鸭子,它就是鸭子

// Worker实现类type closureWorker struct {    processor func(interface{}) interface{}}func (w *closureWorker) Process(payload interface{}) interface{} {    return w.processor(payload)}

定义工作池结构

type Pool struct {    queuedJobs int64    // 成员函数,用于"鸭子"实体    ctor    func() Worker    workers []*workerWrapper    reqChan chan workRequest    workerMut sync.Mutex}func NewFunc(n int, f func(interface{}) interface{}) *Pool {    return New(n, func() Worker {        return &closureWorker{            // 传入真正的实现模块            processor: f,        }    })}func New(n int, ctor func() Worker) *Pool {    p := &Pool{        ctor:    ctor,        reqChan: make(chan workRequest),    }    // 批量创建协程,监听处理来自reqChan的任务    p.SetSize(n)    return p}

相关实体结构如下,配合源码阅读就比较清晰了。

Golang协程泄露怎么预防

这个框架相当于把协程预先创建好做了池化,随后业务层只需要源源不断把"加工数据"输入到workRequest这个chan即可,也就是process()函数,process()模块会把数据输入到内部channel进行处理,池中的worker会进行加工。
这种工厂模式还是值得借鉴的,Go也有很多成熟框架使用了这种写法。

引用原项目README.md的用法示例:

numCPUs := runtime.NumCPU()pool := tunny.NewFunc(numCPUs, func(payload interface{}) interface{} {    var result []byte    // 关心业务层的输入、输出即可    result = wrapSomething()    return result})defer pool.Close()http.HandleFunc("/work", func(w http.ResponseWriter, r *http.Request) {    input, err := ioutil.ReadAll(r.Body)    if err != nil {        http.Error(w, "Internal error", http.StatusInternalServerError)    }    defer r.Body.Close()    // 提交任务给Process    result := pool.Process(input)    w.Write(result.([]byte))})http.ListenAndServe(":8080", nil)

总结

  • Go协程有几个内置信息,协程id、协程栈、协程状态(running/io wait/chan send),通过这些信息可以帮助我们一定程度的避免或者定位问题

  • Go里面创建协程只需要一个Go关键字,但是要合理回收却很关键,必要时可以用协程池做限制

感谢你能够认真阅读完这篇文章,希望小编分享的“Golang协程泄露怎么预防”这篇文章对大家有帮助,同时也希望大家多多支持编程网,关注编程网行业资讯频道,更多相关知识等着你来学习!

免责声明:

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

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

Golang协程泄露怎么预防

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

下载Word文档

猜你喜欢

Golang协程泄露怎么预防

这篇文章主要介绍了Golang协程泄露怎么预防,具有一定借鉴价值,感兴趣的朋友可以参考下,希望大家阅读完这篇文章之后大有收获,下面让小编带着大家一起了解一下。泄露案例关于协程泄露很多时候我们往往会忽略它,直到机器资源负载异常才引起重视。之前
2023-06-29

Linux协程与内存泄漏的预防

在Linux中使用协程时,需要注意内存泄漏的预防措施,以下是一些建议:使用内存池:使用内存池可以减少内存分配和释放的次数,从而减少内存碎片和提高内存的利用率。可以使用一些开源的内存池库,如jemalloc、tcmalloc等,也可以自己实现
Linux协程与内存泄漏的预防
2024-08-06

Docker中怎么防止信息泄露

今天小编给大家分享一下Docker中怎么防止信息泄露的相关知识点,内容详细,逻辑清晰,相信大部分人都还太了解这方面的知识,所以分享这篇文章给大家参考一下,希望大家阅读完这篇文章后有所收获,下面我们一起来了解一下吧。概述容器和Docker让我
2023-06-27

win11泄露版怎么升级预览版

这篇文章主要介绍“win11泄露版怎么升级预览版”,在日常操作中,相信很多人在win11泄露版怎么升级预览版问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”win11泄露版怎么升级预览版”的疑惑有所帮助!接下来
2023-07-01

Docker敏感信息怎么防止泄露

这篇文章主要介绍“Docker敏感信息怎么防止泄露”,在日常操作中,相信很多人在Docker敏感信息怎么防止泄露问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”Docker敏感信息怎么防止泄露”的疑惑有所帮助!
2023-06-17

golang怎么检测内存是否泄露

在Go语言中,可以使用内置的`runtime`包来检测内存泄漏。具体的步骤如下:1. 导入`runtime`包:```goimport "runtime"```2. 在需要检测内存泄漏的地方,使用`runtime.GC()`函数触发一次垃圾
2023-09-27

ThreadLocal内存泄漏怎么预防

这篇文章主要介绍“ThreadLocal内存泄漏怎么预防”,在日常操作中,相信很多人在ThreadLocal内存泄漏怎么预防问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”ThreadLocal内存泄漏怎么预防
2023-06-29

Android中怎么利用Handler防止内存泄露

今天就跟大家聊聊有关Android中怎么利用Handler防止内存泄露,可能很多人都不太了解,为了让大家更加了解,小编给大家总结了以下内容,希望大家根据这篇文章可以有所收获。 Handler可能导致的内存泄露及其优化 1 关于常见
2023-05-30

java读取大文件怎么防止内存泄露

在Java中,可以使用以下方法来防止内存泄露:1. 使用缓冲读取:使用BufferedReader或者Scanner类来读取大文件,这些类可以在读取文件时使用缓冲区,以减少内存的使用。2. 分批读取:将大文件分成多个小块进行读取,避免一次性
2023-08-24

qt程序内存泄露怎么解决

解决Qt程序内存泄漏的方法如下:1. 使用对象的父子关系:在创建对象时,将对象的父对象设置为合适的父对象。这样,当父对象被销毁时,它的所有子对象也会被自动销毁,从而避免内存泄漏。2. 使用智能指针:使用std::unique_ptr或std
2023-08-18

CDN怎么帮助防止网站篡改和数据泄露

CDN(内容分发网络)可以帮助防止网站篡改和数据泄露的方法如下:加密传输:CDN可以使用HTTPS协议来保护数据在传输过程中的安全,通过加密通信可以有效防止被窃取或篡改。防御DDoS攻击:CDN可以帮助防止分布式拒绝服务(DDoS)攻击,保
CDN怎么帮助防止网站篡改和数据泄露
2024-04-25

怎么调试Python程序的内存泄露问题

这篇文章主要讲解了“怎么调试Python程序的内存泄露问题”,文中的讲解内容简单清晰,易于学习与理解,下面请大家跟着小编的思路慢慢深入,一起来研究和学习“怎么调试Python程序的内存泄露问题”吧!如果大家在 Linux 或者 macOS
2023-06-16

golang怎么限制协程数量

在Go语言中,可以使用信号量(Semaphore)来限制协程的数量。信号量是一种多线程同步的原语,用于控制对共享资源的访问。可以使用Go语言中的sync包中的Semaphore来实现信号量。sync包中提供了一个WaitGroup类型,可
2023-10-21

golang协程数量太多怎么处理

当使用goroutine数量过多时,可能会引发以下问题:内存消耗:每个goroutine都需要一定的内存空间,如果goroutine数量过多,会占用大量内存。可以考虑使用线程池或者限制并发goroutine的数量,以控制内存消耗。调度开销:
2023-10-22

golang怎么强制结束一个协程

在Go语言中,要强制结束一个协程,可以使用context.Context来实现。context.Context是Go语言中用来传递上下文信息的标准库。它可以用来控制协程的生命周期,包括取消一个协程。以下是一个使用context.Cont
2023-10-21

golang协程调度超时怎么解决

在 Go 语言中,可以使用 context 包来进行协程调度超时的处理。首先,你需要创建一个 context.Context 对象,并设置超时时间。然后,在需要执行耗时操作的协程中,使用 context.WithTimeout 函数将该
2023-10-21

Golang协程池gopool怎么设计与实现

这篇文章主要介绍“Golang协程池gopool怎么设计与实现”,在日常操作中,相信很多人在Golang协程池gopool怎么设计与实现问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”Golang协程池gopo
2023-06-30

Golang中怎么利用协程批量检测代理线路

这篇文章给大家介绍Golang中怎么利用协程批量检测代理线路,内容非常详细,感兴趣的小伙伴们可以参考借鉴,希望对大家能有所帮助。ip代理-Golang协程批量检测代理线路ip代理-Golang协程批量检测代理线路本篇文章讲述如何获取公网ip
2023-06-05

Golang怎么使用http协议实现心跳检测程序

本文小编为大家详细介绍“Golang怎么使用http协议实现心跳检测程序”,内容详细,步骤清晰,细节处理妥当,希望这篇“Golang怎么使用http协议实现心跳检测程序”文章能帮助大家解决疑惑,下面跟着小编的思路慢慢深入,一起来学习新知识吧
2023-07-05

编程热搜

  • 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动态编译

目录