thumbnail
[转]中国目前的贫富差距有多大 – 三亚自驾游感想
原文 过年跑去三亚自驾游,人山人海,我爸高速上追尾了一辆路虎。开路虎的是一位大爷。我们打了双闪下车协商,据我爹描述,他当时心里想着到底路虎大爷要的赔偿超过多少他会选择走保险……然后大爷瞅了车屁股一眼,又瞪了我们一眼,开车走了。我们一家:……大概人家急着赶路,真不在乎这点钱。 一路上我都在跟我爸说,你可别再追人家路虎了。我爹表示科科,前面全是百来万的车,哪个追得起? 春节期间,亚龙湾的酒店均价3000/天。回北京的机票已经1w4了。还是人山人海,遍地都是人。 本地人跟我们说,海南人均收入也不高,沿海的房子本地人都买不起,全是外地人来买。而这些海景房,淡季的时候都是空着的,基本上只有冬天的一两个月有人住。 来买房的,主要还是东北人。我们去海边溜达,发现海边还有东北大妈们在跳广场舞,扭秧歌,不亦乐乎。走到哪里都能听见东北话。别的地方的游客跟我们说了个笑话:国家每次往东北投个几百几千亿振兴经济,海南的房价就要大涨一波。(……)这个笑话真的是我听说的,不是我说的…… 国际免税城同样人挤人。关键是我第一次在奢侈品云集的地方看到这么多人……好比小时候元宵节公园的灯会,小孩子走丢了都找不着的那种程度。美妆区简直挤爆,柜姐们一个比一个高冷,都懒得搭理你,但依旧阻挡不住各个年龄段的女性往前冲,比挤地铁还凶,买起口红来就跟不要钱似的。 最后我啥也没买就走了。因为结账的时候,柜姐说必须要出示机票。我开始认真思考,这么多全国各地牌照的车子里装的都是买了机票的人吗? 想吃大龙虾。酒店的餐厅,最便宜的龙虾398/斤,还有1688/斤的,1888/斤的。后来我看到了西红柿蛋花汤——68/例。望了眼几乎坐满了的餐厅,我开始深思我们这种算不算赤贫阶级。 反正最后我没吃大龙虾。 闺蜜亲切地给我安利了淘宝8毛钱一碗的冲泡的西红柿蛋花汤,月销1.2w笔。火速下单后,感觉自己终于重新回归到了最广大人民群众的行列,内心十分踏实。 要说贫富差距有多大?在三亚,相当一部分游客,一周的开销是全国居民一年的人均收入。还有一部分,去免税店买个包、买块表的钱,够二三线城市的人不吃不喝工作个好几年的。国人全世界爆买的时候,我们都感叹国人好有钱。可是我们还有那么多同胞在贫困线上挣扎。 这些都是中国。我们站立的地方。 根据评论反馈更新补充 免税店这个大家是不是没看懂?我没做好功课,去了才知道要机票,就没买成东西。要机票很正常,但是很多外地人自驾去的免税店,那他们怎么买东西呢?一个推测是:他们是坐飞机来的,然后汽车是司机开过港的…… 刚刚一个评论说,他去搜了机票价格,三亚飞北京才1400,还对我人身攻击了一番,我哭笑不得。春节都结束了好吧?初五初六飞北京1w4,飞哈尔滨1w9,评论区都有人证明的,自己翻翻。 说我夸张了的,自己翻评论区本地人怎么说的。 路虎这个,有说套牌的,也有说路虎耐撞的。其实当时高速上堵得特别厉害,时速20-40之间吧,路虎急刹车,我们也跟着急刹车,就亲密接触了。我们肯定是准备赔偿的,但路虎大爷下来一看啥事儿没有,就走了。评论区还有一位丰田的车主,跟我们家一样kiss了路虎,路虎也是啥事儿没有……看来路虎真的很结实…… 我对东北无恶意,就是写了个道听途说的段子。评论区有一个精选评论,是一个东北网友留言的,他提到一些老职工有职业病,到南方来有助于身体恢复,我觉得是有道理的。还有网友说,早些年海南房价不贵,很多东北人是那个时候买的,我也觉得是有道理的。各种声音都有,大家可以去评论区看看,理性看待这个问题。 参考 知乎 作者晴夕
thumbnail
golang设定最长执行时间的几种方法
前言 web服务开发过程中经常遇到我们需要某一个逻辑最长执行多少时间,超过这个时间需要快速返回而不是继续等待,常用的有这么几种方法 方法一 ch := make(chan bool, 1) timeout := make(chan bool, 1) // Send a message to our timeout channel after 1s go func() { time.Sleep(1 * time.Second) timeout <- true }() go func() { // do something ch <- true }() // Wait for a message, or timeout select { case <-ch: fmt.Println("Read from ch") case <-timeout: fmt.Println("Timed out") } 一句话评价为快速但是资源有浪费,这是最容易想到的方法,但是不足之处在于第一个协程总是需要等待1s,即使我们需要的处理很快就完成,有一定的资源浪费,而且资源没有回收,都需要等待gc。 方法二 ch := make(chan bool, 1) go func() { // do something ch <- true close(ch) }() select { case <-ch: fmt.Println("Read from ch") case <-time.After(1 * time.Second): fmt.Println("Timed out") } 在方法一的基础上优化了一些,提前close了ch,这样如果处理很快完成,ch也很块被释放,但是定时器好像还是仍然需要等1s后才可以交给gc去释放。 方法三 ch := make(chan bool, 1) go func() { ch <- true close(ch) }() timer := time.NewTimer(1 * time.Second) defer timer.Stop() select { case <-ch: fmt.Println("Read from ch") case <-timer.C: fmt.Println("Timed out") } 在方法二的基础上继续优化,定时器修改为可以提前终止的方式,这样我们是主动的去释放掉所有临时的资源了,因此推荐这种方法。 综述 最终完整代码见这个,以接口的方式提供。 var ErrTimeout = errors.New("timeout") func RunWithTimeout(handler func(), timeout time.Duration) error { done := make(chan bool, 1) go func() { handler() done <- true close(done) }() timer := time.NewTimer(timeout) defer timer.Stop() select { case <-timer.C: return ErrTimeout case <-done: return nil } }
thumbnail
gorm enum字段问题
开发时碰到了mysql中字段类型为enum中,比如下面的gender字段 CREATE TABLE `user` ( `id` int(10) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '自增ID', `account` varchar(255) NOT NULL COMMENT '登录账号', `gender` enum('no','male','female') NOT NULL DEFAULT 'no' COMMENT '性别', PRIMARY KEY (`ID`), ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin AUTO_INCREMENT=1 COMMENT='用户表' 对应字段类型看似是string类型的,因此定义结构体定义为了 type User struct { Id int Account string Gender string } 但是在用gorm库create表记录是有如下错误: newUser := &db.User{ Account: "Test", } if err := sqlDB.Create(newUser).Error; err != nil { return err } // err为 Error 1265: Data Truncated 猜想到的问题原因是 Data Truncated通常是因为字段类型不匹配导致的,这里因为设定gender为空,和mysql定义的默认值为no产生了冲突,gorm默认按照string的默认值去insert记录也就出问题了,这里其实需要告诉gorm该字段类型的定义的,因此添加上mysql的标记,让gorm默认值和mysql定义的一致 type User struct { Id int Account string Gender string `gorm:"type:enum('no','male','female');default:no"` } 之后在插入(create)记录就没问题了。
thumbnail
详解goroutine调度
我们都知道Go语言是原生支持语言级并发的,这个并发的最小逻辑单元就是goroutine。goroutine就是Go语言提供的一种用户态线程,当然这种用户态线程是跑在内核级线程之上的。当我们创建了很多的goroutine,并且它们都是跑在同一个内核线程之上的时候,就需要一个调度器来维护这些goroutine,确保所有的goroutine都使用cpu,并且是尽可能公平的使用cpu资源。 这个调度器的原理以及实现值得我们去深入研究一下。支撑整个调度器的主要有4个重要结构,分别是M、G、P、Sched,前三个定义在runtime.h中,Sched定义在proc.c中。 - Sched结构就是调度器,它维护有存储M和G的队列以及调度器的一些状态信息等。 - M代表内核级线程,一个M就是一个线程,goroutine就是跑在M之上的;M是一个很大的结构,里面维护小对象内存cache(mcache)、当前执行的goroutine、随机数发生器等等非常多的信息。 - P全称是Processor,处理器,它的主要用途就是用来执行goroutine的,所以它也维护了一个goroutine队列,里面存储了所有需要它来执行的goroutine,这个P的角色可能有一点让人迷惑,一开始容易和M冲突,后面重点聊一下它们的关系。 - G就是goroutine实现的核心结构了,G维护了goroutine需要的栈、程序计数器以及它所在的M等信息。 理解M、P、G三者的关系对理解整个调度器非常重要,我从网络上找了一个图来说明其三者关系: 地鼠(gopher)用小车运着一堆待加工的砖。M就可以看作图中的地鼠,P就是小车,G就是小车里装的砖。一图胜千言啊,弄清楚了它们三者的关系,下面我们就开始重点聊地鼠是如何在搬运砖块的。 启动过程 在关心绝大多数程序的内部原理的时候,我们都试图去弄明白其启动初始化过程,弄明白这个过程对后续的深入分析至关重要。在asm_amd64.s文件中的汇编代码_rt0_amd64就是整个启动过程,核心过程如下: CALL runtime·args(SB) CALL runtime·osinit(SB) CALL runtime·hashinit(SB) CALL runtime·schedinit(SB) // create a new goroutine to start program PUSHQ $runtime·main·f(SB) // entry PUSHQ $0 // arg size CALL runtime·newproc(SB) POPQ AX POPQ AX // start this M CALL runtime·mstart(SB) 启动过程做了调度器初始化runtime·schedinit后,调用runtime·newproc创建出第一个goroutine,这个goroutine将执行的函数是runtime·main,这第一个goroutine也就是所谓的主goroutine。我们写的最简单的Go程序”hello,world”就是完全跑在这个goroutine里,当然任何一个Go程序的入口都是从这个goroutine开始的。最后调用的runtime·mstart就是真正的执行上一步创建的主goroutine。 启动过程中的调度器初始化runtime·schedinit函数主要根据用户设置的GOMAXPROCS值来创建一批小车(P),不管GOMAXPROCS设置为多大,最多也只能创建256个小车(P)。这些小车(p)初始创建好后都是闲置状态,也就是还没开始使用,所以它们都放置在调度器结构(Sched)的pidle字段维护的链表中存储起来了,以备后续之需。 查看runtime·main函数可以了解到主goroutine开始执行后,做的第一件事情是创建了一个新的内核线程(地鼠M),不过这个线程是一个特殊线程,它在整个运行期专门负责做特定的事情——系统监控(sysmon)。接下来就是进入Go程序的main函数开始Go程序的执行。 至此,Go程序就被启动起来开始运行了。一个真正干活的Go程序,一定创建有不少的goroutine,所以在Go程序开始运行后,就会向调度器添加goroutine,调度器就要负责维护好这些goroutine的正常执行。 创建goroutine(G) 在Go程序中,时常会有类似代码: go do_something() go关键字就是用来创建一个goroutine的,后面的函数就是这个goroutine需要执行的代码逻辑。go关键字对应到调度器的接口就是runtime·newproc。runtime·newproc干的事情很简单,就负责制造一块砖(G),然后将这块砖(G)放入当前这个地鼠(M)的小车(P)中。 每个新的goroutine都需要有一个自己的栈,G结构的sched字段维护了栈地址以及程序计数器等信息,这是最基本的调度信息,也就是说这个goroutine放弃cpu的时候需要保存这些信息,待下次重新获得cpu的时候,需要将这些信息装载到对应的cpu寄存器中。 假设这个时候已经创建了大量的goroutne,就轮到调度器去维护这些goroutine了。 创建内核线程(M) Go程序中没有语言级的关键字让你去创建一个内核线程,你只能创建goroutine,内核线程只能由runtime根据实际情况去创建。runtime什么时候创建线程?以地鼠运砖图来讲,砖(G)太多了,地鼠(M)又太少了,实在忙不过来,刚好还有空闲的小车(P)没有使用,那就从别处再借些地鼠(M)过来直到把小车(p)用完为止。这里有一个地鼠(M)不够用,从别处借地鼠(M)的过程,这个过程就是创建一个内核线程(M)。创建M的接口函数是: void newm(void (*fn)(void), P *p) newm函数的核心行为就是调用clone系统调用创建一个内核线程,每个内核线程的开始执行位置都是runtime·mstart函数。参数p就是一辆空闲的小车(p)。 每个创建好的内核线程都从runtime·mstart函数开始执行了,它们将用分配给自己小车去搬砖了。 调度核心 newm接口只是给新创建的M分配了一个空闲的P,也就是相当于告诉借来的地鼠(M)——“接下来的日子,你将使用1号小车搬砖,记住是1号小车;待会自己到停车场拿车。”,地鼠(M)去拿小车(P)这个过程就是acquirep。runtime·mstart在进入schedule之前会给当前M装配上P,runtime·mstart函数中的代码: } else if(m != &runtime·m0) { acquirep(m->nextp); m->nextp = nil; } schedule(); if分支的内容就是为当前M装配上P,nextp就是newm分配的空闲小车(P),只是到这个时候才真正拿到手罢了。没有P,M是无法执行goroutine的,就像地鼠没有小车无法运砖一样的道理。对应acquirep的动作是releasep,把M装配的P给载掉;活干完了,地鼠需要休息了,就把小车还到停车场,然后睡觉去。 地鼠(M)拿到属于自己的小车(P)后,就进入工场开始干活了,也就是上面的schedule调用。简化schedule的代码如下: static void schedule(void) { G *gp; gp = runqget(m->p); if(gp == nil) gp = findrunnable(); if (m->p->runqhead != m->p->runqtail && runtime·atomicload(&runtime·sched.nmspinning) == 0 && runtime·atomicload(&runtime·sched.npidle) > 0) // TODO: fast atomic wakep(); execute(gp); } schedule函数被我简化了太多,主要是我不喜欢贴大段大段的代码,因此只保留主干代码了。这里涉及到4大步逻辑: 1. runqget, 地鼠(M)试图从自己的小车(P)取出一块砖(G),当然结果可能失败,也就是这个地鼠的小车已经空了,没有砖了。 2. findrunnable, 如果地鼠自己的小车中没有砖,那也不能闲着不干活是吧,所以地鼠就会试图跑去工场仓库取一块砖来处理;工场仓库也可能没砖啊,出现这种情况的时候,这个地鼠也没有偷懒停下干活,而是悄悄跑出去,随机盯上一个小伙伴(地鼠),然后从它的车里试图偷一半砖到自己车里。如果多次尝试偷砖都失败了,那说明实在没有砖可搬了,这个时候地鼠就会把小车还回停车场,然后睡觉休息了。如果地鼠睡觉了,下面的过程当然都停止了,地鼠睡觉也就是线程sleep了。 3. wakep, 到这个过程的时候,可怜的地鼠发现自己小车里有好多砖啊,自己根本处理不过来;再回头一看停车场居然有闲置的小车,立马跑到宿舍一看,你妹,居然还有小伙伴在睡觉,直接给屁股一脚,“你妹,居然还在睡觉,老子都快累死了,赶紧起来干活,分担点工作。”,小伙伴醒了,拿上自己的小车,乖乖干活去了。有时候,可怜的地鼠跑到宿舍却发现没有在睡觉的小伙伴,于是会很失望,最后只好向工场老板说——”停车场还有闲置的车啊,我快干不动了,赶紧从别的工场借个地鼠来帮忙吧。”,最后工场老板就搞来一个新的地鼠干活了。 4. execute,地鼠拿着砖放入火种欢快的烧练起来。 注: “地鼠偷砖”叫work stealing,一种调度算法。 到这里,貌似整个工场都正常的运转起来了,无懈可击的样子。不对,还有一个疑点没解决啊,假设地鼠的车里有很多砖,它把一块砖放入火炉中后,何时把它取出来,放入第二块砖呢?难道要一直把第一块砖烧练好,才取出来吗?那估计后面的砖真的是等得花儿都要谢了。这里就是要真正解决goroutine的调度,上下文切换问题。 调度点 当我们翻看channel的实现代码可以发现,对channel读写操作的时候会触发调用runtime·park函数。goroutine调用park后,这个goroutine就会被设置位waiting状态,放弃cpu。被park的goroutine处于waiting状态,并且这个goroutine不在小车(P)中,如果不对其调用runtime·ready,它是永远不会再被执行的。除了channel操作外,定时器中,网络poll等都有可能park goroutine。 除了park可以放弃cpu外,调用runtime·gosched函数也可以让当前goroutine放弃cpu,但和park完全不同;gosched是将goroutine设置为runnable状态,然后放入到调度器全局等待队列(也就是上面提到的工场仓库,这下就明白为何工场仓库会有砖块(G)了吧)。 除此之外,就轮到系统调用了,有些系统调用也会触发重新调度。Go语言完全是自己封装的系统调用,所以在封装系统调用的时候,可以做不少手脚,也就是进入系统调用的时候执行entersyscall,退出后又执行exitsyscall函数。 也只有封装了entersyscall的系统调用才有可能触发重新调度,它将改变小车(P)的状态为syscall。还记一开始提到的sysmon线程吗?这个系统监控线程会扫描所有的小车(P),发现一个小车(P)处于了syscall的状态,就知道这个小车(P)遇到了goroutine在做系统调用,于是系统监控线程就会创建一个新的地鼠(M)去把这个处于syscall的小车给抢过来,开始干活,这样这个小车中的所有砖块(G)就可以绕过之前系统调用的等待了。被抢走小车的地鼠等系统调用返回后,发现自己的车没,不能继续干活了,于是只能把执行系统调用的goroutine放回到工场仓库,自己睡觉去了。 从goroutine的调度点可以看出,调度器还是挺粗暴的,调度粒度有点过大,公平性也没有想想的那么好。总之,这个调度器还是比较简单的。 现场处理 goroutine在cpu上换入换出,不断上下文切换的时候,必须要保证的事情就是保存现场和恢复现场,保存现场就是在goroutine放弃cpu的时候,将相关寄存器的值给保存到内存中;恢复现场就是在goroutine重新获得cpu的时候,需要从内存把之前的寄存器信息全部放回到相应寄存器中去。 goroutine在主动放弃cpu的时候(park/gosched),都会涉及到调用runtime·mcall函数,此函数也是汇编实现,主要将goroutine的栈地址和程序计数器保存到G结构的sched字段中,mcall就完成了现场保存。恢复现场的函数是runtime·gogocall,这个函数主要在execute中调用,就是在执行goroutine前,需要重新装载相应的寄存器。 参考 goroutine与调度器
thumbnail
通俗易懂 白话goroutine的实现
一个线程就是一个栈加一堆资源。操作系统一会让cpu跑线程A,一会让cpu跑线程B,靠A和B的栈来保存A和B的执行状态。 每个线程都有他自己的栈。但是线程又老贵了,花不起那个钱,所以go发明了goroutine。大致就是说给每个goroutine弄一个分配在heap里面的栈来模拟线程栈。 比方说有3个goroutine,A,B,C,就在heap上弄三个栈出来。然后Go让一个单线程的scheduler开始跑他们仨。相当于 { A(); B(); C() },连续的,串行的跑。 和操作系统不太一样的是,操作系统可以随时随地把你线程停掉,切换到另一个线程。这个单线程的scheduler没那个能力啊,他就是user space的一段朴素的代码,他跑着A的时候控制权是在A的代码里面的。A自己不退出谁也没办法。所以A跑一小段后需要主动说,老大(scheduler),我不想跑了,帮我把我的所有的状态保存在我自己的栈上面,让我歇一会吧。这时候你可以看做A返回了。A返回了B就可以跑了,然后B跑一小段说,跑够了,保存状态,返回,然后C再跑。C跑一段也返回了。这样跑完{A(); B(); C()}之后,我们发现,好像他们都只跑了一小段啊。所以外面要包一个循环,大致是: goroutine_list = [A, B, C] while(goroutine): for goroutine in goroutine_list: r = goroutine() if r.finished(): goroutine_list.remove(r) 比如跑完一圈A,B,C之后谁也没执行完,那么就在回到A执行一次。由于我们把A的栈保存在了HEAP里,这时候可以把A的栈复制粘贴会系统栈里(我很确定真实情况不是这么玩的,会意就行),然后再调用A,这时候由于A是跑到一半自己说跳出来的,所以会从刚刚跳出来的地方继续执行。 比如A的内部大致上是这样 def A: 上次跑到的地方 = 找到上次跑哪儿了 读取所有临时变量 goto 上次跑到的地方 a = 1 print("do something") go.scheduler.保存程序指针 // 设置"这次跑哪儿了" go.scheduler.保存临时变量们 go.scheduler.跑够了_换人 //相当于return print("do something again") print(a) 第一次跑A,由于这是第一次,会打印do something,然后保存临时变量a,并保存跑到的地方,然后返回。再跑一次A,他会找到上次返回的地方的下一句,然后恢复临时变量a,然后接着跑,会打印“do something again"和1所以你看出来了,这个关键就在于每个goroutine跑一跑就要让一让。 一般支持这种玩意(叫做coroutine)的语言都是让每个coroutine自己说,我跑够了,换人。goroutine比较文艺的地方就在于,他可以来帮你判断啥时候“跑够了”。其中有一大半就是靠的你说的“异步并发”。 go把每一个能异步并发的操作,像你说的文件访问啦,网络访问啦之类的都包包好,包成一个看似朴素的而且是同步的“方法”,比如string readFile(我瞎举得例子)。但是神奇的地方在于,这个方法里其实会调用“异步并发”的操作,比如某操作系统提供的asyncReadFile。你也知道,这种异步方法都是很快返回的。所以你自己在某个goroutine里写了string s = go.file.readFile("/root") 其实go偷偷在里面执行了某操作系统的API asyncReadFIle。跑起来之后呢,这个方法就会说,我当前所在的goroutine跑够啦,把刚刚跑的那个异步操作的结果保存下下,换人: // 实际上 handler h = someOS.asyncReadFile("/root") //很快返回一个handler while (!h.finishedAsyncReadFile()): //很快返回Y/N go.scheduler.保存现状() go.scheduler.跑够了_换人() // 相当于return,不过下次会从这里的下一句开始执行 string s = h.getResultFromAsyncRead() 然后scheduler就换下一个goroutine跑了。等下次再跑回刚才那个goroutine的时候,他就看看,说那个asyncReadFile到底执行完没有啊,如果没有,就再换个人吧。如果执行完了,那就把结果拿出来,该干嘛干嘛。所以你看似写了个同步的操作,已经被go替换成异步操作了。 还有另外一种情况是,某个goroutine执行了某个不能异步调用的会blocking的系统调用,这个时候goroutine就没法玩那种异步调用的把戏了。他会把你挪到一个真正的线程里让你在那个县城里等着,他接茬去跑别的goroutine。比如A这么定义 def A: print("do something") go.os.InvokeSomeReallyHeavyAndBlockingSystemCall() print("do something 2") go会帮你转成def 真实的A: print("do something") Thread t = new Thread( () => { SomeReallyHeavyAndBlockingSystemCall(); }) t.start() while !t.finished(): go.scheduler.保存现状 go.scheduler.跑够了_换人 print("finished") 所以真实的A还是不会blocking,还是可以跟别的小伙伴(goroutine)愉快地玩耍(轮流往复的被执行),但他其实已经占了一个真是的系统线程了。当然会有一种情况就是A完全没有调用任何可能的“异步并发”的操作,也没有调用任何的同步的系统调用,而是一个劲的用CPU做运算(比如用个死循环调用a++)。在早期的go里,这个A就把整个程序block住了。后面新版本的go好像会有一些处理办法,比如如果你A里面call了任意一个别的函数的话,就有一定几率被踢下去换人。好像也可以自己主动说我要换人的,可以去查查新的go的spec。 部分代码举例: // 源文件:go/src/runtime/proc.go if s == _Psyscall { // 备注:goroutine 中触发系统调用的情况 // Retake P from syscall if it's there for more than 1 sysmon tick (at least 20us). ========================================================== t := int64(_p_.syscalltick) if int64(pd.syscalltick) != t { pd.syscalltick = uint32(t) pd.syscallwhen = now continue } ... (省略) ... if atomic.Cas(&_p_.status, s, _Pidle) { ... (省略) ... handoffp(_p_) // 备注:切换 P 实体…
thumbnail
通过一道题谈谈golang的组合
我们从这个例子说起 type People struct{} func (p *People) ShowA() { fmt.Println("showA") p.ShowB() } func (p *People) ShowB() { fmt.Println("showB") } type Teacher struct { People } func (t *Teacher) ShowB() { fmt.Println("teacher showB") } func main() { t := Teacher{} t.ShowA() } 这个题的结果是 showA showB 最开始我判断错了,误以为按照重载的概念来讲,想到的结果是 showA teacher showB golang官方从来没说支持继承、重载之类的,以上例子中只是叫组合而已,只是把另外一个结构体的方法组合过来,底层实现上只是加了一个匿名类型的People。 也就是说main里面的t.ShowA(),实际上只是隐藏里组合过来的匿名类型的People,我们展开来看实际底层是这么调用的,t.People.ShowA(),只不过t.People是匿名了,简化为t.ShowA(),因此t.ShowA()里面调用的p.ShowB()实际上是t.People.ShowB(),也就是showB的结果了。 以上可以看出这种实际上是匿名组合而已,和继承不是一个概念,继承是将基类的方法都继承过来,组合当然不是了。
thumbnail
如何理解持续集成 持续集成是什么
因为最近项目因为新特性修改导致老特性失效的问题,这里也就谈谈持续集成的问题,小公司一般都不注重持续集成,这一环节缺失导致质量很难把控。 一、概念 持续集成指的是,频繁地(一天多次)将代码集成到主干。 它的好处主要有两个。 - 快速发现错误。每完成一点更新,就集成到主干,可以快速发现错误,定位错误也比较容易。 - 防止分支大幅偏离主干。如果不是经常集成,主干又在不断更新,会导致以后集成的难度变大,甚至难以集成。 持续集成的目的,就是让产品可以快速迭代,同时还能保持高质量。它的核心措施是,代码集成到主干之前,必须通过自动化测试。只要有一个测试用例失败,就不能集成。 Martin Fowler说过,"持续集成并不能消除Bug,而是让它们非常容易发现和改正。" 与持续集成相关的,还有两个概念,分别是持续交付和持续部署。 二、持续交付 持续交付(Continuous delivery)指的是,频繁地将软件的新版本,交付给质量团队或者用户,以供评审。如果评审通过,代码就进入生产阶段。 持续交付可以看作持续集成的下一步。它强调的是,不管怎么更新,软件是随时随地可以交付的。 三、持续部署 持续部署(continuous deployment)是持续交付的下一步,指的是代码通过评审以后,自动部署到生产环境。 持续部署的目标是,代码在任何时刻都是可部署的,可以进入生产阶段。 持续部署的前提是能自动化完成测试、构建、部署等步骤。它与持续交付的区别,可以参考下图。 四、流程 根据持续集成的设计,代码从提交到生产,整个过程有以下几步。 4.1 提交 流程的第一步,是开发者向代码仓库提交代码。所有后面的步骤都始于本地代码的一次提交(commit)。 4.2 测试(第一轮) 代码仓库对commit操作配置了钩子(hook),只要提交代码或者合并进主干,就会跑自动化测试。 测试有好几种。 - 单元测试:针对函数或模块的测试 - 集成测试:针对整体产品的某个功能的测试,又称功能测试 - 端对端测试:从用户界面直达数据库的全链路测试 第一轮至少要跑单元测试。 4.3 构建 通过第一轮测试,代码就可以合并进主干,就算可以交付了。 交付后,就先进行构建(build),再进入第二轮测试。所谓构建,指的是将源码转换为可以运行的实际代码,比如安装依赖,配置各种资源(样式表、JS脚本、图片)等等。 4.4 测试(第二轮) 构建完成,就要进行第二轮测试。如果第一轮已经涵盖了所有测试内容,第二轮可以省略,当然,这时构建步骤也要移到第一轮测试前面。 第二轮是全面测试,单元测试和集成测试都会跑,有条件的话,也要做端对端测试。所有测试以自动化为主,少数无法自动化的测试用例,就要人工跑。 需要强调的是,新版本的每一个更新点都必须测试到。如果测试的覆盖率不高,进入后面的部署阶段后,很可能会出现严重的问题。 4.5 部署 通过了第二轮测试,当前代码就是一个可以直接部署的版本(artifact)。将这个版本的所有文件打包( tar filename.tar * )存档,发到生产服务器。 生产服务器将打包文件,解包成本地的一个目录,再将运行路径的符号链接(symlink)指向这个目录,然后重新启动应用。 4.6 回滚 一旦当前版本发生问题,就要回滚到上一个版本的构建结果。最简单的做法就是修改一下符号链接,指向上一个版本的目录。
thumbnail
区块链原理简要介绍
区块链(blockchain)是眼下的大热门,新闻媒体大量报道,宣称它将创造未来。 可是,简单易懂的入门文章却很少。区块链到底是什么,有何特别之处,很少有解释。 下面,我就来尝试,写一篇最好懂的区块链教程。毕竟它也不是很难的东西,核心概念非常简单,几句话就能说清楚。我希望读完本文,你不仅可以理解区块链,还会明白什么是挖矿、为什么挖矿越来越难等问题。 需要说明的是,我并非这方面的专家。虽然很早就关注,但是仔细地了解区块链,还是从今年初开始。文中的错误和不准确的地方,欢迎大家指正。 一、区块链的本质 区块链是什么?一句话,它是一种特殊的分布式数据库。 首先,区块链的主要作用是储存信息。任何需要保存的信息,都可以写入区块链,也可以从里面读取,所以它是数据库。 其次,任何人都可以架设服务器,加入区块链网络,成为一个节点。区块链的世界里面,没有中心节点,每个节点都是平等的,都保存着整个数据库。你可以向任何一个节点,写入/读取数据,因为所有节点最后都会同步,保证区块链一致。 二、区块链的最大特点 分布式数据库并非新发明,市场上早有此类产品。但是,区块链有一个革命性特点。 区块链没有管理员,它是彻底无中心的。其他的数据库都有管理员,但是区块链没有。如果有人想对区块链添加审核,也实现不了,因为它的设计目标就是防止出现居于中心地位的管理当局。 正是因为无法管理,区块链才能做到无法被控制。否则一旦大公司大集团控制了管理权,他们就会控制整个平台,其他使用者就都必须听命于他们了。 但是,没有了管理员,人人都可以往里面写入数据,怎么才能保证数据是可信的呢?被坏人改了怎么办?请接着往下读,这就是区块链奇妙的地方。 三、区块 区块链由一个个区块(block)组成。区块很像数据库的记录,每次写入数据,就是创建一个区块。 每个区块包含两个部分。 区块头(Head):记录当前区块的元信息 区块体(Body):实际数据 区块头包含了当前区块的多项元信息。 生成时间 实际数据(即区块体)的 Hash 上一个区块的 Hash ... 这里,你需要理解什么叫Hash,这是理解区块链必需的。 所谓 Hash 就是计算机可以对任意内容,计算出一个长度相同的特征值。区块链的 Hash 长度是256位,这就是说,不管原始内容是什么,最后都会计算出一个256位的二进制数字。而且可以保证,只要原始内容不同,对应的 Hash 一定是不同的。 举例来说,字符串123的Hash是a8fdc205a9f19cc1c7507a60c4f01b13d11d7fd0(十六进制),转成二进制就是256位,而且只有123能得到这个 Hash。 因此,就有两个重要的推论。 推论1:每个区块的 Hash 都是不一样的,可以通过 Hash 标识区块。 推论2:如果区块的内容变了,它的 Hash 一定会改变。 四、 Hash 的不可修改性 区块与 Hash 是一一对应的,每个区块的 Hash 都是针对"区块头"(Head)计算的。 Hash = SHA256(区块头) 上面就是区块Hash的计算公式,Hash由区块头唯一决定,SHA256是区块链的Hash算法。 前面说过,区块头包含很多内容,其中有当前区块体的 Hash(注意是"区块体"的Hash,而不是整个区块),还有上一个区块的 Hash。这意味着,如果当前区块的内容变了,或者上一个区块的Hash变了,一定会引起当前区块的 Hash改变。 这一点对区块链有重大意义。如果有人修改了一个区块,该区块的 Hash 就变了。为了让后面的区块还能连到它,该人必须同时修改后面所有的区块,否则被改掉的区块就脱离区块链了。由于后面要提到的原因,Hash 的计算很耗时,同时修改多个区块几乎不可能发生,除非有人掌握了全网51%以上的计算能力。 正是通过这种联动机制,区块链保证了自身的可靠性,数据一旦写入,就无法被篡改。这就像历史一样,发生了就是发生了,从此再无法改变。 五、采矿 由于必须保证节点之间的同步,所以新区块的添加速度不能太快。试想一下,你刚刚同步了一个区块,准备基于它生成下一个区块,但这时别的节点又有新区块生成,你不得不放弃做了一半的计算,再次去同步。因为每个区块的后面,只能跟着一个区块,你永远只能在最新区块的后面,生成下一个区块。所以,你别无选择,一听到信号,就必须立刻同步。 所以,区块链的发明者中本聪(这是假名,真实身份至今未知)故意让添加新区块,变得很困难。他的设计是,平均每10分钟,全网才能生成一个新区块,一小时也就六个。 这种产出速度不是通过命令达成的,而是故意设置了海量的计算。也就是说,只有通过极其大量的计算,才能得到当前区块的有效 Hash,从而把新区块添加到区块链。由于计算量太大,所以快不起来。 这个过程就叫做采矿(mining),因为计算有效 Hash 的难度,好比在全世界的沙子里面,找到一粒符合条件的沙子。计算 Hash 的机器就叫做矿机,操作矿机的人就叫做矿工。 六、难度系数 读到这里,你可能会有一个疑问,人们都说采矿很难,可是采矿不就是用计算机算出一个 Hash 吗,这正是计算机的强项啊,怎么会变得很难,迟迟算不出来呢? 原来不是任意一个 Hash 都可以,只有满足条件的 Hash 才会被区块链接受。这个条件特别苛刻,使得绝大部分 Hash 都不满足要求,必须重算。 原来,区块头包含一个难度系数(difficulty),这个值决定了计算Hash的难度。举例来说,第100000个区块的难度系数是14484.16236122。 区块链协议规定,使用一个常量除以难度系数,可以得到目标值(target)。显然,难度系数越大,目标值就越小。 Hash的有效性跟目标值密切相关,只有小于目标值的 Hash才是有效的,否则Hash无效,必须重算。由于目标值非常小,Hash小于该值的机会极其渺茫,可能计算10亿次,才算中一次。这就是采矿如此之慢的根本原因。 区块头里面还有一个 Nonce 值,记录了 Hash 重算的次数。第 100000 个区块的 Nonce 值是274148111,即计算了 2.74 亿次,才得到了一个有效的 Hash,该区块才能加入区块链。 七、难度系数的动态调节 就算采矿很难,但也没法保证,正好十分钟产出一个区块,有时一分钟就算出来了,有时几个小时可能也没结果。总体来看,随着硬件设备的提升,以及矿机的数量增长,计算速度一定会越来越快。 为了将产出速率恒定在十分钟,中本聪还设计了难度系数的动态调节机制。他规定,难度系数每两周(2016个区块)调整一次。如果这两周里面,区块的平均生成速度是9分钟,就意味着比法定速度快了10%,因此难度系数就要调高10%;如果平均生成速度是11分钟,就意味着比法定速度慢了10%,因此难度系数就要调低10%。 难度系数越调越高(目标值越来越小),导致了采矿越来越难。 八、区块链的分叉 即使区块链是可靠的,现在还有一个问题没有解决:如果两个人同时向区块链写入数据,也就是说,同时有两个区块加入,因为它们都连着前一个区块,就形成了分叉。这时应该采纳哪一个区块呢? 现在的规则是,新节点总是采用最长的那条区块链。如果区块链有分叉,将看哪个分支在分叉点后面,先达到6个新区块(称为"六次确认")。按照10分钟一个区块计算,一小时就可以确认。 由于新区块的生成速度由计算能力决定,所以这条规则就是说,拥有大多数计算能力的那条分支,就是正宗的比特链。 九、总结 区块链作为无人管理的分布式数据库,从2009年开始已经运行了8年,没有出现大的问题。这证明它是可行的。 但是,为了保证数据的可靠性,区块链也有自己的代价。一是效率,数据写入区块链,最少要等待十分钟,所有节点都同步数据,则需要更多的时间;二是能耗,区块的生成需要矿工进行无数无意义的计算,这是非常耗费能源的。 因此,区块链的适用场景,其实非常有限。 不存在所有成员都信任的管理当局 写入的数据不要求实时使用 挖矿的收益能够弥补本身的成本 如果无法满足上述的条件,那么传统的数据库是更好的解决方案。 目前,区块链最大的应用场景(可能也是唯一的应用场景),就是以比特币为代表的加密货币。 十、参考链接 阮一峰 区块链入门教程
thumbnail
程序员自黑 杀掉一个程序猿不需要用枪 只需要。。。
杀一个程序员不需要用枪,改三次需求就可以了。 程序猿的读书历程:x 语言入门 —> x 语言应用实践 —> x 语言高阶编程 —> x 语言的科学与艺术 —> 编程之美 —> 编程之道 —> 编程之禅—> 颈椎病康复指南。 程序猿最烦两件事,第一件事是别人要他给自己的代码写文档,第二件呢?是别人的程序没有留下文档。 问:程序猿最讨厌康熙的哪个儿子。答:胤禩。因为他是八阿哥(bug) Delphi象吉普车,什么路上都能开,却在啥路上也开不好;PB就象卡丁车,只能在固定线路上开,到室外就有些不稳;VC象跑车,你开得起 却买不起,而且一旦发生故障,想修都找不到毛病在哪;Java象敞棚车,不管刮风下雨还是艳阳高照,都能照开不误;VB就是摩托车,骑的时间越长,你越痛 恨它! 程序员的四大理想:南极有套房,澳大利亚有群羊,全世界电脑死光光,孩儿有个娘。 诸葛亮是一个优秀的程序猿,每一个锦囊都是应对不同的case而编写的!但是优秀的程序猿也敌不过更优秀的bug!六出祈山,七进中原,鞠躬尽瘁,死而后已的诸葛亮只因为有一个错误的case-马谡,整个结构就被break了! 程序猿要了3个孩子,分别取名叫Ctrl. Alt 和Delete,如果他们不听话,程序猿就只要同时敲他们一下就会好的…... 有一天某程序员去买肉,要了一公斤, 拿到公平电子秤上一称:”额......怎么少了24克......” C++程序员看不起C 程序员, C 程序员看不起java程序员, java程序员看不起C#程序员,C#程序员看不起美工。周末了,美工带着妹子出去约会了,一群SX程序员还在加班...... 程序员爱情观:爱情就是死循环,一旦执行就陷进去了;爱上一个人,就是内存泄漏–你永远释放不了;真正爱上一个人的时候,那就是常量限定,永远不会改变;女朋友就是私有变量,只有我这个类才能调用;情人就是指针用的时候一定要注意,要不然就带来巨大的灾难。 随机函数可以帮你实现家庭和谐: Talk(){:top word(1)=”恩!”; word(2)=”好的!”;word(3)=”然后呢?”;word(4)=”有道理”;i=random(4); say word(i) goto top;} 有一种崩溃叫密码输入有误;有一种惊慌叫做账号异地登陆;有一种感情叫隐身对其可见;有一种误会叫人机离线;有一种失落叫没有访问权限;有一种感情叫站点访问失败;有一种无奈叫bug无法复现...... 据说有一位软件工程师,一位硬件工程师和一位项目经理同坐车参加研讨会。不幸在从盘山公路下山时坏在半路上了。于是两位工程师和一位经理就如何修车的问题 展开了讨论。硬件工程师说:“我可以用随身携带的瑞士军刀把车坏的部分拆下来,找出原因,排除故障。” 项目经理说:“根据经营管理学,应该召开会议,根据问题现状写出需求报告,制订计划,编写日程安排,逐步逼近,alpha测试,beta1测试和 beta2测试解决问题。” 软件工程说:“咱们还是应该把车推回山顶再开下来,看看问题是否重复发生。” 一个程序员对自己的未来很迷茫,于是去问上帝。“万能的上帝呀,请你告诉我,我的未来会怎样?”上帝说:“我的孩子,你去问Lippman, 他现在领导的程序员的队伍可能是地球上最大的”。于是他去问Lippman。Lippman说:“程序员的未来就是驾驭程序员”。这个程序员对这个未来不满意,于是他又去问上帝。“万能的上帝呀,请你告诉我,我的未来会怎样?”。上帝说:“我的孩子,你去问Gates,他现在所拥有的财产可能是地球上最多的”。于是他去问Gates。Gates说:“程序员的未来就是榨取程序员”。这个程序员对这个未来不满意,于是他又去问上帝。“万能的上帝呀,请你告诉我,我的未来会怎样?”。上帝说:“我的孩子,你去问侯捷,他写的计算机书的读者可能是地球上最多的”。于是他去问侯捷。侯捷说:“程序员的未来就是诱惑程序员”。这个程序员对这个未来不满意,于是他又去问上帝。“万能的上帝呀,请你告诉我,我的未来会怎样?”。上帝摇摇头:“唉,我的孩子,你还是别当程序员了”。 IT工程师=加班狂+程序员+测试工程师+实施工程师+网络工程师+电工+装卸工+搬运工+超人,有同感的转走。 用一句话总结了HTML,CSS,JS的关系。HTML是名词,JS是动词,CSS是形容词和副词。 我是个程序猿,一天我坐在路边一边喝水一边苦苦检查bug。这时一个乞丐在我边上坐下了,开始要饭,我觉得可怜,就给了他1块钱,然后接着调试程序。他可能生意不好,就无聊的看看我在干什么,然后过了一会,他幽幽的说,这里少了个分号......分号......分号...... 十年前,女:“对不起,我不会喜欢你的,你不要再坚持了,就好比让 Linux 和 Windows 同时运行在一台PC机上,可能吗?”男生听后默默走开,十年后,在一次虚拟技术大会上,我听到一名虚拟技术开发程序员给我讲述了这个故事。 【程序员被提bug之后的反应】1.怎么可能;2.在我这是好的,不信你来看看;3.真是奇怪,刚刚还好好的;4.肯定是数据问题;5.你清下缓存试试;6.重启下电脑试试;7.你装的什么版本的类库(jdk)8.这谁写的代码;9.尼玛怎么还在用360安全浏览器 ;10.用户不会像你这么操作的。 知道JAVA程序员和C程序员的差别吗?食堂里,吃完饭就走的是JAVA程序员,吃完饭还要自己 收拾的那就是是C程序员。至于为什么会这样. 大家都明白(因为JAVA自带垃圾回收机制......C需要手动释放内存)←这就是原因 计算机系的男同学追班里一女同学,结果此女总是躲躲闪闪。 男的看没戏,就另找了一个去追,结果这女的不满意了,质问这男的为啥抛弃她。 男的问:“请教一个电脑问题,如果你点击一个程序,总是提示‘没有响应’,怎么办?” 女的说:“马上结束任务。” 男的:“对,我也是这样想的。” 对于程序员来说. 没老婆不悲催。悲催的是. 没老婆. 控制台还不停的提示你Error:could not find the object 假如生活欺骗了你,不要悲伤不要心急。《代码大全》会一直陪伴着你…… ”如果你ctrl+alt+del,蹦出任务管理器,你从上到下扫一眼,所有的进程你都认识,知道他们是干什么的,并且知道关掉有什么后果,而且你还能从CPU和内存占用的数字跳动上清楚的知道电脑现在什么状态,那么你应该没有女朋友”………..你妹啊 普通青年用IDE(Visual Studio, Eclipse, XCode);文艺青年用VIM, Emacs;二逼青年将IDE设置成VIM模式。 程序员换IDE相当于搬家,换主力语言相当于改嫁,换操作系统相当于参加FBI证人保护计划…... 热火朝天的办公室,一精壮青年一边啃着馒头,一边看着眼前产品,愁眉紧锁的他陷入了沉思:产品下一步应该怎么走?如何保证代码质量?如何缩短 项目时间?如何控制项目成本?一个个难题需要他思索,抉择。此时,传来项目经理的吆喝:“程旭元,先别敲代码了!给我修下电脑……” 说出我爱你需要3秒,解释需要3小时,证明需要一辈子;bug三个字母,发现需要3秒,找到需要3小时,debug需要一辈子。 我真的想让这个世界变得更好,但是他们不给我源代码……