精华内容
下载资源
问答
  • 从失败中总结经验的例子
    千次阅读
    2012-08-01 00:50:50

    去年4月份,因为种种原因,离开了干了4年的百度。当初也没有想过创业或者内部创业,正如许多百度技术出身的经理一样,厌倦了各种系统架构与协调配合,也厌倦了与一群操蛋的PM合作,希望能够做点自己喜欢的事情。离开百度后进入了一个社交游戏公司,这个时候人人HR找到了我,说是高层希望见面认识认识,结果过去一顿面试,开始让我过去负责糯米的产品和技术,我拒掉了(不是不喜欢团购,是这些产品都太像了,糯米那个时候是半个pm兼职)  ,然后说有个人人和艺龙合资的内部创业项目“风车”(开始叫蜻蜒,后来由于一些原因保留了蜻蜓,让我们另找域名,我找到了风车),旅游方向,觉得不错,而且人人马上就要上市,就过去了。

    现在想想,一句话特到有感觉,那就是:选择大于努力。无论是大学生找工作、是各种原因的换工作、还是与朋友一块创业,谨慎的选择,选择靠谱的公司,选择靠谱的产品,选择靠谱的经理和伙伴,选择靠谱的老板,多花些时间接触和侧面了解,能够避免许多的弯路。我就是走上了这么一条弯路。

    不过,成功失败各有所得,在于正确面对。对我个人发展来说,这段弯路其实还是非常有收获的。如果加以总结沉淀,也能变成以后一种历练。正如一折店的周志清和我说的:陈老太太帮你创业失败买单,还给你发工资,这次创业你实际是赚了。

    我也不是很喜欢写回忆录的人,不打算写流水胀样的记录内部创业的点点滴滴。写些心得,与好朋友起分享,也是帮自己沉淀。大家也别太认真这些东西,毕竟从别人的失败中学到的会很有限,很浅。很多道理很简单很简单。

    1. 别先君子,后小人

    清华读研究生期间,我与几个朋友一块创业,做电信设备供应商   (VoIP信令监测仪)。一台系统能够卖到38万-45万,成本只有2-3万,开始发展还不错,然后很多厂家包括华为也进入这个领域。不过这些外在因素并没有打垮我们,而是几个创始人开始内讧了,CEO陪电信客户欧洲吃喝嫖赌,又不见单子出来;  CTO觉得他也卖了2台,完全可以单独干;兼职做硬件的创始人觉得你们这样搞公司迟早完蛋,赶紧提现退股。

    最后,创业黄了,兄弟见面反目。想想开始一腔热情一起打天下的时候,大家都是君子,说了可以不要工资,可以不要Title,自己掏钱买股份,但是后来有一点点起色后,都开始算计和相互算计了。

    内部创业呢,看著光鲜,丰富的资源和支撑,但是实际做起来很困难,内部创业的成功率应该是没有外部创业高的。刚开始别太乐观,别太信口头承诺,多争取一些,多走下来些现实的书面的东西。

    公司内部创业,内部合作是关键,也是最难的地方。所以, 一般会从各个部门抽调一些人员到这个团队(也有很多是垃圾人员踢到这边的)  ,这样做一方面底层合作流畅一些,另一方面也有利于公司文化的传承。不过,一开始自己并没有意识到这块的问题。当老板把这个项目和团队成员介绍给各个平行Team的头的时候,各个部门的头都会挖心掏肺的表达:欢迎过来一起抢占互联网的制高点,俺们兄弟部门砸锅卖铁也要支持你们。其实,这个话不是说给你听的,而是说给老板听的。

    等到具体合作的事情,一般这样表述:这个事情不是我们的KPI; 你们需要提前准备全年的计划,其中和我们相关的,需要和老板审批;我们产品也想支持,不过RD人子不够;我们RD支持没有问题,但是需要和项目经理商量排期,并且和产品确认需求。你经常说公司很重视这块新业务,他说我们部门现在的确很XXXX…….

    所以,在许多公司都是高管直接抓创新项曰的前期孵化和资源的协调(我和老板写了一个详细的介绍,老板很政府的回答说:Good point!  we will study it。如果不行,可以一开始就把这些资源给沟通好,系统和技术层面需要提升哪些支持;广告资源每个季度提供多少等价位;BD需要做好哪些支持。开始就小入到底,然后和气共事。

    2. 找个靠谱的老板

    最近有个很热门互联网公司找到我,希望我过去,我犹豫了一阵,最后还是放弃了。说实话,我是比较怕那种,创业被收购,然后做大公司做高营的头了。也不是说这种都不靠谱,是我自己一朝被蛇咬,十年怕井绳。

    公司跨部门配合,虽然比较复杂,但是用心经营还是挺好协调的(我在百度写过两篇内部配合的心得文章,现在百度内网还可以搜索到,感兴趣的同学可以找我要)。但是最难配合的,还是创业项目,说白了,你没有资源,光靠许诺和忽悠,没有人屌你。能够支持你的,除了过去的资源,新建立的关系,最有效的还是你的老板。你的老板和你风格是否match,是否积极帮你寻求资源,是否看好这个新方向都是需要去研究的,是否做好了长久的计划和打算。要是你的老板自己直接做产品经理或者开发经理的还是算了吧。

    产品经理被迫离开前和老板聊了许多次,但是后来还是走了。走了后,老板这么对我说的:晓峰,你们这个新产品方向是偷的别人的idea,你和产品总监的问题好像不是你反馈的那样,这些我都会安排人去调查,不过我现在太忙,没有时间搞这个事情,既然新的产品方向已经确定,你们就full speed  ahead,把它做好!

    我是从这个时候开始对人人网失去信心的,也是那个被蛇咬的一下。管理上有个基本的原则   疑人不用,用人不疑。这个管理原则你不懂,但是你至少应该知道人情事故就算怀疑也不要先说出来啊,调查明白先啊,亲!

    有个人人网非常资深的总监,和我一起聊天,她说了一句话,让我有点彻头彻尾的醒悟:内部创业,你首先要想清楚老板要什么,而不是你的用户要什么。这话理论上不一定正确,但是绝对有现实的意义。

    百度文库在11年3月开始,由于种种原因(新到PM的兴趣点,版权问题的困扰) ,开始做电子付费阅读方向。这个当时我是非常反对的,不过我4月就走了。后来百度还收购了已经发不出工资的番薯网,到12年初也彻底的放弃了,团队业务也都解散了。那么,我们需要想想,百度老板对文库的希望是什么?对一群新产品的希望是什么?是希望你每个月赚20万,还是希望你能够去弥补网页搜索的内容?答案肯定是后者了。所以贴吧做打豆豆,空间开放平台接入游戏,文库卖书最后都得歇菜。但是百度游戏的情况又不同了,老板对你的定位是赚点小钱,搞独家代理做面子做品牌的事情也违背初衷。

    那么,作为人人和艺龙合资的项目, 我们需要明白董事会的同学,他们需要什么。我们先来看艺龙, 艺龙是一家传统的OTA公司,老板以前是宝洁卖卫生巾的(这个不是什么讽刺,宝洁的管理非常出色、业务真的做得很好) ,他对蜻蜓的希望只有一个:利用人人的流量,进行产品销售,所以风车下月就要能赚钱。同样的产品在糯米旅游频道, go.qq.com(不知道以后还在不在) ,前者非常Match艺龙的需求,每月也能卖2.4~3万间夜,后者腾讯没有投入推广资源,估计会落得撤掉的下场。所以社交旅游这个概念, 在艺龙听起来,估计就和一坨慢慢发酵的大便一样。我们再看陈老太太,他其实明自社区这个东西,需要经营,花钱也会砸坏这个道理,所以他比较偏互联网一点点。但是面对从20美元跌到3美元的资本市场压力,老太太经常说的是我给你导100万人人同核心用户过去,这个产品能够自己起来么?1 don’t want money, I want success! 他着急出产品,着急出名(不要听一个人喊的现实主义口号,要看一个人追名逐利的行为)。

    3. 善待你的下属

    无论是创业还是内部创业,要和愿意跟随你的手下讲清楚机遇的同时,讲清楚风险。不然失败后,你最大的内疚肯定是对这群跟着你赴汤蹈火的兄弟。

    10年我组建百度日本IS技术团队,有许多的同学都是我“忽悠”过去的,虽然自己尽了最大的力,但是毕竟我也是泥菩萨。10年10月,百度体面的关闭日文业务,这是我这辈子最煎熬的时刻,我想我永远记得花3个月组建的30人的技术团队,花3个月打造成为明星团队,再花3个月时间送走每个的兄弟姐妹。不是因为别的,是因为内疚,因为自己没有和他们说明白风险(或者当时自己没有看到风险)。我现在还记得他们每一个人名字,每一个人的眼神。

    后来到人人,自己已经能够清楚的看到风险了,所以对于每一个同学,我都说了相关的风险。还有几个同学希望过来,也被我拒绝了。风车上线前一个月,刚过完年,老板突然说要关掉产品。那个时候,我也很郁闷,因为2月份是最难找工作的时候,加上12年年初整个IT行业不景气。后来,和老板争取了3个月的时间(5.14号没有骗到钱就散伙) ,上线产品,找融资,到5月份解散的时候,已经比较平静了,该找下家的已经找到了。

    有追随我的手下,降薪加入人人,进入了静安中心的作坊式锅炉房; 有追随我的手下,希望放弃百度Offer加入风车团队,只因为团队氛围和对产品的认可;有追随我的手下,身兼数职,加班加点,只因为团队需要;有追随我的于下,去忍受那复杂的报销制度,恶心的内部推诿。我觉得,人生最大的资产,就是这些朋友。风车在,我对他们每一个人都像朋友,风车不在了,我也会帮助他们每一个人。

    最近和朋友聊,他说,晓峰你有睡不着的时候吗?我说有啊,流量涨我睡不着,流量跌我也睡不着。朋友说,不是这个意思, 麦帮网的刘建国睡不着,是因为那帮追随他的朋友,哪些降薪跟着他的朋友。2月份的时候,因为这个事情的确好几夜睡不着,到5月份的时候,好些了。

    4. 活下去是创业的关键

    现在创业,是越来越容易进入了。自己有什么骨灰级的爱好,做个产品出来;做技术的,可以做些什么服务或者应用出来;做产品的,平时多看业界产品,多看techcruch ,有自己的思路了,Copy一个出来; 做运营的,有什么线下资源,开个小店或者做个流量入口。门槛低是互联网的特点,但是,进入容易也标志着容易死掉。

    自己创业,活下去是关键。你得选个靠谱点方向,或者你的钱够你试错许多次,可能让你交一些学费后找到方向。仔细的规划你的钱怎么花,每一个关键点大概在什么时候,你的钱是否可以支撑到这个时候。什么时候能够开始有收入, ROI什么时候正向。估计每个阶段的风险,估计投资后的问题。关注用户的留存率,关注团队的土气和忍耐度。

    公司内部创业,活下去也是关键。比较麻烦一点的是,内部创业还会面临老板的问题。很多时候,选择的方向不是你定的,而是老板决策后安排的。那么内部创业,首先是要搞清楚,老板到底要什么,搞好向上管理非常重要。如果风车再做二次,我觉得我能够在这个方面做得更好。老板或者董事会不支持你了,再好的规划也不管用。

    举个例子吧。最近接触了家传统的公司,他们董事会希望在互联网行业有所突破, 那么,选择了亲子这个充向,因为老板们一水的都有小孩, 觉得这个方向比较了解,比较有潜力,做什么怎么做他们也不知道。如果让我去做这个项目的CEO,首先,我会了解各个老板的情况,了解投资的大概规模,公司对这个项目时间上的期许(说白了,是很急躁、还是安心发展它,大部分都是前者) ,老板希望这个项目解决他什么问题。然后我再去了解业界的产品,产业链的情况,美国同行的情况。最后做些用户调研,如果碰到做这个行业的产品或者运营就更好了。选择几个切入点, 做好这几个切入点的分析(每个milestone的规模,人力和资本的投入、产出、竞争壁垒;) ,对于有同类产品的,要搞清楚对方发展的具体情况,自己为什么还有机会,对于国内没有的产品, 要搞清楚到人为什么不做(多和到人交流,不要以为自己什么独创, 泄露了机密,你想到的肯定有无数人想到,或者试错了)。最好,还能画些简单而华丽的主要功能UI设计图出来。

    以找投资人的心态和准备,去争取一个你觉得靠谱的方向(符合公司基因,符合市场规律,符合投入规模) ,让他们对这个事情信心十足, 让他们做好更长期投入的打算,让他们明自各种风险都是在你的掌控之下, 让他们知道每个节点的前景和时间点,争取到成文的支持。然后找到符合基因的产品负责人、运营负责人和技术负责人,开始搞起。团队组建、文化组建、公司内部关系梳理这些就不讲了,大同小异。

    做得过程中,也要保持好项目的进展汇报,不断的给老板打气,让他保持对项目的信心。比如,项目的阶段性进展,汇报出来,让老板觉得可控;业界同类产品的发展,汇报出来,让他们看到这个元向的业界认可;内部的支持,汇报出来感谢,让老板知道,让兄弟部门更加支持;对于遇到的重要问题,随时汇报出来,让老板知道你能够handle; 需要什么重要资源,描述清楚投入产出,让老板愿意割肉。老板喜欢细节的,材料准备完善一些;老板喜欢拍马屁的,学学人人网的部门经理。毕竟,大公司里做什么,也都是做职业经理,做好自己能够控制的事情,容忍包容自己不能控制的事情,分清楚哪些能够控制哪些不能控制, 是大道理。

    不是为了别的,是为了创业能够活下去。

    5. 弥补创业基因不足

    风车解散消息传出,很多认识或者不认识朋友都来安慰我,也有人说为啥失败,注定失败什么的。我直认同这样一些道理创业成功是偶然,失败是必然;成功的企业有它的共性,失败的企业各有个的不同;从别人失败的创业中学不到太多有用的东西,只有自己刻骨铭心的失政后才明白这些都是成长的过程。

    最近和朋友聊到做产品经理的基因问题。一个好的产品经理,需要拥有自己去做的产品的个人基因,这样成功的可能性会大得多。比如,不用论坛、微博和Facebook的人做社区的产品不合适;不用360客户端的同学做客户端也不合适;不爱好旅游的同学也别做旅游产品;没有小孩的别做亲子产品;没有退休父母的同学,也别做老年人的产品;不爱摄影、不玩单反的同学, 别做相册产品;不爱AV ,不收藏种子的同学,别做海量存储产品;没有上班一台机器, 下班一台机器的同学,也别设计云存储产品。

    这里描述的一个道理:自挠其痒(scratch your own itch) 。做得产品才有感觉,会用竞争对手的产品,分析优劣势,才会用自己的产品,才会和用户打成片获得反馈。运营&设计也是这个道理, 所以我招运营的同学一定会看看他的微博帐号。

    创业也有个基因的问题,这个是我失败后学会的。我理解呢,创业者分为这样几种:搞纯IT的(学校的,大公司出来的, 产品技术同学) ;传统领域的(媒体、金融、房地产之类的) ;搞运营的(大公司BD 、运营、市场、之前小公司创业的许多人也是) ,不一定全。

    • 搞纯IT的,想法容易过于简单,对市场和用户太乐观。能每做出叫好的产品, 但是很难叫座。非常冲动,有个简单的想法就出来干,也有可能成大事, 比如Facebook的马克。
    • 搞传统的,想法很直接,有资源,做东西离钱很近。做得东西大部分被人鄙视,但是能够很滋润的发小财。非常现实,东西也很简单实用,比如大部分的淘宝店主。
    • 搞运营的,想法比较复杂,对于市场和用户反馈比较正面。比较难做出产品来, 折腾很久才让人知道它到底是啥。这些人许多都是某个行业的骨灰级玩家,有丰富的资源,很容易折腾出各种论坛、组织平台出来,线下搞得风生水起, 来钱很慢。

    Founder是什么人,直接决定着他的创业基因,所以需要比较互补的Co-Founder来弥补(性格弥补之类的是另外个话题了)。搞IT的+搞运营的,可能能够做出一款稳扎稳打的产品, 比如豆瓣,也可能是个很不靠谱的产品;搞IT的+搞传统的,能够做一个比较刚需的产品,比如京东; 搞运营的+搞传统的,能够做出一款很好的产业附属产品,比如大谷; 如果三个方面都不错,那就非常好了,比如微博就是这样个产品(比较专业的产品和设计,很强的媒体和名人背景,超强的运营)。

    6. 产品

    最后说说产品吧,社交用户和社交关系的垂直化商业挖掘,目前是一个不太明朗的话题,不如百度那么好应用在垂直领域(比如mp3搜索,图片视频搜索) 。Facebook也尝试过许多垂直东西,社交商业、社交旅游等,都不是很成功,人人也做经纬、风车,也都死掉了。原因很简单, Social graph在某个领域就变成了个Interest Graph,或者Local Interest Graph,产品重新建立Graph的难度很大。搜索呢不存在这个Graph问题,是垂直化特性满足。

    垂直化的应用, 游戏是一个不错的方向,它兼顾Social和Interest两个属性,又抛弃了Local 的特点。类似的领域,我觉得还有社会热点、明星八卦、读书、听歌、电影。新浪微博、豆瓣把握的不错。

    旅游和大众点评(包括团购)非常不同。前者一年一次,一次掏很多钱,有点像结婚, 一辈子隆重一次,砸锅卖铁; 后者一周一次,一次几块钱,有点像生活,茶米油盐。前者使用频率很低,后者使用频率很高。

    对于使用频率很低的东西,做社交不是很合适(社交就应该鸡毛蒜皮,明星八卦, 愤青政治) ,比较合适做工具类产品(要简单,刚需的) ,做打包的产品(比如组团,包机这种,赚的多) ,做品牌(去哪儿接受百度投资、和携程纠葛) ,做产品销售,做专业内容(道道、 Lonely Planet、马蜂窝)。plus:不过这里面有个需求不是刚需,计划制定,它是一个补充,尤真是线下服务的补充,所以我不是很认可”第一步”、“图客圈”目前的模式是个刚需。

    对于频率很高的东西,做好入口(百度,客户端,预装等) ,做好口碑(好事不出门,坏事传干里) ,做好成本控制与流程优化等等。

    我经常听到有人说要做Social travel、Social Pets、Social baby等等。我一直觉得产品要利用好Social的传播, 把Social当做SEO之后的另一个营销工具。不要做Social产品,除非你真的有这个基因,有时间和资金(国内还是很需要后者) ,也有被腾讯收购或者打败的承担。

    结束语

    最近不少老板找我过去帮忙,也有提供VP、CTO甚至CEO岗位的,也有不少天使找我聊,希望我能够在旅游行业继续做,微博上也有无数的朋友安慰和鼓励我。证明中国互联网也越来越成熟,对于创业失贩的同学也越来越包容和认可。

    也有同行马后炮的说一句,我早就不看好之类的,这些人对我来说没有什么用和影响。

    创业的朋友,都会觉得这个过程中的收获,是值得的, 这就够了。

     

    文章来源:http://sns.itianhe.cn/sns/read.php?tid-2972.html

    更多相关内容
  • 大学如何学习——我的五年失败经验总结

    千次阅读 多人点赞 2020-10-07 15:03:24
    在接下来的内容,我会结合上我5年的经验(3年在校,大四实习,毕业后裸辞考研并一战成硕),提出些许建议,如果觉得有用可以记录下,没有就当看看我过去失败的4年本科时间。 主要是最近和几个新生聊起这件事,就...

    嘿,朋友,如果你偶然间看到了这篇文章,那么可能你现在也处在一个十分迷茫的情况下吧。
    这篇文章只是说一个抛砖引玉,毕竟我也还在漫漫求学路上,而你,有可能现在或者说未来都会比我强很多,所以这篇文章也不是说什么教导,而是我把我这5年的经验都写下来,供大家参考,帮助各位也帮助我自己。
    在接下来的内容中,我会结合上我5年的经验(3年在校,大四实习,毕业后裸辞考研并一战成硕),提出些许建议,如果觉得有用可以记录下,没有就当看看我过去失败的4年本科时间。

    主要是最近和几个新生聊起这件事,就觉得感觉大家有冲劲,但是又不知道该怎么学习,我又不能一对一针对性的讲,太浪费时间了,我也还有一啪啦算法和论文等着在,所以索性就写这么篇博客。

    这事要从高中的时候说起,那时候又爱玩,又立志不读计算机,所以接踵而至的是高考失利,比模拟考低了差不多40-60分,最后阴差阳错因缘际会的来到了一个双非学校的计算机学院物联网工程,当时十分想转专业去传统工科,后来听到了专业介绍后才留了下来。

    当时回想整个高中阶段的日子,觉得十分对不起爸妈,然后大一那一年天天泡图书馆看书,整个大一我成绩是 3 / 105 3 / 105 3/105,又是班长又是做学生会的,就觉得好像计算机也挺简单的嘛,然后就洋洋得意的过了大一。

    大二是我本科这几年的一个分水岭,由于大一上的唯一一门编程课是网页设计(HTML和CSS),当时我拿的是全专业最高分,就觉得好像编程也挺简单的嘛,直到被C++教育做人。那个时候我耍了第一个女朋友,那女生挺爱玩的,而且自诩甚高,认为自己智商180,然后天天带我耍,于是大二那年图书馆也没怎么去了,就每天腻歪在一起,又遇到了C++,毕竟有对象这种事就开始复杂了嘛,你懂的,然后大二上期,我很果断的挂了C++。幸好后来那女的把我绿了,这才从苦海中解脱出来,感谢那个这么不长眼的男的!所以这也是我要说的第一个建议:

    耍朋友可以,大家十几二十岁血气方刚的小伙子小姑凉也很正常,但是要节制,该学习就滚去学习,以后还有那么一辈子,真没必要赔上自己的青春。

    当时挂C++,补考也挂了,于是我就开始迷茫了,我在怀疑我是不是真的不适合读计算机。幸好生命中还是有那么几个贵人,当时认识了我们系主任,又不小心加了个联系方式,那个时候我就天天跑去找他聊天,就跟他谈我不想读计算机了,我想考研跨考到行政管理这一块去(因为当时在学生会做个部长,又打算应聘主席嘛,而且后来也确实应聘上了)。后来和系主任抽了几根烟后,他终于把我劝了回来,老老实实读计算机。所以这也是我想说的第二个事:

    如果觉得迷茫,不妨多找找老师聊聊,毕竟老师基本都是博士学历起步,你觉得很困惑的事情,他看来也就是一点点事,三言两语可能就给你解决了。关于博士学历,我就引用我之前在知乎上看到的一句话(链接找不到了),博士学历意味着这个世界上基本没有任何有技术含量的事情难得到你了。
    当然你也可以找找你认为三观很正,或者说技术很强的学长学姐请教,这个我觉得很重要,如果三观不正(比如天天给你灌输逃课想逃就逃,考试就去作弊)或者技术不强,那么很容易把你带拐(尤其是针对那种刚进校的新生),而拐下去了,就不一定扳的回来了。就好比线代中,如果你行列式为0,那么在实数空间中你是找不到逆矩阵的一样。

    那次和系主任聊过后,就开始天天敲代码,当时和几个朋友组队去做个东西,就天天捣鼓java SE(主要是java swing),捣鼓了一个学期,最后发现,java swing这东西唯一的作用就是中国高校期末考试要考。大三的时候捣鼓java EE,后来实习做产品经理,之后又开始想做Web开发,反正各行各业我都摸过,也都不知道该怎么摸,也没摸熟,就等于我什么都不会,所以这也是第三个建议:

    一定要找清楚自己的方向,比如你以后是打算考研(本专业 | 非本专业)、考博(本专业 | 非本专业)、工作(本专业 | 非本专业)、考公务员,工作又可以再分为硬件、软件等。但是这个东西定下来后就不要随意更改了,不然你就会像我一样浪费大把时间,强行反复提高自己学习成本,所以不明确的话,就多问问老师、学长学姐,多看看综述。

    而且我觉得这个方向,一定是个大的方向,现在回想才发现为什么高中班主任第一次班会要我们写下5年的目标。因为这个方向会指引你去完成它,就像我现在的目标很明确,三年后去川大软件工程读博。然后你就可以在这个目标的基础上完善近期的目标,就比如我这三年研究生(其实细说只有2年,因为研三就要开始准备申请了)的目标现在暂定的是,研一做情感分析(sentiment analysis),争取今年12月弄篇3区以上的论文,最迟明年5月出,然后开始转用户画像(user profile)。保底在毕业时能够有至少3篇3区以上的文章。当然,可能会有人说“就这?”,因为我相信裤裆大了什么鸟都有,呸,林子大了什么鸟都有,所以理论上来讲,肯定会有钢精看得到这篇文章,所以我还是想坦白,我自认为我智商情商都不高(没测过,怕打击到自己弱小的自尊心),所以我也只能在有限的智商里面尽可能的做更好的事情。

    然后还有个动手能力这个吧,大家都是工科生,都清楚动手能力对我们工科生意味着什么,当然真不是歧视,反正在我这么多年的经验来看,动手能力强的女生真没几个……我还是想说,如果你一个计算机科班出身的,你连一个毕业设计都做不出来,那么你觉得你这四年对得起谁?所以这也算是第四个建议吧:

    遇到问题,多动动手,我一直在拿写代码和写作文做比较,我认为写代码就像写作文一样,你得先有逻辑,先有思路,才能把你脑海中的字组成词,词组成句,而且大家都是面向百度编程的嘛,语法不会百度下不就得了?更重要的是思路,思路怎么来?多动手做,做着做着就会了,好好看,好好学。

    后来我和室友闹崩了,这里我必须把这两个室友拿出来鞭尸。我有两个室友,一个每天晚上打游戏到两三点,还砸键盘,骂电脑(因为熄灯了,他玩的单机,反正我是真搞不懂玩个单机有啥好骂好砸的),另外一个看动漫看到两三点,虽然戴耳机,但是耳机漏音,而且是不是很阴森的给你“嘿嘿嘿”的笑一下,同时两个人有时候还要交流心得,交流的声音我也不夸大,反正就是他们白天说话的那个音量。他们的作息时间我毕业的时候整理了一份:

    • 04:00-13:00,睡觉
    • 13:00-16:00,床上耍手机
    • 16:00-17:00,起床洗漱
    • 17:00-23:30,出门网吧上网,顺便吃晚饭(是的你没看错,一天只有一顿饭,两个接近一米八,体重好像只有80几)
    • 23:30-03:00,看动漫,耍游戏
    • 03:00-04:00,耍手机

    大体是这样,差别不会太大。后来我就忍受不了,又想毕业的时候感谢室友不杀之恩,所以我就搬出去住了。我很感谢幸好我这最后一点自律还存在,还没有迷失自己成为行尸走肉,所以这也是第五个建议:

    交友要慎重,近朱者赤近墨者黑。

    接着时间推移,推移到了上班,俗话说得好,第一份工作基本决定了你以后的路,然后我又很幸运的遇到了这家*公司。在这里,我第一次遇到了社会的毒打。首先最直观的感受是,再不会有人对你倾囊相授了。做产品经理第一天,十一点过,主管让我画个矢量图,下午两点交,我从没碰过,当时一脸懵逼,然后一直百度,到两点终于交了。

    后来遇到的第一个正式的事情是录制用户手册的视频,当时我根本不知道用户手册是个啥,就找一个女同事求助,然后那天下午,我在录视频,她就在旁边耍手机,我录了一下午,她到下班点就走,还顺理成章把她下午应该做的工作推给了我。

    这在社会中也很正常,因为他教了你,你就会威胁到他的地位,有可能你上去了然后成了他的上司。这也对应了上面我说的为什么我看到的女生动手能力都不强的原因,因为很多情况下女生撒个娇卖个萌,总会有人帮你解决问题的,但是这永远不是解决问题的方法,因为你要收获,就得有付出,而且付出一般来说永远比收获的多,毕竟没有免费午餐定律嘛。所以这里也是第六个建议吧:

    遇到问题自行思考,自行解决,毕竟计算机的社区很强大,遇到实在不会的再考虑问人,毕竟学校还是相对来说单纯的。但是如果遇到问题就问人,那么你其实错过了自行思考自行解决的这么一个过程,这个过程是我觉得无论是以后工作还是科研过程中很重要的一个环节。

    最后我实在忍受不了那个小公司,然后选择了辞职,辞职无非就是钱没给够,又受了委屈嘛,反正我是钱也没有,委屈还一啪啦,选择了考研。这我觉得是我人生中至今为止做的最正确的一个决定吧,虽然又回到了本科的学校,但是这一次我有了新的方向,新的目标,而且很坚定。我先附一个我考研的作息表吧,再谈谈第七个建议:
    考研作息表

    我觉得吧,无论什么事,你要做你认为有意义的事情。就像系主任从大二的时候开始经常对我说的一句话“你要想清楚要做个什么样的人”。比如我考研的时候实在学不进去了,那么我会选择去网吧,毕竟我坐着也是浪费时间浪费心情,为何还要一遍又一遍折磨自己的心态,不如去放松?但是你要对得起你的每一次选择。

    最后还是加一个建议,因为我有强迫症,而且是完美主义,所以每次写博客或者给别人讲题的时候,都是时常在想我写的或讲的清不清楚,明不明白,他人看不看得懂,但是我要说的核心并非是这种精神,而是:

    你要怎样把所学的转换为自己的,我认为最好的方法就是学到了之后给别人讲一遍,看别人能否明白,这样你也可以重新巩固你的知识,查找遗漏点,就像我之前写的每一篇博客,我的查阅了许多的资料,把很多资料融会贯通了起来,才开始写的,有的时候写的东西我自认为没写明白的,又开始查阅资料,反反复复的,这样也有了更清楚的认识。就是说你要怎样有了输入值后,如何输出的问题。就比如盗个经典神经网络的图:

    神经网络

    写在最后

    写这个文章吧,不是说我说的做的就是对的,因为我也还是个摸索者,每个人的想法做法也都不一样,我这也是用这5年的失败总结出来的经验,大家看着觉得有用不妨点个赞哈哈哈,毕竟写了快4000字了,没用的话就当做是个笑话吃饭的时候多刨两口,不一定防脱发,但能保证你吃得饱对吧。

    最后再送一句我经常对学弟学妹,也是师兄对我说的话:

    你还有时间

    ====================================

    更新于20-10-13,主要之前写漏了,这两天突然想起来了……

    你在敲代码的时候也经常遇到bug,或者你刚入门经常遇到的语法错误,都是很正常的事,有的人也因此放弃了计算机这个香饽饽。然后再说回我自己,上面写到了我工作的时候的事情,其实最主要的是,我去那家公司其实就是个错误的决定,再决定转岗去做**产品经理又是错上加错,所以才辞职回来考的研。
    程序员三连

    不过我的这一段人生经历是极其宝贵的,因为如果我没有经历过这一段的失败,那么我就根本不知道我要的是什么,我也是浑浑噩噩混日子摸鱼的那种混吃等死的上班族之一,就是说你没有经历过失去,你就不会理解到自己曾经拥有过的东西,更不会现在手上拥有的宝贝。所以我这里也再补充一个建议:

    俗话说得好,吃一堑长一智,年轻人永远不要怕犯错,因为你在校,犯的错大部分都是小问题(虽然在你当时看起来可能很严重),你有的是大把的时间修正,而对于你这个错误而言,真正宝贵的是经验,能够帮助你以后不再犯类似的错误,同时这种发现问题,寻找方案,解决错误的能力也是我们工科生必备的能力之一。

    ================================

    更新于20-10-15,因为昨天大连发生了那个事嘛,就有点深有感触

    就像蜡笔小新一样,他身边的每个同龄朋友都有很多梦想,我只记得那个装逼男的梦想好像是立志做个科学家,于是经常去补课那些。就像我们一样,身边总是有许多人有形形色色伟大的梦想,但是最后又有多少人成为了自己想要的模样?又有多少人活成了小新爸爸妈妈那种中年油腻的形象?

    我举着个例子并不是说梦想不重要,而是我们要学会接受自己的平庸。在某一阶段越优秀的人就越难接受自己下一阶段的失败(比如我本科有个同学,班上成绩中等左右吧,到大四都还念念不忘自己的高考理综成绩,都大四了还天天拿出来吹牛逼)。

    这个社会总是会有比你强的人,山外有山,天外有天,许多人在经历了社会的毒打后就会慢慢发现自己也就是茫茫人海中那千篇一律的那一个(比如我,以前上班的时候每天都要挨骂,有一次隔壁部门一个领导深夜一个电话打过来骂了我一个多小时)。而我所说的平庸,并非是你从此开始放弃,自暴自弃,而是:

    你要接受别人比你强,只有强者才会用欣赏的眼神去看待别人。同时你要在你有限的能力里面尽可能做出最好的结果。别给自己太大压力,你还有亲人,你还有兄弟,你甚至有可能还有对象,除了(广义的)成绩以外,你还有许多快乐的时光值得你骄傲!

    展开全文
  • 华为C语言编程规范(精华总结

    万次阅读 多人点赞 2020-03-24 09:48:55
    另外,一旦把私有定义、声明放到独立的头文件,就无法技术上避免别人include之,难以保证这些定义最后真的只是私有的。 5、禁止头文件循环依赖 头文件循环依赖,指a.h包含b.h,b.h包含c.h,c.h包含a.h之类导致...

    目录

    1、代码总体原则

    2、头文件

    2、函数

    3、标识符命名与定义

    4、变量

    5、宏、常量

    6、表达式

    7、注释

    8、排版与格式

    9、代码编辑编译 


    “编写程序应该以人为本,计算机第二。”                                                               

                                                                                                                                           ——Steve McConnell

    “无缘进华为,但可以用华为的标准要求自己。”                                                               

                                                                                                                                           ——不脱发的程序猿


    1、代码总体原则

    1、清晰第一 

    清晰性是易于维护、易于重构的程序必需具备的特征。代码首先是给人读的,好的代码应当可以像文章一样发声朗诵出来。

    目前软件维护期成本占整个生命周期成本的40%~90%。根据业界经验,维护期变更代码的成本,小型系统是开发期的5倍,大型系统(100万行代码以上)可以达到100倍。业界的调查指出,开发组平均大约一半的人力用于弥补过去的错误,而不是添加新的功能来帮助公司提高竞争力。

    一般情况下,代码的可阅读性高于性能,只有确定性能是瓶颈时,才应该主动优化。

    2、简洁为美

    简洁就是易于理解并且易于实现。代码越长越难以看懂,也就越容易在修改时引入错误。写的代码越多,意味着出错的地方越多,也就意味着代码的可靠性越低。因此,我们提倡大家通过编写简洁明了的代码来提升代码可靠性。

    废弃的代码(没有被调用的函数和全局变量)要及时清除,重复代码应该尽可能提炼成函数。

    3、选择合适的风格,与代码原有风格保持一致

    产品所有人共同分享同一种风格所带来的好处,远远超出为了统一而付出的代价。在公司已有编码规范的指导下,审慎地编排代码以使代码尽可能清晰,是一项非常重要的技能。 如果重构/ / 修改其他风格的代码时,比较明智的做法是根据 现有 代码 的 现有风格继续编写代码,或者使用格式转换工具进行转换成公司内部风格。


    2、头文件

    对于C语言来说,头文件的设计体现了大部分的系统设计。 不合理的头文件布局是编译时间过长的根因,不合理的头文件实际上反映了不合理的设计。

    1、头文件中适合放置接口的声明,不适合放置实现

    头文件是模块(Module)或单元(Unit)的对外接口。头文件中应放置对外部的声明,如对外提供的函数声明、宏定义、类型定义等。

    要求:

    • 内部使用的函数(相当于类的私有方法)声明不应放在头文件中。
    • 内部使用的宏、枚举、结构定义不应放入头文件中。
    • 变量定义不应放在头文件中,应放在.c文件中。
    • 变量的声明尽量不要放在头文件中,亦即尽量不要使用全局变量作为接口。变量是模块或单元的内部实现细节,不应通过在头文件中声明的方式直接暴露给外部,应通过函数接口的方式进行对外暴露。 即使必须使用全局变量,也只应当在.c中定义全局变量,在.h中仅声明变量为全局的。

    2、头文件应当职责单一,切忌依赖复杂

    头文件过于复杂,依赖过于复杂是导致编译时间过长的主要原因。很多现有代码中头文件过大,职责过多,再加上循环依赖的问题,可能导致为了在.c中使用一个宏,而包含十几个头文件。

    错误示例:某平台定义WORD类型的头文件:

    #include <VXWORKS.H>
    #include <KERNELLIB.H>
    #include <SEMLIB.H>
    #include <INTLIB.H>
    #include <TASKLIB.H>
    #include <MSGQLIB.H>
    #include <STDARG.H>
    #include <FIOLIB.H>
    #include <STDIO.H>
    #include <STDLIB.H>
    #include <CTYPE.H>
    #include <STRING.H>
    #include <ERRNOLIB.H>
    #include <TIMERS.H>
    #include <MEMLIB.H>
    #include <TIME.H>
    #include <WDLIB.H>
    #include <SYSLIB.H>
    #include <TASKHOOKLIB.H>
    #include <REBOOTLIB.H>
    …
    typedef unsigned short WORD;
    …

    这个头文件不但定义了基本数据类型WORD,还包含了stdio.h syslib.h等等不常用的头文件。如果工程中有10000个源文件,而其中100个源文件使用了stdio.h的printf,由于上述头文件的职责过于庞大,而WORD又是每一个文件必须包含的,从而导致stdio.h/syslib.h等可能被不必要的展开了9900次,大大增加了工程的编译时间。

    3、头文件应向稳定的方向包含 

    头文件的包含关系是一种依赖,一般来说,应当让不稳定的模块依赖稳定的模块,从而当不稳定的模块发生变化时,不会影响(编译)稳定的模块。

    就我们的产品来说,依赖的方向应该是: 产品依赖于平台,平台依赖于标准库。某产品线平台的代码中已经包含了产品的头文件,导致平台无法单独编译、发布和测试,是一个非常糟糕的反例。除了不稳定的模块依赖于稳定的模块外,更好的方式是两个模块共同依赖于接口,这样任何一个模块的内部实现更改都不需要重新编译另外一个模块。在这里,我们假设接口本身是最稳定的。

    4、每一个 .c 文件应有一个同名 .h 文件,用于声明需要对外公开的接口

    如果一个.c文件不需要对外公布任何接口,则其就不应当存在,除非它是程序的入口,如main函数所在的文件。

    现有某些产品中,习惯一个.c文件对应两个头文件,一个用于存放对外公开的接口,一个用于存放内部需要用到的定义、声明等,以控制.c文件的代码行数。编者不提倡这种风格。这种风格的根源在于源文件过大,应首先考虑拆分.c文件,使之不至于太大。另外,一旦把私有定义、声明放到独立的头文件中,就无法从技术上避免别人include之,难以保证这些定义最后真的只是私有的。

    5、禁止头文件循环依赖

    头文件循环依赖,指a.h包含b.h,b.h包含c.h,c.h包含a.h之类导致任何一个头文件修改,都导致所有包含了a.h/b.h/c.h的代码全部重新编译一遍。而如果是单向依赖,如a.h包含b.h,b.h包含c.h,而c.h不包含任何头文件,则修改a.h不会导致包含了b.h/c.h的源代码重新编译。

    6、  .c/.h文件禁止包含用不到的头文件

    很多系统中头文件包含关系复杂,开发人员为了省事起见,可能不会去一一钻研,直接包含一切想到的头文件,甚至有些产品干脆发布了一个god.h,其中包含了所有头文件,然后发布给各个项目组使用,这种只图一时省事的做法,导致整个系统的编译时间进一步恶化,并对后来人的维护造成了巨大的麻烦。

    7、  头文件应当自包含 

    简单的说,自包含就是任意一个头文件均可独立编译。如果一个文件包含某个头文件,还要包含另外一个头文件才能工作的话,就会增加交流障碍,给这个头文件的用户增添不必要的负担。

    示例:如果a.h不是自包含的,需要包含b.h才能编译,会带来的危害:每个使用a.h头文件的.c文件,为了让引入的a.h的内容编译通过,都要包含额外的头文件b.h。额外的头文件b.h必须在a.h之前进行包含,这在包含顺序上产生了依赖。

    注意:该规则需要与“.c/.h文件禁止包含用不到的头文件”规则一起使用,不能为了让a.h自包含,而在a.h中包含不必要的头文件。a.h要刚刚可以自包含,不能在a.h中多包含任何满足自包含之外的其他头文件。

    8、总是编写内部 #include 保护符( #define  保护)

    多次包含一个头文件可以通过认真的设计来避免。如果不能做到这一点,就需要采取阻止头文件内容被包含多于一次的机制。通常的手段是为每个文件配置一个宏,当头文件第一次被包含时就定义这个宏,并在头文件被再次包含时使用它以排除文件内容。所有头文件都应当使用#define 防止头文件被多重包含,命名格式为FILENAME_H,为了保证唯一性,更好的命名是PROJECTNAME_PATH_FILENAME_H。

    注:没有在宏最前面加上单下划线"_",是因为一般以单下划线"_"和双下划线"__"开头的标识符为ANSIC等使用,在有些静态检查工具中,若全局可见的标识符以"_"开头会给出告警。

    定义包含保护符时,应该遵守如下规则:

    • 保护符使用唯一名称;

    • 不要在受保护部分的前后放置代码或者注释。

    正确示例:假定VOS工程的timer模块的timer.h,其目录为VOS/include/timer/timer.h,应按如下方式保护:

    #ifndef VOS_INCLUDE_TIMER_TIMER_H
    #define VOS_INCLUDE_TIMER_TIMER_H
    ...
    #endif
    
    也可以使用如下简单方式保护:
    
    #ifndef TIMER_H
    #define TIMER_H
    ...
    #endif

    例外情况:头文件的版权声明部分以及头文件的整体注释部分(如阐述此头文件的开发背景、使用注意事项等)可以放在保护符(#ifndef XX_H)前面。

    9、禁止在头文件中定义变量

    在头文件中定义变量,将会由于头文件被其他.c文件包含而导致变量重复定义。

    10、只能通过包含头文件的方式使用其他 .c 提供的接口,禁止在.c 中通过 extern 的方式使用外部函数接口、变量

    若a.c使用了b.c定义的foo()函数,则应当在b.h中声明extern int foo(int input);并在a.c中通过#include <b.h>来使用foo。禁止通过在a.c中直接写extern int foo(int input);来使用foo,后面这种写法容易在foo改变时可能导致声明和定义不一致。

    11、禁止在 extern "C" 中包含头文件

    在extern "C"中包含头文件,会导致extern "C"嵌套,Visual Studio对extern "C"嵌套层次有限制,嵌套层次太多会编译错误。
    在extern "C"中包含头文件,可能会导致被包含头文件的原有意图遭到破坏。

    错误示例:

    extern “C”
    {
    #include “xxx.h”
    ...
    }
    

    正确示例:

    #include “xxx.h”
    extern “C”
    {
    ...
    } 

    12、一个模块通常包含多个 .c 文件,建议放在同一个目录下,目录名即为模块名。为方便外部使用者,建议每一个模块提供一个 .h ,文件名为目录名

    需要注意的是,这个.h并不是简单的包含所有内部的.h,它是为了模块使用者的方便,对外整体提供的模块接口。以Google test(简称GTest)为例,GTest作为一个整体对外提供C++单元测试框架,其1.5版本的gtest工程下有6个源文件和12个头文件。但是它对外只提供一个gtest.h,只要包含gtest.h即可使用GTest提供的所有对外提供的功能,使用者不必关系GTest内部各个文件的关系,即使以后GTest的内部实现改变了,比如把一个源文件c拆成两个源文件,使用者也不必关心,甚至如果对外功能不变,连重新编译都不需要。对于有些模块,其内部功能相对松散,可能并不一定需要提供这个.h,而是直接提供各个子模块或者.c的头文件。

    比如产品普遍使用的VOS,作为一个大模块,其内部有很多子模块,他们之间的关系相对比较松散,就不适合提供一个vos.h。而VOS的子模块,如Memory(仅作举例说明,与实际情况可能有所出入),其内部实现高度内聚,虽然其内部实现可能有多个.c和.h,但是对外只需要提供一个Memory.h声明接口。

    13、如果一个模块包含多个子模块,则建议每一个子模块提供一个对外的 .h,文件名为子模块名

    降低接口使用者的编写难度

    14、头文件不要使用非习惯用法的扩展名,如 .inc 

    目前很多产品中使用了.inc作为头文件扩展名,这不符合c语言的习惯用法。在使用.inc作为头文件扩展名的产品,习惯上用于标识此头文件为私有头文件。但是从产品的实际代码来看,这一条并没有被遵守,一个.inc文件被多个.c包含比比皆是。

    除此之外,使用.inc还导致source insight、Visual stduio等IDE工具无法识别其为头文件,导致很多功能不可用,如“跳转到变量定义处”。虽然可以通过配置,强迫IDE识别.inc为头文件,但是有些软件无法配置,如Visual Assist只能识别.h而无法通过配置识别.inc。

    15、同一产品统一包含头文件排列方式

    常见的包含头文件排列方式:功能块排序、文件名升序、稳定度排序。

    正确示例1:以升序方式排列头文件可以避免头文件被重复包含:

    #include <a.h>
    #include <b.h>
    #include <c/d.h>
    #include <c/e.h>
    #include <f.h>

    正确示例2:以稳定度排序,建议将不稳定的头文件放在前面,如把产品的头文件放在平台的头文件前面:

    #include <product.h>
    #include <platform.h>

    相对来说,product.h修改的较为频繁,如果有错误,不必编译platform.h就可以发现product.h的错误,可以部分减少编译时间。


    2、函数

    函数设计的精髓:编写整洁函数,同时把代码有效组织起来。

    整洁函数要求:代码简单直接、不隐藏设计者的意图、用干净利落的抽象和直截了当的控制语句将函数有机组织起来。

    代码的有效组织包括:逻辑层组织和物理层组织两个方面。逻辑层,主要是把不同功能的函数通过某种联系组织起来,主要关注模块间的接口,也就是模块的架构。物理层,无论使用什么样的目录或者名字空间等,需要把函数用一种标准的方法组织起来。例如:设计良好的目录结构、函数名字、文件组织等,这样可以方便查找。

    1、一个函数仅完成一件功能

    一个函数实现多个功能给开发、使用、维护都带来很大的困难。

    将没有关联或者关联很弱的语句放到同一函数中,会导致函数职责不明确,难以理解,难以测试和改动。

    2、重复代码应该尽可能提炼成函数

    重复代码提炼成函数可以带来维护成本的降低。

    重复代码是我司不良代码最典型的特征之一。在“代码能用就不改”的指导原则之下,大量的烟囱式设计及其实现充斥着各产品代码之中。新需求增加带来的代码拷贝和修改,随着时间的迁移,产品中堆砌着许多类似或者重复的代码。

    项目组应当使用代码重复度检查工具,在持续集成环境中持续检查代码重复度指标变化趋势,并对新增重复代码及时重构。当一段代码重复两次时,即应考虑消除重复,当代码重复超过三次时,应当立刻着手消除重复。

    3、避免函数过长,新增函数不超过 50 行 (非空非注释行) 

    过长的函数往往意味着函数功能不单一,过于复杂。

    函数的有效代码行数,即NBNC(非空非注释行)应当在[1,50]区间。

    例外:某些实现算法的函数,由于算法的聚合性与功能的全面性,可能会超过50行。

    延伸阅读材料: 业界普遍认为一个函数的代码行不要超过一个屏幕,避免来回翻页影响阅读;一般的代码度量工具建议都对此进行检查,例如Logiscope的函数度量:"Number of Statement" (函数中的可执行语句数)建议不超过20行,QA C建议一个函数中的所有行数(包括注释和空白行)不超过50行。

    4、避免函数的代码块嵌套过深,新增函数的代码块嵌套不超过4层

    函数的代码块嵌套深度指的是函数中的代码控制块(例如:if、for、while、switch等)之间互相包含的深度。每级嵌套都会增加阅读代码时的脑力消耗,因为需要在脑子里维护一个“栈”(比如,进入条件语句、进入循环„„)。应该做进一步的功能分解,从而避免使代码的阅读者一次记住太多的上下文。优秀代码参考值:[1, 4]。

    错误示例:代码嵌套深度为5层:

    void serial (void)
    {
        if (!Received)
        {
            TmoCount = 0;
             switch (Buff)
            {
                case AISGFLG:
                    if ((TiBuff.Count > 3)&& ((TiBuff.Buff[0] == 0xff) || (TiBuf.Buff[0] == CurPa.ADDR)))
                    {
                        Flg7E = false;
                        Received = true;
                    }
                    else
                    {
                        TiBuff.Count = 0;
                        Flg7D = false;
                        Flg7E = true;
                    }
                    break;
                default:
                    break;
            }
        }
    }

    5、 可重入函数应避免使用共享变量;若需要使用,则应通过互斥手段(关中断、信号量)对其加以保护

    可重入函数是指可能被多个任务并发调用的函数。在多任务操作系统中,函数具有可重入性是多个任务可以共用此函数的必要条件。共享变量指的全局变量和static变量。编写C语言的可重入函数时,不应使用static局部变量,否则必须经过特殊处理,才能使函数具有可重入性。

    示例:函数square_exam返回g_exam平方值。那么如下函数不具有可重入性。

    int g_exam;
    unsigned int example( int para )
    {
        unsigned int temp;
        g_exam = para; // (**)
        temp = square_exam ( );
        return temp;
    }

    此函数若被多个线程调用的话,其结果可能是未知的,因为当(**)语句刚执行完后,另外一个使用本函数的线程可能正好被激活,那么当新激活的线程执行到此函数时,将使g_exam赋于另一个不同的para值,所以当控制重新回到“temp =square_exam ( )”后,计算出的temp很可能不是预想中的结果。此函数应如下改进。 

    int g_exam;
    unsigned int example( int para )
    {
        unsigned int temp;
        [申请信号量操作] // 若申请不到“信号量”,说明另外的进程正处于
        g_exam = para; //给g_exam赋值并计算其平方过程中(即正在使用此
        temp = square_exam( ); // 信号),本进程必须等待其释放信号后,才可继
        [释放信号量操作] // 续执行。其它线程必须等待本线程释放信号量后
        // 才能再使用本信号。
        return temp;
    }

    6、对参数的合法性检查,由调用者负责还是由接口函数负责,应在项目组/模块内应统一规定。缺省由调用者负责。

    对于模块间接口函数的参数的合法性检查这一问题,往往有两个极端现象,即:要么是调用者和被调用者对参数均不作合法性检查,结果就遗漏了合法性检查这一必要的处理过程,造成问题隐患;要么就是调用者和被调用者均对参数进行合法性检查,这种情况虽不会造成问题,但产生了冗余代码,降低了效率。

    7、对函数的错误返回码要全面处理

    一个函数(标准库中的函数/第三方库函数/用户定义的函数)能够提供一些指示错误发生的方法。这可以通过使用错误标记、特殊的返回数据或者其他手段,不管什么时候函数提供了这样的机制,调用程序应该在函数返回时立刻检查错误指示。

    8、设计高扇入,合理扇出(小于7)的函数

    扇出是指一个函数直接调用(控制)其它函数的数目,而扇入是指有多少上级函数调用它。

    扇出过大,表明函数过分复杂,需要控制和协调过多的下级函数;而扇出过小,例如:总是1,表明函数的调用层次可能过多,这样不利于程序阅读和函数结构的分析,并且程序运行时会对系统资源如堆栈空间等造成压力。通常函数比较合理的扇出(调度函数除外)通常是3~5。 

    扇出太大,一般是由于缺乏中间层次,可适当增加中间层次的函数。扇出太小,可把下级函数进一步分解多个函数,或合并到上级函数中。当然分解或合并函数时,不能改变要实现的功能,也不能违背函数间的独立性。扇入越大,表明使用此函数的上级函数越多,这样的函数使用效率高,但不能违背函数间的独立性而单纯地追求高扇入。公共模块中的函数及底层函数应该有较高的扇入。

    较良好的软件结构通常是顶层函数的扇出较高,中层函数的扇出较少,而底层函数则扇入到公共模块中。

    9、废弃代码(没有被调用的函数和变量) ) 要及时清除

    程序中的废弃代码不仅占用额外的空间,而且还常常影响程序的功能与性能,很可能给程序的测试、维护等造成不必要的麻烦。

    10、函数不变参数使用const 

    不变的值更易于理解/跟踪和分析,把const作为默认选项,在编译时会对其进行检查,使代码更牢固/更安全。

    正确示例:C99标准 7.21.4.4 中strncmp 的例子,不变参数声明为const。

    int strncmp(const char *s1, const char *s2, register size_t n)
    {
        register unsigned char u1, u2;
        while (n-- > 0)
        {
            u1 = (unsigned char) *s1++;
            u2 = (unsigned char) *s2++;
            if (u1 != u2)
            {
                return u1 - u2;
            }
            if (u1 == '\0')
            {
                return 0;
            }
        }
        return 0;
    }

    11、函数应避免使用全局变量、静态局部变量和 I/O 操作,不可避免的地方应集中使用

    带有内部“存储器”的函数的功能可能是不可预测的,因为它的输出可能取决于内部存储器(如某标记)的状态。这样的函数既不易于理解又不利于测试和维护。在C语言中,函数的static局部变量是函数的内部存储器,有可能使函数的功能不可预测。

    错误示例:如下函数,其返回值(即功能)是不可预测的。

    unsigned int integer_sum( unsigned int base )
    {
        unsigned int index;
        static unsigned int sum = 0;// 注意,是static类型的。
        // 若改为auto类型,则函数即变为可预测。
        for (index = 1; index <= base; index++)
        {
            sum += index;
        }
        return sum;
    }

    12、检查函数所有非参数输入的有效性,如数据文件、公共变量等

    函数的输入主要有两种:一种是参数输入;另一种是全局变量、数据文件的输入,即非参数输入。函数在使用输入参数之前,应进行有效性检查。

    13、 函数的参数个数不超过5个

    函数的参数过多,会使得该函数易于受外部(其他部分的代码)变化的影响,从而影响维护工作。函数的参数过多同时也会增大测试的工作量。

    函数的参数个数不要超过5个,如果超过了建议拆分为不同函数。

    14、除打印类函数外,不要使用可变长参函数。

    可变长参函数的处理过程比较复杂容易引入错误,而且性能也比较低,使用过多的可变长参函数将导致函数的维护难度大大增加。

    15、在源文件范围内声明和定义的所有函数,除非外部可见,否则应该增加static关键字

    如果一个函数只是在同一文件中的其他地方调用,那么就用static声明。使用static确保只是在声明它的文件中是可见的,并且避免了和其他文件或库中的相同标识符发生混淆的可能性。

    正确示例:建议定义一个STATIC宏,在调试阶段,将STATIC定义为static,版本发布时,改为空,以便于后续的打热补丁等操作。

    #ifdef _DEBUG
    #define STATIC static
    #else
    #define STATIC
    #endif

    3、标识符命名与定义

    标识符的命名规则历来是一个敏感话题,典型的命名风格如unix风格、windows风格等,从来无法达成共识。实际上,各种风格都有其优势也有其劣势,而且往往和个人的审美观有关。我们对标识符定义主要是为了让团队的代码看起来尽可能统一,有利于代码的后续阅读和修改,产品可以根据自己的实际需要指定命名风格,规范中不再做统一的规定。

    1、标识符的命名要清晰、明了,有明确含义,同时使用完整的单词或大家基本可以理解的缩写,避免使人产生误解

    尽可能给出描述性名称,不要节约空间,让别人很快理解你的代码更重要。

    正确示例:

    int error_number;
    int number_of_completed_connection;

    错误示例:

    int n;
    int nerr;
    int n_comp_conns;

    2、除了常见的通用缩写以外,不使用单词缩写,不得使用汉语拼音

    较短的单词可通过去掉“元音”形成缩写,较长的单词可取单词的头几个字母形成缩写,一些单词有大家公认的缩写,常用单词的缩写必须统一。协议中的单词的缩写与协议保持一致。对于某个系统使用的专用缩写应该在注视或者某处做统一说明。

    正确示例:一些常见可以缩写的例子:

    argument 可缩写为 arg
    buffer 可缩写为 buff
    clock 可缩写为 clk
    command 可缩写为 cmd
    compare 可缩写为 cmp
    configuration 可缩写为 cfg
    device 可缩写为 dev
    error 可缩写为 err
    hexadecimal 可缩写为 hex
    increment 可缩写为 inc
    initialize 可缩写为 init
    maximum 可缩写为 max
    message 可缩写为 msg
    minimum 可缩写为 min
    parameter 可缩写为 para
    previous 可缩写为 prev
    register 可缩写为 reg
    semaphore 可缩写为 sem
    statistic 可缩写为 stat
    synchronize 可缩写为 sync
    temp 可缩写为 tmp

    3、产品/项目组内部应保持统一的命名风格

    Unix like和windows like风格均有其拥趸,产品应根据自己的部署平台,选择其中一种,并在产品内部保持一致。

    4、用正确的反义词组命名具有互斥意义的变量或相反动作的函数等

    正确示例:

    add/remove begin/end create/destroy
    insert/delete first/last get/release
    increment/decrement put/get add/delete
    lock/unlock open/close min/max
    old/new start/stop  next/previous
    source/target show/hide  send/receive
    source/destination copy/paste up/down

    5、尽量避免名字中出现数字编号,除非逻辑上的确需要编号

    错误示例:如下命名,使人产生疑惑。

    #define EXAMPLE_0_TEST_
    #define EXAMPLE_1_TEST_

    正确示例:应改为有意义的单词命名。

    #define EXAMPLE_UNIT_TEST_
    #define EXAMPLE_ASSERT_TEST_

    6、标识符前不应添加模块、项目、产品、部门的名称作为前缀

    很多已有代码中已经习惯在文件名中增加模块名,这种写法类似匈牙利命名法,导致文件名不可读,并且带来带来如下问题:

    • 第一眼看到的是模块名,而不是真正的文件功能,阻碍阅读;
    • 文件名太长;
    • 文件名和模块绑定,不利于维护和移植。若foo.c进行重构后,从a模块挪到b模块,若foo.c
    • 中有模块名,则需要将文件名从a_module_foo.c改为b_module_foo.c。

    7、平台/ / 驱动等适配代码的标识符命名风格保持和平台

    涉及到外购芯片以及配套的驱动,这部分的代码变动(包括为产品做适配的新增代码),应该保持原有的风格。

    8、重构/修改部分代码时,应保持和原有代码的命名风格一致

    根据源代码现有的风格继续编写代码,有利于保持总体一致。

    9、文件命名统一采用小写字符

    因为不同系统对文件名大小写处理会不同(如MS的DOS、Windows系统不区分大小写,但是Linux系统则区分),所以代码文件命名建议统一采用全小写字母命名。

    10、全局变量应增加“g_” 前缀,静态变量应增加“s_”

    首先,全局变量十分危险,通过前缀使得全局变量更加醒目,促使开发人员对这些变量的使用更加小心。

    其次,从根本上说,应当尽量不使用全局变量,增加g_和s_前缀,会使得全局变量的名字显得很丑陋,从而促使开发人员尽量少使用全局变量。

    11、禁止使用单字节命名变量,但 允许 定义i 、j、k作为局部循环变量

    12、 不建议使用匈牙利命名法

    匈牙利命名法是一种编程时的命名规范。基本原则是:变量名=属性+类型+对象描述。匈牙利命名法源于微软,然而却被很多人以讹传讹的使用。而现在即使是微软也不再推荐使用匈牙利命名法。历来对匈牙利命名法的一大诟病,就是导致了变量名难以阅读,这和本规范的指导思想也有冲突,所以本规范特意强调,变量命名不应采用匈牙利命名法,而应该想法使变量名为一个有意义的词或词组,方便代码的阅读。

    变量命名需要说明的是变量的含义,而不是变量的类型。在变量命名前增加类型说明,反而降低了变量的可读性;更麻烦的问题是,如果修改了变量的类型定义,那么所有使用该变量的地方都需要修改。

    13、使用名词或者形容词+名词方式命名变量 

    14、函数命名应以函数要执行的动作命名,一般采用动词或者动词+名词的结构

    正确示例:找到当前进程的当前目录:

    DWORD GetCurrentDirectory( DWORD BufferLength, LPTSTR Buffer );

    15、函数指针除了前缀,其他按照函数的命名规则命名

    16、对于数值或者字符串等等常量的定义,建议采用全大写字母,单词之间加下划线“_”的方式命名(枚举同样建议使用此方式定义)

    正确示例:

    #define PI_ROUNDED 3.14

    17、除了头文件或编译开关等特殊标识定义,宏定义不能使用下划线“_”开头和结尾

    一般来说,‟_‟开头、结尾的宏都是一些内部的定义,ISO/IEC 9899(俗称C99)中有如下的描述(6.10.8 Predefined macro names):

    None of these macro names (这里上面是一些内部定义的宏的描述),nor the identifier defined,shall be the subject of a #define or a #undef preprocessing directive.Any other predefined macro names shall begin with a leading underscore fol lowedby an uppercase letter ora second underscore.


    4、变量

    1、一个变量只有一个功能,不能把一个变量用作多种用途

    一个变量只用来表示一个特定功能,不能把一个变量作多种用途,即同一变量取值不同时,其代表的意义也不同。

    错误示例:具有两种功能的反例

    WORD DelRelTimeQue( void )
    {
        WORD Locate;
        Locate = 3; 
        Locate = DeleteFromQue(Locate); /* Locate具有两种功能:位置和函数DeleteFromQue的返回值 */
        return Locate;
    }

    正确做法:使用两个变量

    WORD DelRelTimeQue( void )
    {
        WORD Ret;
        WORD Locate;
        Locate = 3;
        Ret  = DeleteFromQue(Locate);
        return Ret;
    }

    2、结构功能单一,不要设计面面俱到的数据结构 

    相关的一组信息才是构成一个结构体的基础,结构的定义应该可以明确的描述一个对象,而不是一组相关性不强的数据的集合。设计结构时应力争使结构代表一种现实事务的抽象,而不是同时代表多种。结构中的各元素应代表同一事务的不同侧面,而不应把描述没有关系或关系很弱的不同事务的元素放到同一结构中。

    错误示例:如下结构不太清晰、合理。

    typedef struct STUDENT_STRU
    {
        unsigned char name[32]; /* student's name */
        unsigned char age; /* student's age */
        unsigned char sex; /* student's sex, as follows */
        /* 0 - FEMALE; 1 - MALE */
        unsigned char teacher_name[32]; /* the student teacher's name */
        unsigned char teacher_sex; /* his teacher sex */
    } STUDENT;

    正确示例:若改为如下,会更合理些。

    typedef struct TEACHER_STRU
    {
        unsigned char name[32]; /* teacher name */
        unsigned char sex; /* teacher sex, as follows */
        /* 0 - FEMALE; 1 - MALE */
        unsigned int teacher_ind; /* teacher index */
    } TEACHER;
    
    typedef struct STUDENT_STRU
    {
        unsigned char name[32]; /* student's name */
        unsigned char age; /* student's age */
        unsigned char sex; /* student's sex, as follows */
        /* 0 - FEMALE; 1 - MALE */
        unsigned int teacher_ind; /* his teacher index */
    } STUDENT; 

    3、不用或者少用全局变量

    单个文件内部可以使用static的全局变量,可以将其理解为类的私有成员变量。

    全局变量应该是模块的私有数据,不能作用对外的接口使用,使用static类型定义,可以有效防止外部文件的非正常访问,建议定义一个STATIC宏,在调试阶段,将STATIC定义为static,版本发布时,改为空,以便于后续的打补丁等操作。

    4、防止局部变量与全局变量同名

    尽管局部变量和全局变量的作用域不同而不会发生语法错误,但容易使人误解。

    5、通讯过程中使用的结构,必须注意字节序

    通讯报文中,字节序是一个重要的问题,我司设备使用的CPU类型复杂多样,大小端、32位/64位的处理器也都有,如果结构会在报文交互过程中使用,必须考虑字节序问题。由于位域在不同字节序下,表现看起来差别更大,所以更需要注意对于这种跨平台的交互,数据成员发送前,都应该进行主机序到网络序的转换;接收时,也必须进行网络序到主机序的转换。

    6、严禁使用未经初始化的变量作为右值 

    在首次使用前初始化变量,初始化的地方离使用的地方越近越好。

    7、构造仅有一个模块或函数可以修改、创建,而其余有关模块或函数只访问的全局变量,防止多个不同模块或函数都可以修改、创建同一全局变量的现象

    降低全局变量耦合度。

    8、使用面向接口编程思想,通过 API 访问数据:如果本模块的数据需要对外部模块开放 ,应提供接口函数来设置、获取,同时注意全局数据的访问互斥

    避免直接暴露内部数据给外部模型使用,是防止模块间耦合最简单有效的方法。定义的接口应该有比较明确的意义,比如一个风扇管理功能模块,有自动和手动工作模式,那么设置、查询工作模块就可以定义接口为SetFanWorkMode,GetFanWorkMode;查询转速就可以定义为GetFanSpeed;风扇支持节能功能开关,可以定义EnabletFanSavePower等。

    9、明确全局变量的初始化顺序,避免跨模块的初始化依赖 

    系统启动阶段,使用全局变量前,要考虑到该全局变量在什么时候初始化,使用全局变量和初始化全局变量,两者之间的时序关系,谁先谁后,一定要分析清楚,不然后果往往是低级而又灾难性的。

    10、尽量减少没有必要的数据类型默认转换与强制转换

    当进行数据类型强制转换时,其数据的意义、转换后的取值等都有可能发生变化,而这些细节若考虑不周,就很有可能留下隐患。

    错误示例:如下赋值,多数编译器不产生告警,但值的含义还是稍有变化。

    char ch;
    unsigned short int exam;
    ch = -1;
    exam = ch; // 编译器不产生告警,此时exam为0xFFFF。

    5、宏、常量

    1、用宏定义表达式时,要使用完备的括号 

    因为宏只是简单的代码替换,不会像函数一样先将参数计算后,再传递。

    错误示例:如下定义的宏都存在一定的风险

    #define RECTANGLE_AREA(a, b) a * b
    #define RECTANGLE_AREA(a, b) (a * b)
    #define RECTANGLE_AREA(a, b) (a) * (b)

    正确示例:

    #define RECTANGLE_AREA(a, b) ((a) * (b))

    这是因为:如果定义 #define RECTANGLE_AREA(a, b) a * b  或 #define RECTANGLE_AREA(a, b) (a * b)则 c/RECTANGLE_AREA(a, b)  将扩展成 c/a * b , c  与 b 本应该是除法运算,结果变成了乘法运算,造成错误。

    如果定义 #define RECTANGLE_AREA(a, b) (a * b)则 RECTANGLE_AREA(c + d, e + f) 将扩展成: (c + d * e + f), d 与 e 先运算,造成错误。 

    2、将宏所定义的多条表达式放在大括号中

    3、使用宏时,不允许参数发生变化

    错误示例:

    #define SQUARE(a) ((a) * (a))
    int a = 5;
    int b;
    b = SQUARE(a++); // 结果:a = 7,即执行了两次增。

    正确示例:

    b = SQUARE(a);
    a++; // 结果:a = 6,即只执行了一次增。

    同时也建议即使函数调用,也不要在参数中做变量变化操作,因为可能引用的接口函数,在某个版本升级后,变成了一个兼容老版本所做的一个宏,结果可能不可预知。

    4、不允许直接使用魔鬼数字 

    使用魔鬼数字的弊端:代码难以理解;如果一个有含义的数字多处使用,一旦需要修改这个数值,代价惨重。

    使用明确的物理状态或物理意义的名称能增加信息,并能提供单一的维护点。

    解决途径:对于局部使用的唯一含义的魔鬼数字,可以在代码周围增加说明注释,也可以定义局部const变量,变量命名自注释。对于广泛使用的数字,必须定义const全局变量/宏;同样变量/宏命名应是自注释的。0作为一个特殊的数字,作为一般默认值使用没有歧义时,不用特别定义。

    5、除非必要,应尽可能使用函数代替宏

    宏对比函数,有一些明显的缺点:

    • 宏缺乏类型检查,不如函数调用检查严格;
    • 宏展开可能会产生意想不到的副作用,如#define SQUARE(a) (a) * (a)这样的定义,如果是SQUARE(i++),就会导致i被加两次;如果是函数调用double square(double a) {return a * a;}则不会有此副作用;
    • 以宏形式写的代码难以调试难以打断点,不利于定位问题;
    • 宏如果调用的很多,会造成代码空间的浪费,不如函数空间效率高。

    错误示例:下面的代码无法得到想要的结果:

    #define MAX_MACRO(a, b) ((a) > (b) ? (a) : (b))
    
    int MAX_FUNC(int a, int b) {
        return ((a) > (b) ? (a) : (b));
    }
    
    int testFunc()
    {
        unsigned int a = 1;
        int b = -1;
        printf("MACRO: max of a and b is: %d\n", MAX_MACRO(++a, b));
        printf("FUNC : max of a and b is: %d\n", MAX_FUNC(a, b));
        return 0;
    }

    上面宏代码调用中,由于宏缺乏类型检查,a和b的比较变成无符号数的比较,结果是a < b,所以a只加了一次,所以最终的输出结果是:

    MACRO: max of a and b is: -1
    FUNC : max of a and b is: 2

    6、常量建议使用 const 定义代替宏

    “尽量用编译器而不用预处理”,因为#define经常被认为好象不是语言本身的一部分。看下面的语句:

    #define ASPECT_RATIO 1.653

    编译器会永远也看不到ASPECT_RATIO这个符号名,因为在源码进入编译器之前,它会被预处理程序去掉,于是ASPECT_RATIO不会加入到符号列表中。如果涉及到这个常量的代码在编译时报错,就会很令人费解,因为报错信息指的是1.653,而不是ASPECT_RATIO。如果ASPECT_RATIO不是在你自己写的头文件中定义的,你就会奇怪1.653是从哪里来的,甚至会花时间跟踪下去。这个问题也会出现在符号调试器中,因为同样地,你所写的符号名不会出现在符号列表中。
    解决这个问题的方案很简单:不用预处理宏,定义一个常量:

    const double ASPECT_RATIO = 1.653;

    这种方法很有效,但有两个特殊情况要注意。首先,定义指针常量时会有点不同。因为常量定义一般是放在头文件中(许多源文件会包含它),除了指针所指的类型要定义成const外,重要的是指针也经常要定义成const。例如,要在头文件中定义一个基于char*的字符串常量,你要写两次const:

    const char * const authorName = "Scott Meyers";

    延伸阅读材料:关于const和指针的使用,这里摘录两段ISO/IEC 9899(俗称C99)的描述: 

    7、宏定义中尽量不使用 return 、 goto 、 continue 、 break等改变程序流程的语句

    如果在宏定义中使用这些改变流程的语句,很容易引起资源泄漏问题,使用者很难自己察觉。

    错误示例:在某头文件中定义宏CHECK_AND_RETURN:

    #define CHECK_AND_RETURN(cond, ret) {if (cond == NULL_PTR) {return ret;}}
    //然后在某函数中使用(只说明问题,代码并不完整):
    pMem1 = VOS_MemAlloc(...);
    CHECK_AND_RETURN(pMem1 , ERR_CODE_XXX)
    pMem2 = VOS_MemAlloc(...);
    CHECK_AND_RETURN(pMem2 , ERR_CODE_XXX) /*此时如果pMem2==NULL_PTR,则pMem1未释放函数就返回了,造成内存泄漏。*/

    所以说,类似于CHECK_AND_RETURN这些宏,虽然能使代码简洁,但是隐患很大,使用须谨慎。 


    6、表达式

    1、表达式的值在标准所允许的任何运算次序下都应该是相同的

    2、函数调用不要作为另一个函数的参数使用,否则对于代码的调试、阅读都不利

    错误示例:如下代码不合理,仅用于说明当函数作为参数时,由于参数压栈次数不是代码可以控制的,可能造成未知的输出:

    int g_var;
    
    int fun1()
    {
        g_var += 10;
        return g_var;
    }
    
    int fun2()
    {
        g_var += 100;
        return g_var;
    }
    
    int main(int argc, char *argv[], char *envp[])
    {
        g_var = 1;
        printf("func1: %d, func2: %d\n", fun1(), fun2());
        g_var = 1;
        printf("func2: %d, func1: %d\n", fun2(), fun1());
    }

    上面的代码,使用断点调试起来也比较麻烦,阅读起来也不舒服,所以不要为了节约代码行,而写这种代码。

    3、赋值语句不要写在 if 等语句中,或者作为函数的参数使用

    因为if语句中,会根据条件依次判断,如果前一个条件已经可以判定整个条件,则后续条件语句不会再运行,所以可能导致期望的部分赋值没有得到运行。

    错误示例:

    int main(int argc, char *argv[], char *envp[])
    {
        int a = 0;
        int b;
        if ((a == 0) || ((b = fun1()) > 10))
        {
            printf("a: %d\n", a);
        }
        printf("b: %d\n", b);
    }

    作用函数参数来使用,参数的压栈顺序不同可能导致结果未知。

    4、用括号明确表达式的操作顺序,避免过分依赖默认优先级

    使用括号强调所使用的操作符,防止因默认的优先级与设计思想不符而导致程序出错;同时使得代码更为清晰可读,然而过多的括号会分散代码使其降低了可读性。

    5、赋值操作符不能使用在产生布尔值的表达式上 

    示例:

    x = y;
    if (x != 0)
    {
        foo ();
    }

    不能写成:

    if (( x = y ) != 0)
    {
        foo ();
    }

    或者更坏的:

    if (x = y)
    {
        foo ();
    }

    7、注释

     1、优秀的代码可 以自我解释,不通过注释即可轻易读懂

    优秀的代码不写注释也可轻易读懂,注释无法把糟糕的代码变好,需要很多注释来解释的代码往往存在坏味道,需要重构。

    错误示例:注释不能消除代码的坏味道:

    /* 判断m是否为素数*/
    /* 返回值:: 是素数,: 不是素数*/
    int p(int m)
    {
        int k = sqrt(m);
        for (int i = 2; i <= k; i++)
            if (m % i == 0)
                break; /* 发现整除,表示m不为素数,结束遍历*/
        /* 遍历中没有发现整除的情况,返回*/
        if (i > k)
            return 1;
        /* 遍历中没有发现整除的情况,返回*/
        else
            return 0;
    }

    重构代码后,不需要注释:

    int IsPrimeNumber(int num)
    {
        int sqrt_of_num = sqrt (num);
        for (int i = 2; i <= sqrt_of_num; i++)
        {
            if (num % i == 0)
            {
                return FALSE;
            }
        }
        return TRUE;
    }

    2、注释的内容要清楚、明了,含义准确,防止注释二义性

    有歧义的注释反而会导致维护者更难看懂代码,正如带两块表反而不知道准确时间。

    3、在代码的功能、意图层次上进行注释,即注释解释 代码难以直接表达的意图 , 而不是重复描述代码

    注释的目的是解释代码的目的、功能和采用的方法,提供代码以外的信息,帮助读者理解代码,防止没必要的重复注释信息。对于实现代码中巧妙的、晦涩的、有趣的、重要的地方加以注释。注释不是为了名词解释(what),而是说明用途(why)。

    4、修改代码时,维护代码周边的所有注释,以保证注释与代码的一致性,不再有用的注释要删除

    不要将无用的代码留在注释中,随时可以从源代码配置库中找回代码;即使只是想暂时排除代码,也要留个标注,不然可能会忘记处理它。

    5、文件头部应进行注释,注释必须列出:版权说明、版本号、生成日期、作者姓名、工号、内容、功能说明、与其它文件的关系、修改日志等,头文件的注释中还应有函数功能简要说明

    正确示例:下面这段头文件的头注释比较标准,当然,并不局限于此格式,但上述信息建议要包含在内。

    6、函数声明处注释描述函数功能、性能及用法,包括输入和输出参数、函数返回值、可重入的要求等;定义处详细描述函数功能和实现要点,如实现的简要步骤、实现的理由、 设计约束等

    重要的、复杂的函数,提供外部使用的接口函数应编写详细的注释。

    7、全局变量要有较详细的注释,包括对其功能、取值范围以及存取时注意事项等的说明

    正确示例:

    /* The ErrorCode when SCCP translate */
    /* Global Title failure, as follows */ /* 变量作用、含义*/
    /* 0 -SUCCESS 1 -GT Table error */
    /* 2 -GT error Others -no use */ /* 变量取值范围*/
    /* only function SCCPTranslate() in */
    /* this modual can modify it, and other */
    /* module can visit it through call */
    /* the function GetGTTransErrorCode() */ /* 使用方法*/
    BYTE g_GTTranErrorCode;

    8、注释应放在其代码上方相邻位置或右方,不可放在下面,如放于上方则需与其上面的代码用空行隔开,且与下方代码缩进相同

    正确示例:

    /* active statistic task number */
    #define MAX_ACT_TASK_NUMBER 1000
    #define MAX_ACT_TASK_NUMBER 1000 /* active statistic task number */
    可按如下形式说明枚举/数据/联合结构。
    /* sccp interface with sccp user primitive message name */
    enum SCCP_USER_PRIMITIVE
    {
        N_UNITDATA_IND, /* sccp notify sccp user unit data come */
        N_NOTICE_IND, /* sccp notify user the No.7 network can not transmission this message */
        N_UNITDATA_REQ, /* sccp user's unit data transmission request*/
    };

    9、对于 switch语句下的case语句,如果因为特殊情况需要处理完一个case后进入下一个case处理,必须在该case语句处理完、下一个case语句前加上明确的注释

    这样比较清楚程序编写者的意图,有效防止无故遗漏break语句。

    case CMD_FWD:
        ProcessFwd();
        /* now jump into c ase CMD_A */
    case CMD_A:
        ProcessA();
        break;
    //对于中间无处理的连续case,已能较清晰说明意图,不强制注释。
    switch (cmd_flag)
        {
            case CMD_A:
            case CMD_B:
        {
            ProcessCMD();
            break;
        }
        ……
    }

    10、避免在注释中使用缩写,除非是业界通用或子系统内标准化的缩写

    11、同一产品或项目组统一注释风格

    12、避免在一行代码或表达式的中间插入注释

    除非必要,不应在代码或表达中间插入注释,否则容易使代码可理解性变差

    13、注释应考虑程序易读及外观排版的因素,使用的语言若是中、英兼有的,建议多使用中文,除非能用非常流利准确的英文表达,对于有外籍员工的,由产品确定注释语言

    注释语言不统一,影响程序易读性和外观排版,出于对维护人员的考虑,建议使用中文。

    14、文件头、函数头、全局常量变量、类型定义的注释格式采用工具可识别的格式

    采用工具可识别的注释格式,例如doxygen格式,方便工具导出注释形成帮助文档。以doxygen格式为例,文件头,函数和全部变量的注释的示例如下:


    8、排版与格式

    1、程序块采用缩进风格编写, 每级缩进为4个空格

    2、相对独立的程序块之间、变量说明之后必须加空行 

    错误示例:如下例子不符合规范。

    if (!valid_ni(ni))
    {
        // program code
        ...
    }
    repssn_ind = ssn_data[index].repssn_index;
    repssn_ni = ssn_data[index].ni;

    正确示例:

    if (!valid_ni(ni))
    {
        // program code
        ...
    }
    
    repssn_ind = ssn_data[index].repssn_index;
    repssn_ni = ssn_data[index].ni;

    3、一条语句不能过长,如不能拆分需要分行写。一行到底多少字符换行比较合适,产品可以自行确定

    对于目前大多数的PC来说,132比较合适(80/132是VTY常见的行宽值);对于新PC宽屏显示器较多的产品来说,可以设置更大的值。换行时有如下建议:

    • 换行时要增加一级缩进,使代码可读性更好;
    • 低优先级操作符处划分新行;换行时操作符应该也放下来,放在新行首;
    • 换行时建议一个完整的语句放在一行,不要根据字符数断行。

    正确示例:

    if ((temp_flag_var == TEST_FLAG)
    &&(((temp_counter_var - TEST_COUNT_BEGIN) % TEST_COUNT_MODULE) >= TEST_COUNT_THRESHOLD))
    {
        // process code
    }

    4、多个短语句(包括赋值语句)不允许写在同一行内 ,即一行只写一条语句

    错误示例:

    int a = 5; int b= 10; //不好的排版

    正确示例:

    int a = 5;
    int b= 10;

    5、if 、 for 、 do 、 while 、 case 、 switch 、 default 等语句独占一行

    执行语句必须用缩进风格写,属于if、for、do、while、case、switch、default等下一个缩进级别;

    一般写if、for、do、while等语句都会有成对出现的„{}‟,对此有如下建议可以参考:if、for、do、while等语句后的执行语句建议增加成对的“{}”;如果if/else配套语句中有一个分支有“{}”,那么另一个分支即使一行代码也建议增加“{}”;添加“{”的位置可以在if等语句后,也可以独立占下一行;独立占下一行时,可以和if在一个缩进级别,也可以在下一个缩进级别;但是如果if语句很长,或者已经有换行,建议“{”使用独占一行的写法。

    6、在两个以上的关键字、变量、常量进行对等操作时,它们之间的操作符之前、之后或者前后要加空格 ; 进行非对等操作时,如果是关系密切的立即操作符(如-> > ),后不应加空格

    采用这种松散方式编写代码的目的是使代码更加清晰。

    在已经非常清晰的语句中没有必要再留空格,如括号内侧(即左括号后面和右括号前面)不需要加空格,多重括号间不必加空格,因为在C语言中括号已经是最清晰的标志了。在长语句中,如果需要加的空格非常多,那么应该保持整体清晰,而在局部不加空格。给操作符留空格时不要连续留两个以上空格。

    正确示例:

    1、逗号、分号只在后面加空格。

    int a, b, c;

    2、比较操作符, 赋值操作符"="、 "+=",算术操作符"+"、"%",逻辑操作符"&&"、"&",位域操作符"<<"、"^"等双目操作符的前后加空格。 

    if (current_time >= MAX_TIME_VALUE)
    a = b + c;
    a *= 2;
    a = b ^ 2;

    3、"!"、"~"、"++"、"--"、"&"(地址操作符)等单目操作符前后不加空格。

    *p = 'a'; // 内容操作"*"与内容之间
    flag = !is_empty; // 非操作"!"与内容之间
    p = &mem; // 地址操作"&" 与内容之间
    i++; 

     4、"->"、"."前后不加空格。

    p->id = pid; // "->"指针前后不加空格

    5、if、for、while、switch等与后面的括号间应加空格,使if等关键字更为突出、明显。

    if (a >= b && c > d)

    7、注释符(包括/**/、//)与注释内容之间要用一个空格进行分隔

    8、源程序中关系较为紧密的代码应尽可能相邻


    9、代码编辑编译 

    1、使用编译器的最高告警级别,理解所有的告警,通过修改代码而不是降低告警级别来消除所有告警

    编译器是你的朋友,如果它发出某个告警,这经常说明你的代码中存在潜在的问题。

    2、在产品软件(项目组)中,要统一编译开关、静态检查选项以及相应告警清除策略

    如果必须禁用某个告警,应尽可能单独局部禁用,并且编写一个清晰的注释,说明为什么屏蔽。某些语句经编译/静态检查产生告警,但如果你认为它是正确的,那么应通过某种手段去掉告警信息。

    4、本地构建工具(如 PC-Lint)的配置应该和持续集成的一致

    两者一致,避免经过本地构建的代码在持续集成上构建失败

    5、 使用版本控制(配置管理)系统,及时签入通过本地构建的代码,确保签入的代码不会影响构建成功

    及时签入代码降低集成难度。

    6、要小心地使用编辑器提供的块拷贝功能编程

    以上为自我总结,感兴趣的同志,推荐阅读全文,也就60余页。

    展开全文
  • 高并发系统建设经验总结

    千次阅读 多人点赞 2021-10-30 00:20:26
    作者:listenzhang,腾讯 PCG 后台开发工程师前言早期从事运单系统的开发和维护工作,最早的日均百万单,到日均千万单,业务的快速发展再加上外卖业务的特点是,业务量集中在午高峰和...

    4497da28f96fa0ce2ed5fe6342b22a07.gif

    作者:listenzhang,腾讯 PCG 后台开发工程师

    前言

    早期从事运单系统的开发和维护工作,从最早的日均百万单,到日均千万单,业务的快速发展再加上外卖业务的特点是,业务量集中在午高峰和晚高峰两个高峰期,所以高峰期并发请求量也是水涨船高,每天都要面对高并发的挑战。拿运单系统来举例,日常午高峰核心查询服务的 QPS 在 20 万以上,Redis 集群的 QPS 更是在百万级,数据库 QPS 也在 10 万级以上,TPS 在 2 万以上。

    在这么大的流量下,主要的工作也是以围绕如何建设系统的稳定性和提升容量展开,下面主要从基础设施、数据库、架构、应用、规范这几方面谈谈如何建设高并发的系统。以下都是我个人这几年的一些经验总结,架构没有银弹,因此也称不上是最佳实践,仅供参考

    基础设施

    在分层架构中,最底层的就是基础设施。基础设置一般来说包含了物理服务器、IDC、部署方式等等。就像一个金字塔,基础设施就是金字塔的底座,只有底座稳定了,上层才能稳定。

    8040f8501182ca97e6ee4b081ebe0f3c.png

    异地多活

    多活可以分为同城多活、异地多活等等,实现方式也有多种,比如阿里使用的单元化方案,饿了么使用的是多中心的方案,关于多活的实现可以参考:饿了么多活实现分享。当时做多活的主要出发点是保证系统的高可用性,避免单 IDC 的单点故障问题,同时由于每个机房的流量都变成了总流量的 1/N,也变相提升了系统容量,在高并发的场景下可以抗更多的流量。下图是活的整体架构,来源于上面多活实现的分享文章中。

    449db48a935caaaa144b109756b6db31.png

    数据库

    数据库是整个系统最重要的组成部分之一,在高并发的场景下很大一部分工作是围绕数据库展开的,主要需要解决的问题是如何提升数据库容量。

    读写分离

    互联网的大部分业务特点是读多写少,因此使用读写分离架构可以有效降低数据库的负载,提升系统容量和稳定性。核心思路是由主库承担写流量,从库承担读流量,且在读写分离架构中一般都是 1 主多从的配置,通过多个从库来分担高并发的查询流量。比如现在有 1 万 QPS 的以及 1K 的 TPS,假设在 1 主 5 从的配置下,主库只承担 1K 的 TPS,每个从库承担 2K 的 QPS,这种量级对 DB 来说是完全可接受的,相比读写分离改造前,DB 的压力明显小了许多。

    5f8197d4908eccf60fbb5642f1d11eed.png

    这种模式的好处是简单,几乎没有代码改造成本或只有少量的代码改造成本,只需要配置数据库主从即可。缺点也是同样明显的:

    主从延迟

    MySQL 默认的主从复制是异步的,如果在主库插入数据后马上去从库查询,可能会发生查不到的情况。正常情况下主从复制会存在毫秒级的延迟,在 DB 负载较高的情况下可能存在秒级延迟甚至更久,但即使是毫秒级的延迟,对于实时性要求较高的业务来说也是不可忽视的。所以在一些关键的查询场景,我们会将查询请求绑定到主库来避免主从延迟的问题。关于主从延迟的优化网上也有不少的文章分享,这里就不再赘述。

    从库的数量是有限的

    一个主库能挂载的从库数量是很有限的,没办法做到无限的水平扩展。从库越多,虽然理论上能承受的 QPS 就越高,但是从库过多会导致主库主从复制 IO 压力更大,造成更高的延迟,从而影响业务,所以一般来说只会在主库后挂载有限的几个从库。

    无法解决 TPS 高的问题

    从库虽然能解决 QPS 高的问题,但没办法解决 TPS 高的问题,所有的写请求只有主库能处理,一旦 TPS 过高,DB 依然有宕机的风险。

    分库分表

    当读写分离不能满足业务需要时,就需要考虑使用分库分表模式了。当确定要对数据库做优化时,应该优先考虑使用读写分离的模式,只有在读写分离的模式已经没办法承受业务的流量时,我们才考虑分库分表的模式。分库分表模式的最终效果是把单库单表变成多库多表,如下图。

    1252affc5233672d599a239679836f4a.png

    首先来说下分表,分表可以分为垂直拆分和水平拆分。垂直拆分就是按业务维度拆,假设原来有张订单表有 100 个字段,可以按不同的业务纬度拆成多张表,比如用户信息一张表,支付信息一张表等等,这样每张表的字段相对来说都不会特别多。

    7ecb6b4ccc09459eda4403d229873cbd.png

    水平拆分是把一张表拆分成 N 张表,比如把 1 张订单表,拆成 512 张订单子表。

    564ced7333cbb9e6a97ca5f39a5ea845.png

    在实践中可以只做水平拆分或垂直拆分,也可以同时做水平及垂直拆分。

    5ff009d470e70e00cf342cf99ab627c2.png

    说完了分表,那分库是什么呢?分库就是把原来都在一个 DB 实例中的表,按一定的规则拆分到 N 个 DB 实例中,每个 DB 实例都会有一个 master,相当于是多 mater 的架构,同时为了保证高可用性,每个 master 至少要有 1 个 slave,来保证 master 宕机时 slave 能及时顶上,同时也能保证数据不丢失。拆分完后每个 DB 实例中只会有部分表。

    由于是多 master 的架构,分库分表除了包含读写分离模式的所有优点外,还可以解决读写分离架构中无法解决的 TPS 过高的问题,同时分库分表理论上是可以无限横向扩展的,也解决了读写分离架构下从库数量有限的问题。当然在实际的工程实践中一般需要提前预估好容量,因为数据库是有状态的,如果发现容量不足再扩容是非常麻烦的,应该尽量避免。

    在分库分表的模式下可以通过不启用查询从库的方式来避免主从延迟的问题,也就是说读写都在主库,因为在分库后,每个 master 上的流量只占总流量的 1/N,大部分情况下能扛住业务的流量,从库只作为 master 的备份,在主库宕机时执行主从切换顶替 master 提供服务使用。说完了好处,再来说说分库分表会带来的问题,主要有以下几点:

    改造成本高

    分库分表一般需要中间件的支持,常见的模式有两种:客户端模式和代理模式。客户端模式会通过在服务上引用 client 包的方式来实现分库分表的逻辑,比较有代表的是开源的 sharding JDBC。代理模式是指所有的服务不是直接连接 MySQL,而是通过连接代理,代理再连接到 MySQL 的方式,代理需要实现 MySQL 相关的协议。

    1e6bce9524c288601d621f1819084124.png

    两种模式各有优劣势,代理模式相对来说会更复杂,但是因为多了一层代理,在代理这层能做更多的事情,也比较方便升级,而且通过代理连接数据库,也能保证数据库的连接数稳定。使用客户端模式好处是相对来说实现比较简单,无中间代理,理论上性能也会更好,但是在升级的时候需要业务方改造代码,因此升级会比代理模式更困难。

    事务问题

    在业务中我们会使用事务来处理多个数据库操作,通过事务的 4 个特性——一致性、原子性、持久性、隔离性来保证业务流程的正确性。在分库分表后,会将一张表拆分成 N 张子表,这 N 张子表可能又在不同的 DB 实例中,因此虽然逻辑上看起来还是一张表,但其实已经不在一个 DB 实例中了,这就造成了无法使用事务的问题。

    最常见的就是在批量操作中,在分库分表前我们可以同时把对多个订单的操作放在一个事务中,但在分库分表后就不能这么干了,因为不同的订单可能属于不同用户,假设我们按用户来分库分表,那么不同用户的订单表位于不同的 DB 实例中,多个 DB 实例显然没办法使用一个事务来处理,这就需要借助一些其他的手段来解决这个问题。在分库分表后应该要尽量避免这种跨 DB 实例的操作,如果一定要这么使用,优先考虑使用补偿等方式保证数据最终一致性,如果一定要强一致性,常用的方案是通过分布式事务的方式。

    无法支持多维度查询

    分库分表一般只能按 1-2 个纬度来分,这个维度就是所谓的sharding key。常用的维度有用户、商户等维度,如果按用户的维度来分表,最简单的实现方式就是按用户 ID 来取模定位到在哪个分库哪个分表,这也就意味着之后所有的读写请求都必须带上用户 ID,但在实际业务中不可避免的会存在多个维度的查询,不一定所有的查询都会有用户 ID,这就需要我们对系统进行改造。

    5d6bc7555a2983fdb6fd84e8d49ae3b6.png

    为了能在分库分表后也支持多维度查询,常用的解决方案有两种,第一种是引入一张索引表,这张索引表是没有分库分表的,还是以按用户 ID 分库分表为例,索引表上记录各种维度与用户 ID 之间的映射关系,请求需要先通过其他维度查询索引表得到用户 ID,再通过用户 ID 查询分库分表后的表。这样,一来需要多一次 IO,二来索引表由于是没有分库分表的,很容易成为系统瓶颈。

    第二种方案是通过引入NoSQL的方式,比较常见的组合是ES+MySQL,或者HBase+MySQL的组合等,这种方案本质上还是通过 NoSQL 来充当第一种方案中的索引表的角色,但是相对于直接使用索引表来说,NoSQL具有更好的水平扩展性和伸缩性,只要设计得当,一般不容易成为系统的瓶颈。

    数据迁移

    分库分表一般是需要进行数据迁移的,通过数据迁移将原有的单表数据迁移到分库分表后的库表中。数据迁移的方案常见的有两种,第一种是停机迁移,顾名思义,这种方式简单粗暴,好处是能一步到位,迁移周期短,且能保证数据一致性,坏处是对业务有损,某些关键业务可能无法接受几分钟或更久的停机迁移带来的业务损失。

    另外一种方案是双写,这主要是针对新增的增量数据,存量数据可以直接进行数据同步,关于如何进行双写迁移网上已经有很多分享了,这里也就不赘述,核心思想是同时写老库和新库。双写的好处是对业务的影响小,但也更复杂,迁移周期更长,容易出现数据不一致问题,需要有完整的数据一致性保证方案支持。

    小结

    读写分离模式和分库分表模式推荐优先使用读写分离模式,只有在不满业务需求的情况才才考虑使用分库分表模式。原因是分库分表模式虽然能显著提升数据库的容量,但会增加系统复杂性,而且由于只能支持少数的几个维度读写,从某种意义上来说对业务系统也是一种限制,因此在设计分库分表方案的时候需要结合具体业务场景,更全面的考虑。

    架构

    在高并发系统建设中,架构同样也是非常重要的,这里分享缓存、消息队列、资源隔离等等模式的一些经验。

    缓存

    在高并发的系统架构中缓存是最有效的利器,可以说没有之一。缓存的最大作用是可以提升系统性能,保护后端存储不被大流量打垮,增加系统的伸缩性。缓存的概念最早来源于 CPU 中,为了提高 CPU 的处理速度,引入了 L1、L2、L3 三级高速缓存来加速访问,现在系统中使用的缓存也是借鉴了 CPU 中缓存的做法。

    缓存是个非常大的话题,可以单独写一本书也毫不夸张,在这里总结一下我个人在运单系统设计和实现缓存的时候遇到的一些问题和解决方案。缓存主要分为本地缓存和分布式缓存,本地缓存如Guava CacheEHCache等,分布式缓存如RedisMemcached等,在运单系统中使用的主要以分布式缓存为主。

    如何保证缓存与数据库的数据一致性

    首先是如何保证缓存与数据库的数据一致性问题,基本在使用缓存的时候都会遇到这个问题,同时这也是个高频的面试题。在我负责的运单系统中使用缓存这个问题就更突出了,首先运单是会频繁更新的,并且运单系统对数据一致性的要求是非常高的,基本不太能接受数据不一致,所以不能简单的通过设置一个过期时间的方式来失效缓存。

    关于缓存读写的模式推荐阅读耗子叔的文章:缓存更新的套路,里面总结了几种常用的读写缓存的套路,我在运单系统中的缓存读写模式也是参考了文章中的Write through模式,通过伪代码的方式大概是这样的:

    lock(运单ID) {
     //...
     
        // 删除缓存
       deleteCache();
        // 更新DB
       updateDB();
        // 重建缓存
       reloadCache()
    }

    既然是Write through模式,那对缓存的更新就是在写请求中进行的。首先为了防止并发问题,写请求都需要加分布式锁,锁的粒度是以运单 ID 为 key,在执行完业务逻辑后,先删除缓存,再更新 DB,最后再重建缓存,这些操作都是同步进行的,在读请求中先查询缓存,如果缓存命中则直接返回,如果缓存不命中则查询 DB,然后直接返回,也就是说在读请求中不会操作缓存,这种方式把缓存操作都收敛在写请求中,且写请求是加锁的,有效防止了读写并发导致的写入脏缓存数据的问题。

    缓存数据结构的设计

    缓存要避免大 key 和热 key 的问题。举个例子,如果使用redis中的hash数据结构,那就比普通字符串类型的 key 更容易有大 key 和热 key 问题,所以如果不是非要使用hash的某些特定操作,可以考虑把hash拆散成一个一个单独的 key/value 对,使用普通的string类型的 key 存储,这样可以防止hash元素过多造成的大 key 问题,同时也可以避免单hash key过热的问题。

    读写性能

    关于读写性能主要有两点需要考虑,首先是写性能,影响写性能的主要因素是 key/value 的数据大小,比较简单的场景可以使用JSON的序列化方式存储,但是在高并发场景下使用 JSON 不能很好的满足性能要求,而且也比较占存储空间,比较常见的替代方案有protobufthrift等等,关于这些序列化/反序列化方案网上也有一些性能对比,参考thrift-protobuf-compare - Benchmarking.wiki

    e6ae4f75ec50443415b8420809305e41.png

    读性能的主要影响因素是每次读取的数据包的大小。在实践中推荐使用redis pipeline+批量操作的方式,比如说如果是字符串类型的 key,那就是pipeline+mget的方式,假设一次mget10 个 key,100 个mget为一批 pipeline,那一次网络 IO 就可以查询 1000 个缓存 key,当然这里具体一批的数量要看缓存 key 的数据包大小,没有统一的值。

    适当冗余

    适当冗余的意思是说我们在设计对外的业务查询接口的时候,可以适当的做一些冗余。这个经验是来自于当时我们在设计运单系统对外查询接口的时候,为了追求通用性,将接口的返回值设计成一个大对象,把运单上的所有字段都放在了这个大对象里面直接对外暴露了,这样的好处是不需要针对不同的查询方开发不同的接口了,反正字段就在接口里了,要什么就自己取。

    这么做一开始是没问题的,但到我们需要对查询接口增加缓存的时候发现,由于所有业务方都通过这一个接口查询运单数据,我们没办法知道他们的业务场景,也就不知道他们对接口数据一致性的要求是怎么样的,比如能否接受短暂的数据一致性,而且我们也不知道他们具体使用了接口中的哪些字段,接口中有些字段是不会变的,有些字段是会频繁变更的,针对不同的更新频率其实可以采用不同的缓存设计方案,但很可惜,因为我们设计接口的时候过于追求通用性,在做缓存优化的时候就非常麻烦,只能按最坏的情况打算,也就是所有业务方都对数据一致性要求很高来设计方案,导致最后的方案在数据一致性这块花了大量的精力。

    如果我们一开始设计对外查询接口的时候能做一些适当的冗余,区分不同的业务场景,虽然这样势必会造成有些接口的功能是类似的,但在加缓存的时候就能有的放矢,针对不同的业务场景设计不同的方案,比如关键的流程要注重数据一种的保证,而非关键场景则允许数据短暂的不一致来降低缓存实现的成本。同时在接口中最好也能将会更新的字段和不会更新的字段做一定的区分,这样在设计缓存方案的时候,针对不会更新的字段,可以设置一个较长的过期时间,而会更新的字段,则只能设置较短的过期时间,并且需要做好缓存更新的方案设计来保证数据一致性。

    消息队列

    在高并发系统的架构中,消息队列(MQ)是必不可少的,当大流量来临时,我们通过消息队列的异步处理和削峰填谷的特性来增加系统的伸缩性,防止大流量打垮系统,此外,使用消息队列还能使系统间达到充分解耦的目的。

    5052b688668b31578feab8c5f0fc4221.png

    消息队列的核心模型由生产者(Producer)、消费者(Consumer)和消息中间件(Broker)组成。目前业界常用的开源解决方案有ActiveMQRabbitMQKafkaRocketMQ和近年比较火的Pulsar,关于各种消息中间件的对比可以参考文章:消息队列背后的设计思想

    使用消息队列后,可以将原本同步处理的请求,改为通过消费 MQ 消息异步消费,这样可以减少系统处理的压力,增加系统吞吐量,关于如何使用消息队列有许多的分享的文章,这里我的经验是在考虑使用消息队列时要结合具体的业务场景来决定是否引入消息队列,因为使用消息队列后其实是增加了系统的复杂性的,原来通过一个同步请求就能搞定的事情,需要引入额外的依赖,并且消费消息是异步的,异步天生要比同步更复杂,还需要额外考虑消息乱序、延迟、丢失等问题,如何解决这些问题又是一个很大话题,天下没有免费的午餐,做任何架构设计是一个取舍的过程,需要仔细考虑得失后再做决定。

    服务治理

    服务治理是个很大的话题,可以单独拿出来说,在这里我也把它归到架构中。服务治理的定义是

    一般指独立于业务逻辑之外,给系统提供一些可靠运行的系统保障措施。

    常见的保障措施包括服务的注册发现、可观测性(监控)、限流、超时、熔断等等,在微服务架构中一般通过服务治理框架来完成服务治理,开源的解决方案包括Spring CloudDubbo等。

    在高并发的系统中,服务治理是非常重要的一块内容,相比于缓存、数据库这些大块的内容,服务治理更多的是细节,比如对接口的超时设置到底是 1 秒还是 3 秒,怎么样做监控等等,有句话叫细节决定成败,有时候就是因为一个接口的超时设置不合理而导致大面积故障的事情,我曾经也是见识过的,特别是在高并发的系统中,一定要注意这些细节。

    117b73aae77795e9d90c61ee93579eaf.png
    超时

    对于超时的原则是:一切皆有超时。不管是 RPC 调用、Redis 操作、消费消息/发送消息、DB 操作等等,都要有超时。之前就遇到过依赖了外部组件,但是没有设置合理的超时,当外部依赖出现故障时,把服务所有的线程全部阻塞导致资源耗尽,无法响应外部请求,从而引发故障,这些都是“血”的教训。

    除了要设置超时,还要设置合理的超时也同样重要,像上面提到的故障即使设置了超时,但是超时太久的话依然会因为外部依赖故障而把服务拖垮。如何设置一个合理的超时是很有讲究的,可以从是否关键业务场景、是否强依赖等方面去考虑,没有什么通用的规则,需要结合具体的业务场景来看。比如在一些 C 端展示接口中,设置 1 秒的超时似乎没什么问题,但在一些对性能非常敏感的场景下 1 秒可能就太久了,总之,需要结合具体的业务场景去设置,但无论怎么样,原则还是那句话:一切皆有超时。

    监控

    监控就是系统的眼睛,没有监控的系统就像一个黑盒,从外部完全不知道里面的运行情况,我们就无法管理和运维这个系统。所以,监控系统是非常重要的。系统的可观测性主要包含三个部分——loggingtracingmetrics。主要是使用的自研的监控系统,不得不说真的是非常的好用,具体的介绍可以参考:饿了么 EMonitor 演进史

    熔断

    在微服务框架中一般都会内置熔断的特性,熔断的目的是为了在下游服务出故障时保护自身服务。熔断的实现一般会有一个断路器(Crit Breaker),断路器会根据接口成功率/次数等规则来判断是否触发熔断,断路器会控制熔断的状态在关闭-打开-半打开中流转。熔断的恢复会通过时间窗口的机制,先经历半打开状态,如果成功率达到阈值则关闭熔断状态。

    6bd8881c30362a7c9865c0cd88b38503.png

    如果没有什么特殊需求的话在业务系统中一般是不需要针对熔断做什么的,框架会自动打开和关闭熔断开关。可能需要注意的点是要避免无效的熔断,什么是无效的熔断呢?在以前碰到过一个故障,是服务的提供方在一些正常的业务校验中抛出了不合理的异常(比如系统异常),导致接口熔断影响正常业务。所以我们在接口中抛出异常或者返回异常码的时候一定要区分业务和系统异常,一般来说业务异常是不需要熔断的,如果是业务异常而抛出了系统异常,会导致被熔断,正常的业务流程就会受到影响。

    降级

    降级不是一种具体的技术,更像是一种架构设计的方法论,是一种丢卒保帅的策略,核心思想就是在异常的情况下限制自身的一些能力,来保证核心功能的可用性。降级的实现方式有许多,比如通过配置、开关、限流等等方式。降级分为主动降级和被动降级。

    9c3f860452ec12fe9d85602daea15a0f.png

    在电商系统大促的时候会把一些非核心的功能暂时关闭,来保证核心功能的稳定性,或者当下游服务出现故障且短时间内无法恢复时,为了保证自身服务的稳定性而把下游服务降级,这些都是主动降级。

    被动降级指的是,比如调用了下游一个接口,但是接口超时了,这个时候为了让业务流程能继续执行下去,一般会选择在代码中catch异常,打印一条错误日志,然后继续执行业务逻辑,这种降级是被动的。

    在高并发的系统中做好降级是非常重要的。举个例子来说,当请求量很大的时候难免有超时,如果每次超时业务流程都中断了,那么会大大影响正常业务,合理的做法是我们应该仔细区分强弱依赖,对于弱依赖采用被动降级的降级方式,而对于强依赖是不能进行降级的。降级与熔断类似,也是对自身服务的保护,避免当外部依赖故障时拖垮自身服务,所以,我们要做好充分的降级预案。

    限流

    关于限流的文章和介绍网上也有许多,具体的技术实现可以参考网上文章。关于限流我个人的经验是在设置限流前一定要通过压测等方式充分做好系统容量的预估,不要拍脑袋,限流一般来说是有损用户体验的,应该作为一种兜底手段,而不是常规手段。

    资源隔离

    资源隔离有各种类型,物理层面的服务器资源、中间件资源,代码层面的线程池、连接池,这些都可以做隔离。这里介绍的资源隔离主要是应用部署层面的,比如Set化等等。上文提到的异地多活也算是 Set 化的一种。

    负责运单系统的期间也做过一些类似的资源隔离上的优化。背景是当时出遇到过一个线上故障,原因是某服务部署的服务器都在一个集群,没有按流量划分各自单独的集群,导致关键业务和非关键业务流量互相影响而导致的故障。因此,在这个故障后我也是决定对服务器做按集群隔离部署,隔离的维度主要是按业务场景区分,分为关键集群、次关键集群和非关键集群三类,这样能避免关键和非关键业务互相影响。

    030b1e82be39792f90c090d89550cd0d.png

    小结

    在架构方面,我个人也不是专业的架构师,也是一直在学习相关技术和方法论,上面介绍的很多技术和架构设计模式都是在工作中边学习边实践。如果说非要总结一点经验心得的话,我觉得是注重细节。个人认为架构不止高大上的方法论,技术细节也是同样重要的,正所谓细节决定成败,有时候忘记设置一个小小的超时,可能导致整个系统的崩溃。

    应用

    在高并发的系统中,在应用层面能做的优化也是非常多的,这部分主要分享关于补偿、幂等、异步化、预热等这几方面的优化。

    补偿

    在微服务架构下,会按各业务领域拆分不同的服务,服务与服务之前通过 RPC 请求或 MQ 消息的方式来交互,在分布式环境下必然会存在调用失败的情况,特别是在高并发的系统中,由于服务器负载更高,发生失败的概率会更大,因此补偿就更为重要。常用的补偿模式有两种:定时任务模式或者消息队列模式

    定时任务模式

    定时任务补偿的模式一般是需要配合数据库的,补偿时会起一个定时任务,定时任务执行的时候会扫描数据库中是否有需要补偿的数据,如果有则执行补偿逻辑,这种方案的好处是由于数据都持久化在数据库中了,相对来说比较稳定,不容易出问题,不足的地方是因为依赖了数据库,在数据量较大的时候,会对数据库造成一定的压力,而且定时任务是周期性执行的,因此一般补偿会有一定的延迟。

    消息队列模式

    消息队列补偿的模式一般会使用消息队列中延迟消息的特性。如果处理失败,则发送一个延迟消息,延迟 N 分钟/秒/小时后再重试,这种方案的好处是比较轻量级,除了 MQ 外没有外部依赖,实现也比较简单,相对来说也更实时,不足的地方是由于没有持久化到数据库中,有丢失数据的风险,不够稳定。因此,我个人的经验是在关键链路的补偿中使用定时任务的模式,非关键链路中的补偿可以使用消息队列的模式。除此之外,在补偿的时候还有一个特别重要的点就是幂等性设计。

    幂等

    幂等操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同,体现在业务上就是用户对于同一操作发起的一次请求或者多次请求的结果是一致的,不会因为发起多次而产生副作用。在分布式系统中发生系统错误是在所难免的,当发生错误时,会使用重试、补偿等手段来提高容错性,在高并发的系统中发生系统错误的概率就更高了,所以这时候接口幂等就非常重要了,可以防止多次请求而引起的副作用。

    幂等的实现需要通过一个唯一的业务 ID 或者 Token 来实现,一般的流程是先在 DB 或者缓存中查询唯一的业务 ID 或者 token 是否存在,且状态是否为已处理,如果是则表示是重复请求,那么我们需要幂等处理,即不做任何操作,直接返回即可。

    ffef66e6668bf80484cfa35145c0dcc2.png

    在做幂等性设计的时候需要注意的是并不是所有的场景都要做幂等,比如用户重复转账、提现等等,因为幂等会让外部系统的感知是调用成功了,并没有阻塞后续流程,但其实我们系统内部是没有做任何操作的,类似上面提到的场景,会让用户误以为操作已成功。所以说要仔细区分需要幂等的业务场景和不能幂等的业务场景,对于不能幂等的业务场景还是需要抛出业务异常或者返回特定的异常码来阻塞后续流程,防止引发业务问题。

    异步化

    上文提到的消息队列也是一种异步化,除了依赖外部中间件,在应用内我们也可以通过线程池、协程的方式做异步化。

    cad00dafa74e5717a54d33881f825c36.png

    关于线程池的实现原理,拿 Java 中线程池的模型来举例,核心是通过任务队列和复用线程的方式相配合来实现的,网上关于这些分享的文章也很多。在使用线程池或者协程等类似技术的时候,我个人的经验是有以下两点是需要特别注意的:

    关键业务场景需要配合补偿

    我们都知道,不管是线程池也好,协程也好,都是基于内存的,如果服务器意外宕机或者重启,内存中的数据是会丢失的,而且线程池在资源不足的时候也会拒绝任务,所以在一些关键的业务场景中如果使用了线程池等类似的技术,需要配合补偿一块使用,避免内存中数据丢失造成的业务影响。在我维护的运单系统中有一个关键的业务场景是入单,简单来说就是接收上游请求,在系统中生成运单,这是整个物流履约流量的入口,是特别关键的一个业务场景。

    因为生成运单的整个流程比较长,依赖外部接口有 10 几个,所以当时为了追求高性能和吞吐率,设计成了异步的模式,也就是在线程池中处理,同时为了防止数据丢失,也做了完善的补偿措施,这几年时间入单这块基本没有出过问题,并且由于采用了异步的设计,性能非常好,那我们具体是怎么做的呢。

    a6d245a057f9dfeee407f1125e6eb92c.png

    总的流程是在接收到上游的请求后,第一步是将所有的请求参数落库,这一步是非常关键的,如果这一步失败,那整个请求就失败了。在成功落库后,封装一个 Task 提交到线程池中,然后直接对上游返回成功。后续的所有处理都是在线程池中进行的,此外,还有一个定时任务会定时补偿,补偿的数据源就是在第一步中落库的数据,每一条落库的记录会有一个 flag 字段来表示处理状态,如果发现是未处理或者处理失败,则通过定时任务再触发补偿逻辑,补偿成功后再将 flag 字段更新为处理成功。

    做好监控

    在微服务中像 RPC 接口调用、MQ 消息消费,包括中间件、基础设施等的监控,这些基本都会针对性的做完善的监控,但是类似像线程池一般是没有现成监控的,需要使用方自行实现上报打点监控,这点很容易被遗漏。我们知道线程池的实现是会有内存队列的,而我们也一般会对内存队列设置一个最大值,如果超出了最大值可能会丢弃任务,这时候如果没有监控是发现不了类似的问题的,所以,使用线程池一定要做好监控。那么线程池有哪些可以监控的指标呢,按我的经验来说,一般会上报线程池的活跃线程数以及工作队列的任务个数,这两个指标我认为是最重要的,其他的指标就见仁见智了,可以结合具体业务场景来选择性上报。

    预热

    Warm Up。当系统长期处于低水位的情况下,流量突然增加时,直接把系统拉升到高水位可能瞬间把系统压垮。通过"冷启动",让通过的流量缓慢增加,在一定时间内逐渐增加到阈值上限,给冷系统一个预热的时间,避免冷系统被压垮。

    参考网上的定义,说白了,就是如果服务一直在低水位,这时候突然来一波高并发的流量,可能会一下子把系统打垮。系统的预热一般有 JVM 预热、缓存预热、DB 预热等,通过预热的方式让系统先“热”起来,为高并发流量的到来做好准备。预热实际应用的场景有很多,比如在电商的大促到来前,我们可以把一些热点的商品提前加载到缓存中,防止大流量冲击 DB,再比如 Java 服务由于 JVM 的动态类加载机制,可以在启动后对服务做一波压测,把类提前加载到内存中,同时还有可以提前触发 JIT 编译、Code cache 等等好处。

    还有一种预热的思路是利用业务的特性做一些预加载,比如我们在维护运单系统的时候做过这样一个优化,在一个正常的外卖业务流程中是用户下单后到用户交易系统生成订单,然后经历支付->商家接单->请求配送这样一个流程,所以说从用户下单到请求配送这之间有秒级到分钟级的时间差,我们可以通过感知用户下单的动作,利用这时间差来提前加载一些数据。

    7c9b8063139c5052a1c5c1b69253bf62.png

    这样在实际请求到来的时候只需要到缓存中获取即可,这对于一些比较耗时的操作提升是非常大的,之前我们利用这种方式能提升接口性能 50%以上。当然有个点需要注意的就是如果对于一些可能会变更的数据,可能就不适合预热,因为预热后数据存在缓存中,后面就不会再去请求接口了,这样会导致数据不一致,这是需要特别注意的。

    小结

    在做高并发系统设计的时候我们总是会特别关注架构、基础设施等等,这些的确非常重要,但其实在应用层面能做的优化也是非常多的,而且成本会比架构、基础设施的架构优化低很多。很多时候在应用层面做的优化需要结合具体的业务场景,利用特定的业务场景去做出合理的设计,比如缓存、异步化,我们就需要思考哪些业务场景能缓存,能异步化,哪些就是需要同步或者查询 DB,一定要结合业务才能做出更好的设计和优化。

    规范

    这是关于建设高并发系统经验分享的最后一个部分了,但我认为规范的重要性一点都不比基础设施、架构、数据库、应用低,可能还比这些都更重要。根据二八定律,在软件的整个生命周期中,我们花了 20%时间创造了系统,但要花 80%的时间来维护系统,这也让我想起来一句话,有人说代码主要是给人读的,顺便给机器运行,其实都是体现了可维护性的重要性。

    在我们使用了高大上的架构、做了各种优化之后,系统确实有了一个比较好的设计,但问题是怎么在后续的维护过程中防止架构腐化呢,这时候就需要规范了。

    规范包括代码规范、变更规范、设计规范等等,当然这里我不会介绍如何去设计这些规范,我更想说的是我们一定要重视规范,只有在有了规范之后,系统的可维护性才能有保证。根据破窗理论,通过各种规范我们尽量不让系统有第一扇破窗产生。

    总结

    说了这么多关于设计、优化的方法,最后想再分享两点。

    第一点就是有句著名的话——“过早优化是万恶之源”,个人非常认同,我做的所有这些设计和优化,都是在系统遇到实际的问题或瓶颈的时候才做的,切忌不要脱离实际场景过早优化,不然很可能做无用功甚至得不偿失。

    第二点是在设计的时候要遵循KISS 原则,也就是 Keep it simple, stupid。简单意味着维护性更高,更不容易出问题,正所谓大道至简,或许就是这个道理。

    4cea89bdc0c7327b5132927ad4c69157.png

    以上这些都是我在工作期间维护高并发系统的一些经验总结,鉴于篇幅和个人技术水平原因,可能有些部分没有介绍的特别详细和深入,算是抛砖引玉吧。如果有什么说的不对的地方也欢迎指出,同时也欢迎交流和探讨。

    欢迎在视频号参与 1024程序员节 话题活动

    d561bde7b2a4a58fe5a01462db01b6b7.png

    展开全文
  • 第一年考本校失败,第二年考清华失败,第三年调剂清华网研院学硕上岸。初试成绩:政治:58、英语一:66、数学一:126、专业课(912):94、总分:344。说实话,就凭这个成绩自己应该是没有脸去写什么经验帖的,所以...
  • sdk开发经验总结

    千次阅读 2022-04-26 22:42:54
    文章目录1. 降低接入成本1.1 接入简单1.2 文档和 demo1.3 api接口设计1. 接口保持精简,不要提供过多的接口2. 每个接口均应当提供详细的接口... 总结 博主之前做过一些 sdk 的开发,也对接过一些 sdk ,有一些 sdk 设
  • 这四年时光里,有过目标和追求,也有过遗憾和不舍,四年前刚踏入大学校门时满怀的憧憬和期待,到现在终于结束了自己长达十几年的学生身份,离开校园踏入社会。正式作为一个在互联网苟且偷生,为了生活和前途而努力...
  • 最详细的 K8S 学习笔记总结(2021最新版)

    千次阅读 多人点赞 2021-04-18 11:25:10
    kubelet: 是工作节点执行操作的 agent,负责具体的容器生命周期管理,根据数据库获取的信息来管理容器,并上报 pod 运行状态等。 kube-proxy: 是一个简单的网络访问代理,同时也是一个 Load Balancer。它负责将...
  • 一个失败的项目管理案例

    千次阅读 2020-07-05 21:56:33
    文章目录1、背景介绍2、原因分析3、解决方案4、总结 1、背景介绍 2020 年三月底开始做一个设计管理平台的项目,我被指派为这个项目的项目经理,项目成员包括产品经理(1)、后端(4)、前端(2)、UED(1)、UI...
  • 给自己创业一年整的一个总结,分为五段:**前二十年题记**,**萌芽初期**,**外包初期**,**创业初期**,**未来规划**来说。
  • 1、解决办法 找到C:\Windows\System32\drivers\etc下的 HOST 文件,找到写有git的一行,将其取消注释。...3、HOST文件的作用(来自百度经验https://jingyan.baidu.com/article/5d368d1e0541843f61c0576a.ht...
  • 毕业五年总结

    千次阅读 多人点赞 2021-08-09 02:06:29
    技术生活总结 2021.07,毕业五年了。 第五年主要有这些不同: 工作内容更有挑战,深度和广度比一年前略有提升 ???? 和小肉领证,恋爱关系进入婚姻关系,感情没有改变 ???? 看房买房,感谢父母、朋友援助 ...
  • 某Java大佬在地表最强Java企业(阿里)面试总结

    万次阅读 多人点赞 2020-08-23 19:48:06
    面试题真的是博大精深,也通过这个面试题学到了很多...Hashtable 的方法是Synchronize的,而HashMap的方法在缺省情况下是非Synchronize的。 HashMap把Hashtable的contains方法去掉了,改成containsValue和contains.
  • 搞不懂,只能收藏一下包不挂科 知识点总结 第一章: 软件工程定义: 1968年10月,Fritz Bauer 首次提出了“软件工程”的概念,并将“软件工程”定义为:为了经济地获得能够在实际机器上有效运行的可靠软件,而建立并...
  • C++求余运算符(%)示例详解

    千次阅读 2021-06-05 16:59:54
    C++求余运算符(%)示例详解介绍:%是求余运算符,也叫模除运算符,用于求余数。%要求两个操作数均为整数(或可以隐式转换成整数的类型)。标准规定:如果%左边的操作数为负数时,则模除的结果为负数或者0,如果%左边...
  • 二战浙大CS失败+上岸上科大的一些经验

    万次阅读 多人点赞 2019-03-24 19:23:57
    emmmm,总结觉得自己没考上总结个什么劲,不总结又怕自己忘了,遂,闲来无事,写一写罢。 本人本科某末流211,因头比较铁,且对于杭州很向往,遂报浙大,一战差了二十多分,不服,于是二战,二战结果差了个位分,...
  • 靠写作杀出一条“血”路——我的2020年终总结

    千次阅读 多人点赞 2021-01-08 15:42:50
    3.4 深耕写作,大有可为 技术圈子的 8 年+技术和管理经验和观察告诉我,接近80%+技术人员是不做技术积累的。 一方面:太忙,产品版本一个接一个迭代,版本间隙能响应公司项目管理需求做一次技术总结和分享都很难,...
  • 分享一篇牛人的工作总结

    千次阅读 多人点赞 2019-06-19 10:31:36
    转自http://www.ituring.com.cn/article/497377,让大家感受一下什么叫优秀的架构师,我比较喜欢在总结里大量构图(而且喜欢在excel、ppt直接绘制拼接,不受专业绘图工具的约束),喜欢了就收藏,而且还和性能监控...
  • 配置环境变量失败的坑

    千次阅读 2021-12-15 21:56:42
    大家在配置环境变量有没有发现,明明都按照步骤配置了,但是就是无效。 可能是因为你相对变量配置多了一个分号! 配置环境变量相对变量遇到的坑,不要给相对环境变量路径加上分号。...这里以jdk配置为例子
  • Java线程池例子

    万次阅读 2015-12-04 23:16:36
    最重要的还是在实践中总结经验,企业需要的是能实际解决问题的人。 下面是我写的一个例子,包括3个Java文件,分别是: ExecutorServiceFactory.java ExecutorProcessPool.java ExecutorTest.java 下面贴...
  •  之前在这篇文章的原作者博客里看到过,不过可惜是删除了创业过程的,当时觉得他总结的几条创业失败经验很值得深思(特别是创过业的或者参与过)。今天上午很幸运看到了完整的,觉得他总结的几条创业失败经验...
  • Android开发Handler的经典总结

    千次阅读 多人点赞 2018-04-18 21:10:01
    当然,可能对于一些有工作经验的工程师来说,他们也不一定能很准确地描述,我们来看下API的介绍。Handler是用来结合线程的消息队列来发送、处理“Message对象”和“Runnable对象”的工具。每一个Handler实例之后会...
  • iOS之深入解析Hash在iOS的应用

    万次阅读 2021-10-19 15:43:24
    也就是说,它通过把关键码值映射到表一个位置来访问记录,以加快查找的速度,这个映射函数叫做散列函数,存放记录的数组叫做散列表。 给定表 M,存在函数 f(key),对任意给定的关键字值 key,代入函数后若能得到...
  • 为了总结自己踩过的坑,给后面要考的同学们提供一些微薄的帮助,遂作此文。 考场经验 一. 注意,考试的运行时的黑框,是不能使用Crtl+V进行粘贴的,但这不代表考试不能进行复制粘贴,PAT考试系统里的代码...
  • 举个简单的例子:在100MBIT/s全双工以太网捕捉数据将会产生750MByties/min的数据!在此类网络拥有高速的CPU,大量的内存和足够的磁盘空间是十分有必要的。 如果Wireshark运行时内存不足将会导致异常终止。可以在...
  • python上位机开发经验总结01

    千次阅读 多人点赞 2020-10-02 23:36:31
    魔方机器人比赛使用python写了上位机,总结一些经验待以后翻看。主要分4个方面,python变量与文件的处理,threading多线程模块使用经验,tkinter使用经验,其它零零碎碎的经验 python变量与文件的处理 主要是单...
  • UiPath经验总结

    千次阅读 2020-03-30 17:48:50
    转载:UiPath经验总结 原文链接 https://www.cnblogs.com/ybyebo/archive/2018/09/20/9680149.html https://www.cnblogs.com/ybyebo/p/10086473.html ... 日志策略:将...
  • 例如会计结算,银行的一些单证岗,资金转岗等等,不要因为找工作就盲目的去面一些需要外向性格的岗位,例如销售等等,一来是你的性格确实不合适,二来是你面上的概率其实很低,如果面失败了,既打击到你的自信心,...
  • App架构经验总结

    千次阅读 2019-07-17 17:06:48
    架构因人而异,不同的架构师大多会有不同的看法;架构也因项目而异,不同的项目需求不同,相应的架构也会不同。然而,有些东西还是通用的,是...当有了一定的经验之后,你总会有一些自己的心得体会。而以下内容就...
  • 长时间的历练,成功和失败经验,都证明了这点。 小时候,学得名句“谦虚使人进步”。但,长大后,发现“谦虚”不是最让人进步的最高效手段,而“总结”是。 人在一开始的时候,都是茫然无知的,经过不断实践...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 55,399
精华内容 22,159
关键字:

从失败中总结经验的例子