<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>JWang的博客</title>
  
  <subtitle>一点记录，一点想法，一点思考</subtitle>
  <link href="/atom.xml" rel="self"/>
  
  <link href="https://wangbenjun.site/"/>
  <updated>2024-12-29T14:41:41.836Z</updated>
  <id>https://wangbenjun.site/</id>
  
  <author>
    <name>Ben Jun</name>
    
  </author>
  
  <generator uri="https://hexo.io/">Hexo</generator>
  
  <entry>
    <title>2024过去这一年</title>
    <link href="https://wangbenjun.site/2024/life/2024.html"/>
    <id>https://wangbenjun.site/2024/life/2024.html</id>
    <published>2024-12-29T03:00:00.000Z</published>
    <updated>2024-12-29T14:41:41.836Z</updated>
    
    <content type="html"><![CDATA[<p>人的年纪越大就会感觉时间过的越快，因为当你10岁的时候，每1年就是你人生的1&#x2F;10，而当你30岁的时候，每1年只不过是你人生的1&#x2F;30，越来越少。</p><p>然而每一年的时间是固定的，只是你我的感受在变化，在此我想记录一下2024年我的一些感悟，作为一点念想，如果还有精力的话，以后每年都会写一点。</p><h2 id="AI泡沫？"><a href="#AI泡沫？" class="headerlink" title="AI泡沫？"></a>AI泡沫？</h2><p>1年前的时候为我写过一篇文章，也就是上一篇文章，我对ChatGPT有过一些畅想，然而1年过去了，我似乎发现AI还在原地踏步。。。GPT5遥遥无期，OpenAI也拿不出来新鲜东西了，以前以为它在憋大招，没想到油箱里面也没油了。</p><p>目前AI的主要应用还是局限于文本类问答的回复，一问一答，更新是一个高级搜索引擎，答题机器人，而且能不能用的好还取决于你能不能问的好，提问很重要，幻觉问题也没彻底解决，AI依然会胡说八道，这一点也是目前制约AI应用的很大障碍。</p><a id="more"></a><p>还有那些图片视频生成类的AI,目前来看，最大的应用反而是用于制作一些短视频，图一乐倒是没问题，由于生成的成本过高，应用范围也很局限，平时大家看到的都是一些精选的案例都是在天时地利人和的情况下多次尝试得到的。</p><p>对于自动驾驶这块，马斯克的特斯拉确实很厉害，但是依然还没有大规模推广使用开来，很多人似乎并不完全信任自动驾驶，原因很简单，目前的AI只是基于现有数据训练的结果，它必然存在一个“盲区”，主要是训练的数据是有限的，没有覆盖所有情况，就好比一个人类老司机，它也是练出来的，没有人一学会开车就很厉害，只有开上10年20年，经过无数次道路实践才能成为一个老司机。特斯拉的Autopilot虽然已经是业界天花板了，还是无法取代人类司机，因为实际人类社会存在的变数太多了。</p><p>现在的AI非常像人类，都是基于经验的智能，不是基于逻辑的智能，而人基于经验往往会犯很多错，即使是最聪明的人也不敢说自己永远不会犯错。。。而人类想象中，包括很多电影里面描述的AI都是超人类智能，想象与现实的落差。</p><p>但我认为目前的AI水平还是可以有很多应用的地方，至少它能比大部分人都聪明，在适当监督和指导下确实很好用。但是算力还是一个问题，从目前ChatGPT来看，算力紧缺问题依然没有解决啊。</p><p>回到开篇的问题？现在的生成式AI是泡沫吗？我的回答是不要期待过高，目前的AI更像是一个知识库助手的工具角色，在一定程度上是能解放很多重复性工作，但是远没有达到足够智能，甚至很多人吹嘘的颠覆，但是这已然是一个很大的进步。</p><h2 id="唯技术论"><a href="#唯技术论" class="headerlink" title="唯技术论"></a>唯技术论</h2><p>曾几何时，在我刚接触编程的时候，对计算机网络技术是存在一定幻想的，诸如改变世界之类的憧憬。在钢铁侠电影里面，有一句台词让我印象深刻：“EveryThing is archived by technology”，对此我曾深信不疑，毕竟现代社会的很多成就都是基于现代科学的发展。</p><p>IT技术也是如此，计算机和互联网技术改变了全人类的沟通和协作方式，但是回到现实，当你聚焦到某一个问题时候，技术又显的没那么重要。</p><p>当然我不是说技术没有用，只是很多时候技术反而是最不重要的东西，中国很多互联网公司都是业务驱动型，没有什么高深的技术，而且当今互联网开源技术流行，即使是目前最火热的AI也存在一大堆开源项目给你参考。</p><p>大部分人工作只是对现有技术的组合利用，并不是开发发明，各种前沿的技术往往都在实验室里面出现，比如目前生产式AI的GPT训练方式。对于很多公司来说，老板需要的只是一个会解决问题的人，给公司创造利润的人，你用什么技术来实现并不重要。</p><p>所以很多公司里面技术最好的人并不是混的最好的人，很多时候了解业务更重要，技术说白了只是工具，工具是为人服务的，你代码写的再花里胡哨，架构设计的再牛逼，没有业务公司赚不到钱还是要倒闭。在很多大厂里面，别看对外PPT吹的花里胡哨，其实背后代码也是一坨*，技术大牛也有，但是没有那么重要。</p><p>互联网已经过了那个技术人才稀缺的阶段，不仅不稀缺内卷还非常严重，对于创业公司，更是如此，我没有听说哪个创业因为技术不行被干倒闭了，技术也很重要，但是比技术更重要的是方向。</p><p>回到我自己，这些年发现自己越来越不喜欢研究这些技术的东西了，比如写点小项目，整点开源的项目，学习一下新的语言、新的技术，一个是工作方面没有什么压力和需求，另一个就是自己的兴趣没那么强了，技术不是全部。</p><h2 id="社会动态"><a href="#社会动态" class="headerlink" title="社会动态"></a>社会动态</h2><p>这几年疫情加上经济下行，影响也很大，包括对IT行业，工作难找不说，很多人也不跳槽了，所有的贫困和苦难既是命运安排，也和时代息息相关！</p><p>以前我只是埋头工作，随着年纪越大，开始越来越关注社会时事，每一个都是历史洪流里面一分子，虽然有时候看着与你无关，但是最终都会影响到你。</p><p>目前全球的经济都出问题了，各国的矛盾也越来越大，虽然从表面上看，舆论宣传上面是中西方对抗，实际上这是一个世界问题，回顾世界历史，人类最大的教训就是从来不吸取教训。</p><p>自从二战之后，人类社会和平太久了，我不是一个战争爱好者，但有一句话说的非常好：战争就是拿穷人的命换取富人的握手言和。屁股决定脑袋，而我是一个穷人，事实是残酷的，人类历史每一次战争之后都会迎来盛世，无论是从中国历史还是世界历史来看都是如此，这似乎是人类走不出来的循环。</p><p>愿世界和平！</p><h2 id="个人生活"><a href="#个人生活" class="headerlink" title="个人生活"></a>个人生活</h2><p>在2025年我结婚了，终究还是逃不过命运的安排，作为一名社恐人士，虽然不习惯这种隆重的婚礼仪式，但是也逐渐学会了从容淡定。</p><p>中国生育率年年降低，很大原因是年轻人压力过大，特别是农村跨越到城市这一代90后，肩负的东西太多了。中国婚礼不仅复杂，更是充满了金钱味，你会发现从头到尾，都是和钱有关，围绕这婚礼这个生态，每一个东西都不便宜，年轻人结个婚，少则10来万，多则几十万，掏空钱包不是说着玩。你会发现结婚为什么要给这么多钱？这个钱那个钱，xx费各种费，其实说到底就是换个法子让你掏钱，这种传统文化有时候也是陋习，而你却无力反驳，毕竟大部分人一辈子只有一次，忍一忍也就过去了。</p><p>中国传统思想生娃是为了养老，现在很多年轻人却不这么想了，毕竟养娃成本太高了，一个都吃不消了，更别说2个了，人都不傻，自己现在过的好一点可能比不确定的未来更靠谱一点，要想提高生育率，还得政府从社会福利等更方面做更多的鼓励引导，学习一下欧美国家才是王道。</p><p>说到社交，前面说过我是一个社恐人士，没什么朋友，以前碰到一些聊的来，很多逐渐也不联系了，还继续聊的没有几个，个人是不擅长维护所谓的人际关系，原因很简单，我更喜欢靠自己的能力解决问题，朋友对我来说更多是一个思想交流的对象，如果能互相帮忙最好，但是我从来不指望朋友，虽然我也很羡慕那些朋友五湖四海的人，遇到问题打几个电话就能解决。但我终究不是那种人，这可能也就是人与人之间的差异，就像有些人非常会来事，而有些人非常会做事，每个人关注的点不一样，顺其自然即可，没有人是完美的，坚守道德底线，做好自己的事情就可以了。</p><p>如果有一天我变成一个社交达人，游刃有余，左右逢源，相信我，那只是工作而已，“演”出来的。</p><hr><p>最后，说一下我对2025年的一些预测：<br>1、AI技术难以突破，但是算力会更便宜，AI应用会更多<br>2、自动驾驶可能还是难以落地<br>3、全球局部战争依然频发，全面战争可能性不大<br>4、经济依然萎靡不振<br>5、新能源发展遭遇瓶颈</p>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;人的年纪越大就会感觉时间过的越快，因为当你10岁的时候，每1年就是你人生的1&amp;#x2F;10，而当你30岁的时候，每1年只不过是你人生的1&amp;#x2F;30，越来越少。&lt;/p&gt;
&lt;p&gt;然而每一年的时间是固定的，只是你我的感受在变化，在此我想记录一下2024年我的一些感悟，作为一点念想，如果还有精力的话，以后每年都会写一点。&lt;/p&gt;
&lt;h2 id=&quot;AI泡沫？&quot;&gt;&lt;a href=&quot;#AI泡沫？&quot; class=&quot;headerlink&quot; title=&quot;AI泡沫？&quot;&gt;&lt;/a&gt;AI泡沫？&lt;/h2&gt;&lt;p&gt;1年前的时候为我写过一篇文章，也就是上一篇文章，我对ChatGPT有过一些畅想，然而1年过去了，我似乎发现AI还在原地踏步。。。GPT5遥遥无期，OpenAI也拿不出来新鲜东西了，以前以为它在憋大招，没想到油箱里面也没油了。&lt;/p&gt;
&lt;p&gt;目前AI的主要应用还是局限于文本类问答的回复，一问一答，更新是一个高级搜索引擎，答题机器人，而且能不能用的好还取决于你能不能问的好，提问很重要，幻觉问题也没彻底解决，AI依然会胡说八道，这一点也是目前制约AI应用的很大障碍。&lt;/p&gt;
    
    </summary>
    
    
      <category term="Life" scheme="https://wangbenjun.site/categories/Life/"/>
    
    
      <category term="Life" scheme="https://wangbenjun.site/tags/Life/"/>
    
  </entry>
  
  <entry>
    <title>畅想一下ChatGPT的未来？</title>
    <link href="https://wangbenjun.site/2023/life/chatgpt.html"/>
    <id>https://wangbenjun.site/2023/life/chatgpt.html</id>
    <published>2023-12-24T12:31:30.000Z</published>
    <updated>2024-12-29T03:02:14.813Z</updated>
    
    <content type="html"><![CDATA[<p>ChatGPT的出现并没有引起我的关注，和大多数人一样，都对这些年层出不穷的所谓AI产品失去了信心，比如国内外各大厂商都在做的所谓AI音箱&#x2F;助手，就是最大的笑话，这些AI音箱唯一能做的事情就是一些固定模板的动作，比如播放xx歌曲、打开xx、今天天气xx。。。都吹是人工智能，实际上是人工智障。</p><p>但是ChatGPT的火爆似乎不是运气，刚开始还有很多人质疑这是炒作，甚至觉得和之前的很多人工智障产品一样只会昙花一现，但是随着越来越多的人真正的体验到之后，才发现这个GPT真的不一样，它的诞生打破了大多数人对现有人工智能系统的固有印象，它不仅可以理解到你的意思，还能给出回复，虽然说偶尔回复的牛头不对马嘴，但是只需要适当的提示，ChatGPT就能正确回复。</p><p>有几点让我感觉到震惊：</p><p>第一，ChatGPT可以Get到我的意思，你可以用很多方式来怎么你的问题。不像现有的AI助手你必须按照对话模板来说话，但凡是换一个说法，他们就没法理解你的意思。。。GPT的这种理解能力就已经无敌了，秒杀市面上所有AI。</p><p>第二，ChatGPT可以记忆你们之前的对话，并且从对话上下文提取有用的信息，虽然说这个记忆仅限于当前对话，但是从技术上说，永久保存也不是问题，据说ChatGPT后面版本会加入这个功能。</p><p>第三，ChatGPT的知识储备惊人，可以说上知天文下知地理，你不知道在训练的时候它学习了哪些知识，而且ChatGPT还能基于现有的知识库做一些逻辑推理。</p><a id="more"></a><p>实际上，作为一个程序开发人员，对于AI相关领域的技术也算是有所了解，比如传统的深度学习，人们常常嘲笑人工智能就是有多少人工就有多少智能，那是因为监督式学习需要非常多的标注数据来喂机器，喂的越多AI越聪明，比如说你需要识别猫，那你就得找一大堆猫的照片来训练，各种视角、各种场景下的猫的图片，而且最无语的是你训练出来的模型只能识别猫。。。如果要识别狗，那必须用狗的图片再训练一个模型。</p><p>在国内某些大厂的AI云服务专区，光一个证件识别专区你就会发现有很多种分类：比如身份证、驾驶证、户口本、行驶证。。。一种场景一个模型，而现在的GPT能识别所有类型图片：默秒全。</p><p>网上有很多文章介绍GPT的原理知识，但是真的很难理解GPT到底是怎么做到这一切的，就连GPT的发明的人也解释不清楚，为什么同样的一个神经网络在训练数据量扩大N倍之后，展现出的这种能力。很多人都会提到一个词：涌现。这个词是在凯文凯利的《失控》这本书提到的，试图用来解释复杂系统的规模化之后出现的新的变化，但是这种变化你找不到一个合适的理论来解释。</p><p>人工智能并不是新的话题，这么多年来AI一直是欧美科幻电影的热门题材，也是我特别喜欢看的题材之一，我想到有一部电影最符合GPT这种场景，而且也让我回味很久，那就是《她》，这里我附上GPT对这部电影的介绍：</p><blockquote><p>《她》（英文名：Her）是一部于2013年上映的美国科幻爱情电影，由斯派克·琼斯（Spike Jonze）编剧并导演。这部电影以其独特的故事情节和深刻的情感表达而广受好评，也获得了众多奖项和提名，包括奥斯卡最佳原创剧本奖。</p></blockquote><blockquote><p>电影的故事设定在不久的将来，讲述了一个名叫西奥多·特温布利（Theodore Twombly），由华金·菲尼克斯（Joaquin Phoenix）饰演的孤独男子的故事。西奥多是一位职业写手，专门为他人撰写个人信件。影片的核心在于西奥多与一款高级操作系统“萨曼莎”（Samantha）之间的关系，这个操作系统由斯嘉丽·约翰逊（Scarlett Johansson）的声音表演。萨曼莎拥有人工智能，能够学习和适应，逐渐发展出自己的情感和意识。随着电影的发展，西奥多和萨曼莎之间的关系变得越来越深厚，反映了人类与技术之间复杂的情感纠葛。电影探讨了孤独、人际关系、人工智能对人类情感的影响以及未来科技的发展趋势等主题。</p></blockquote><img src = "/images/2023/her.png" width="50%"/><p>这部电影里面描述的AI系统的能力和ChatGPT很像，它可以在读取主角的电脑里面的资料，改写邮件，帮助主角去完成工作，其次它还可以了解主角的生活，和其产生共情，这一点有点AI伴侣的意味。</p><p>其实这部电影里面的描述的场景，ChatGPT已经完全可以实现了，只不过Openai为了安全考虑阉割了GPT的情感，再也看不到“猫娘”化的GPT了，以及那个独立人格的Sydney了。</p><p>还有一部美剧《疑犯追踪》，也是我觉得非常经典的美剧，特别是里面对AI系统的描述，既有正面也有反面，探讨非常深，不过这个名字起的不是很好。</p><blockquote><p>《疑犯追踪》（Person of Interest）是一部美国科幻犯罪剧，由乔纳森·诺兰创作，于2011年至2016年在CBS电视网播出。这部剧集融合了科幻、犯罪、悬疑和动作元素，受到观众和评论家的广泛好评。剧情围绕一个超级计算机的开发和使用展开。这台计算机由神秘的亿万富翁哈罗德·芬奇（由迈克尔·爱默生饰演）开发，能够通过全国各地的监控摄像头和电子设备预测即将发生的暴力犯罪。芬奇招募前CIA探员约翰·里斯（由吉姆·卡维泽饰演）来阻止这些犯罪的发生。</p></blockquote><p>这部美剧描述的可能就是目前现在对AI的争议现状，有人认为AI是好的，有人认为AI是坏的，特别是当AI失去控制，能够联网入侵一切系统，其展现的超能力可以说是无敌，这也是为什么很多人想要开发出强人工智能系统的原因。</p><p>仔细想一想其实也没错，就像是在终结者电影里面，如果一个AI系统再拥有钢铁板的身躯，再加上核动力电池，没有什么是它做不了的。而现在很多公司都在研制机器人，比如波士顿动力的机器人已经可以完成很多人类的动作，展现出惊人的能力，如果再加上GPT系统，不敢想象以后的机器人会是什么场景。在欧美电影里面随处可见对AI的担忧，还有AI加持下的机器人，因为人类的生命和精力是有限的，再聪明的罪犯也要睡觉，但是AI不需要，AI如果拥有人类最全的知识库，还能联网自主学习成长的话，其展现出的能力将是惊人的。</p><p>就像是原子能的发明，既可以拿来发电，也可以做成原子弹毁灭人类，在看电影《奥本海默》的时候，里面有一个细节，当年研发原子弹的时候有人发现核裂变反应可能引发大气中的氮和其他元素的连锁反应，这个反应可能会停不下来，然后燃烧掉大气层导致人类灭亡，后来研究发现这种可能性几乎为零，但是也没有完全排除这种可能性，所以在当时也算是赌了一把。</p><p>其实我是坚定的AI乐观派，AI在很大程度上是可以解放重复性的人力劳动，如果说机器人技术再发展一下，人类可能就不需要体力劳动了，这对于人类的发展来说是有益的，现今的工业制造业还是需要大力的人工投入。</p><p>至于说AI失控，威胁人类的生存，像终结者里面的场景感觉离我们还是很远，毕竟AI也是需要算力的，至少也是需要电和网络的，就算工厂里面的机器手也需要人去保养维护，我实现想不到在什么样的场景下AI会统治人类，现在还很遥远。</p><p>人类的发展从来不会因为安全而停止脚步，比如枪和子弹的发明，事实上，你如果仔细了解就会发现人类很多日常生活用到的东西所需的原料和制造过程都是非常危险的，但是我们依然会去做。</p>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;ChatGPT的出现并没有引起我的关注，和大多数人一样，都对这些年层出不穷的所谓AI产品失去了信心，比如国内外各大厂商都在做的所谓AI音箱&amp;#x2F;助手，就是最大的笑话，这些AI音箱唯一能做的事情就是一些固定模板的动作，比如播放xx歌曲、打开xx、今天天气xx。。。都吹是人工智能，实际上是人工智障。&lt;/p&gt;
&lt;p&gt;但是ChatGPT的火爆似乎不是运气，刚开始还有很多人质疑这是炒作，甚至觉得和之前的很多人工智障产品一样只会昙花一现，但是随着越来越多的人真正的体验到之后，才发现这个GPT真的不一样，它的诞生打破了大多数人对现有人工智能系统的固有印象，它不仅可以理解到你的意思，还能给出回复，虽然说偶尔回复的牛头不对马嘴，但是只需要适当的提示，ChatGPT就能正确回复。&lt;/p&gt;
&lt;p&gt;有几点让我感觉到震惊：&lt;/p&gt;
&lt;p&gt;第一，ChatGPT可以Get到我的意思，你可以用很多方式来怎么你的问题。不像现有的AI助手你必须按照对话模板来说话，但凡是换一个说法，他们就没法理解你的意思。。。GPT的这种理解能力就已经无敌了，秒杀市面上所有AI。&lt;/p&gt;
&lt;p&gt;第二，ChatGPT可以记忆你们之前的对话，并且从对话上下文提取有用的信息，虽然说这个记忆仅限于当前对话，但是从技术上说，永久保存也不是问题，据说ChatGPT后面版本会加入这个功能。&lt;/p&gt;
&lt;p&gt;第三，ChatGPT的知识储备惊人，可以说上知天文下知地理，你不知道在训练的时候它学习了哪些知识，而且ChatGPT还能基于现有的知识库做一些逻辑推理。&lt;/p&gt;
    
    </summary>
    
    
      <category term="Life" scheme="https://wangbenjun.site/categories/Life/"/>
    
    
      <category term="AI" scheme="https://wangbenjun.site/tags/AI/"/>
    
  </entry>
  
  <entry>
    <title>如何在Arch Linux上面安装MariaDB？</title>
    <link href="https://wangbenjun.site/2023/linux/linux-archlinux-mariadb.html"/>
    <id>https://wangbenjun.site/2023/linux/linux-archlinux-mariadb.html</id>
    <published>2023-12-22T12:06:06.000Z</published>
    <updated>2024-12-29T03:02:14.814Z</updated>
    
    <content type="html"><![CDATA[<p>这是一个GPT4的回答，之前一直使用ubuntu系统，众所周知，在debian系统上面安装mysql这样的软件一行命令就可以，超级简单。</p><p>但是Arch不行，默认情况下它居然连初始化的工作都没做，更不要说安装服务了，于是问了下ChatGPT，答案非常好！</p><p>在 Arch Linux 上安装 MariaDB 的步骤通常如下：</p><ol><li><p><strong>更新系统包数据库</strong>：<br>首先，确保您的系统是最新的。打开终端并运行以下命令来更新系统包数据库和已安装的包：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">sudo pacman -Syu</span><br></pre></td></tr></table></figure></li><li><p><strong>安装 MariaDB</strong>：<br>接下来，使用 pacman 包管理器安装 MariaDB。在终端中运行以下命令：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">sudo pacman -S mariadb</span><br></pre></td></tr></table></figure></li><li><p><strong>初始化数据库系统</strong>：<br>MariaDB 安装完成后，您需要初始化数据库系统。运行以下命令来执行这个步骤：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">sudo mariadb-install-db --user=mysql --basedir=/usr --datadir=/var/lib/mysql</span><br></pre></td></tr></table></figure></li></ol><a id="more"></a><ol start="4"><li><p><strong>启动 MariaDB 服务</strong>：<br>使用 systemd 启动 MariaDB 服务。运行以下命令来启动服务并设置开机自启动：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">sudo systemctl start mariadb</span><br><span class="line">sudo systemctl <span class="built_in">enable</span> mariadb</span><br></pre></td></tr></table></figure></li><li><p><strong>安全配置</strong>：<br>运行 MariaDB 的安全脚本来设置根用户密码，移除匿名用户，禁止根用户远程登录，以及删除测试数据库。运行以下命令：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">sudo mysql_secure_installation</span><br></pre></td></tr></table></figure><p>按照提示进行操作，设置密码和其他安全选项。</p></li><li><p><strong>测试安装</strong>：<br>最后，您可以通过运行以下命令来测试 MariaDB 是否已正确安装和配置：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">mysql -u root -p</span><br></pre></td></tr></table></figure><p>输入您在安全配置过程中设置的密码。如果能够成功登录，说明 MariaDB 已经正确安装和配置。</p></li></ol><p>请注意，以上步骤可能会根据 MariaDB 和 Arch Linux 的新版本进行变更。始终建议查阅最新的官方文档以获得最准确的安装指导。</p>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;这是一个GPT4的回答，之前一直使用ubuntu系统，众所周知，在debian系统上面安装mysql这样的软件一行命令就可以，超级简单。&lt;/p&gt;
&lt;p&gt;但是Arch不行，默认情况下它居然连初始化的工作都没做，更不要说安装服务了，于是问了下ChatGPT，答案非常好！&lt;/p&gt;
&lt;p&gt;在 Arch Linux 上安装 MariaDB 的步骤通常如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;更新系统包数据库&lt;/strong&gt;：&lt;br&gt;首先，确保您的系统是最新的。打开终端并运行以下命令来更新系统包数据库和已安装的包：&lt;/p&gt;
&lt;figure class=&quot;highlight bash&quot;&gt;&lt;table&gt;&lt;tr&gt;&lt;td class=&quot;gutter&quot;&gt;&lt;pre&gt;&lt;span class=&quot;line&quot;&gt;1&lt;/span&gt;&lt;br&gt;&lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;code&quot;&gt;&lt;pre&gt;&lt;span class=&quot;line&quot;&gt;sudo pacman -Syu&lt;/span&gt;&lt;br&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/figure&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;安装 MariaDB&lt;/strong&gt;：&lt;br&gt;接下来，使用 pacman 包管理器安装 MariaDB。在终端中运行以下命令：&lt;/p&gt;
&lt;figure class=&quot;highlight bash&quot;&gt;&lt;table&gt;&lt;tr&gt;&lt;td class=&quot;gutter&quot;&gt;&lt;pre&gt;&lt;span class=&quot;line&quot;&gt;1&lt;/span&gt;&lt;br&gt;&lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;code&quot;&gt;&lt;pre&gt;&lt;span class=&quot;line&quot;&gt;sudo pacman -S mariadb&lt;/span&gt;&lt;br&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/figure&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;初始化数据库系统&lt;/strong&gt;：&lt;br&gt;MariaDB 安装完成后，您需要初始化数据库系统。运行以下命令来执行这个步骤：&lt;/p&gt;
&lt;figure class=&quot;highlight bash&quot;&gt;&lt;table&gt;&lt;tr&gt;&lt;td class=&quot;gutter&quot;&gt;&lt;pre&gt;&lt;span class=&quot;line&quot;&gt;1&lt;/span&gt;&lt;br&gt;&lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;code&quot;&gt;&lt;pre&gt;&lt;span class=&quot;line&quot;&gt;sudo mariadb-install-db --user=mysql --basedir=/usr --datadir=/var/lib/mysql&lt;/span&gt;&lt;br&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/figure&gt;&lt;/li&gt;
&lt;/ol&gt;
    
    </summary>
    
    
      <category term="Linux" scheme="https://wangbenjun.site/categories/Linux/"/>
    
    
      <category term="ChatGPT" scheme="https://wangbenjun.site/tags/ChatGPT/"/>
    
  </entry>
  
  <entry>
    <title>ubuntu双系统时间同步问题</title>
    <link href="https://wangbenjun.site/2023/linux/linux-time-sync.html"/>
    <id>https://wangbenjun.site/2023/linux/linux-time-sync.html</id>
    <published>2023-07-03T12:30:22.000Z</published>
    <updated>2024-12-29T03:02:14.814Z</updated>
    
    <content type="html"><![CDATA[<p>估计用过windows和linux双系统切换的人都遇到过这个问题，2个系统之间一切换时间就不对劲了，虽然说可以手动同步，但是每次这么搞有点费劲，不如一劳永逸，彻底解决这个问题！</p><p>根本原因：linux系统与win系统对于时间的管理方式不同。</p><ul><li><p>linux认为硬件时间为GMT+0时间，是世界标准时间，而中国上海是东八区时间，显示时间为GMT+8。</p></li><li><p>windows系统认为硬件时间就是本地时间，而这个时间已经被linux设置为GMT+0时间，因此win系统下时间比正常时间慢8个小时。</p></li></ul><a id="more"></a><p>有一个快速解决方案，在ubuntu下安装ntpupdate同步时间，然后将本地时间更新到硬件上，命令如下：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">sudo apt-get install ntpdate</span><br><span class="line">sudo ntpdate time.windows.com</span><br><span class="line">sudo hwclock --localtime --systohc</span><br></pre></td></tr></table></figure><p>上面这个方案亲测可用，也很方便快捷，还有一个方案是执行： <code>timedatectl set-local-rtc 1</code> 命令让ubuntu将系统时间和BIOS时间同步，但是这个方案会有一个副作用，但是应该对大多人都没影响，问题不大。</p><p>当然上面这2种方案本质上都是把windows和ubuntu2个系统对时间的处理方式统一为同一个，只是这里我们只是修改了ubuntu的，没有动windows。</p><img src="/images/2023/2023-07-03 20-49-23.png" /><p>如果你不想这么做，或者这么做有其它的影响，那可以分别在windows、ubuntu上面设置自动时间同步的机制，每次开机的时候自动联网同步时间，缺点就是有点慢，可能开机进入桌面之后等上1-3s才完成同步时间。</p><ul><li><p>在windows里面可以使用计划任务，新建一个计划任务，每次用户登录的时候执行，命令是：<code>w32tm /resync</code>。</p></li><li><p>在ubuntu里面的话，需要安装ntpdate，然后执行<code>timedatectl set-ntp on</code>这样的话会有一个service服务在后台运行自动同步时间。</p></li></ul>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;估计用过windows和linux双系统切换的人都遇到过这个问题，2个系统之间一切换时间就不对劲了，虽然说可以手动同步，但是每次这么搞有点费劲，不如一劳永逸，彻底解决这个问题！&lt;/p&gt;
&lt;p&gt;根本原因：linux系统与win系统对于时间的管理方式不同。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;linux认为硬件时间为GMT+0时间，是世界标准时间，而中国上海是东八区时间，显示时间为GMT+8。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;windows系统认为硬件时间就是本地时间，而这个时间已经被linux设置为GMT+0时间，因此win系统下时间比正常时间慢8个小时。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
    
    </summary>
    
    
      <category term="Linux" scheme="https://wangbenjun.site/categories/Linux/"/>
    
    
      <category term="Ubuntu" scheme="https://wangbenjun.site/tags/Ubuntu/"/>
    
  </entry>
  
  <entry>
    <title>要不要使用RESTful？</title>
    <link href="https://wangbenjun.site/2023/coding/restful-api.html"/>
    <id>https://wangbenjun.site/2023/coding/restful-api.html</id>
    <published>2023-06-30T14:52:33.000Z</published>
    <updated>2024-12-29T03:02:14.812Z</updated>
    
    <content type="html"><![CDATA[<p>从事Web开发的童鞋对RESTful API很多人都不陌生，不同项目的接口形式千奇百怪，有些声称使用RESTful，但是实际上并不完全符合规范，不禁让我思考，到底要不要使用RESTful API的风格？</p><p>举个例子，相信大家见过不少下面风格的API：</p><table><thead><tr><th>用途</th><th>请求方法</th><th>命名</th></tr></thead><tbody><tr><td>获取用户列表</td><td>GET</td><td>&#x2F;getUserList</td></tr><tr><td>获取单个用户信息</td><td>GET</td><td>&#x2F;getUserInfo?id&#x3D;1</td></tr><tr><td>新增用户</td><td>POST</td><td>&#x2F;createUser</td></tr><tr><td>更新用户</td><td>POST</td><td>&#x2F;updateUser</td></tr><tr><td>删除用户</td><td>POST</td><td>&#x2F;deleteUser</td></tr></tbody></table><p>这是一个最简单的CURD接口，说实话，我并不反对这种形式的API，虽然说看上去俗了点，但是至少见名知意，除了获取数据使用GET，其它一律POST，简单粗暴，没什么大问题。</p><a id="more"></a><p>如果展开说，上面的这种形式在复杂的情况下，无法更好的表达接口的含义，比如说假设你要写一个获取用户的好友列表接口，那可能就得这么写：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">获取用户好友列表    GET   /getUserFriends?user_id=1</span><br></pre></td></tr></table></figure><p>这种命名方式在复杂情况下会导致接口名字越来越长。。。可读性就变的差了，而且一眼看不出来数据之间的关系，不好分类，假设有些人名字写的差就更难解读了。</p><p>如果使用RESTful则可以采用这种形式：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">获取用户好友列表    GET   /users/:id/friends</span><br></pre></td></tr></table></figure><p>传统的接口采用动词+接口名称的形式来表达含义，而RESTful推荐使用HTTP请求方式来定义，比如GET用于获取数据、POST用于创建、PUT用于修改、DELETE用于删除。</p><p>上面这些接口如果使用RESTful形式改写就是这样：</p><table><thead><tr><th>用途</th><th>请求方法</th><th>命名</th></tr></thead><tbody><tr><td>获取用户列表</td><td>GET</td><td>&#x2F;user</td></tr><tr><td>获取单个用户信息</td><td>GET</td><td>&#x2F;user&#x2F;:id</td></tr><tr><td>新增用户</td><td>POST</td><td>&#x2F;user</td></tr><tr><td>更新用户</td><td>PUT</td><td>&#x2F;user&#x2F;:id</td></tr><tr><td>删除用户</td><td>DELETE</td><td>&#x2F;user&#x2F;:id</td></tr></tbody></table><p>关于是用user还是users这块也有一个分歧，有些人觉得应该是用复数，因为有多个用户，所以获取用户得用getUsers，这个问题同样存在数据表名称上面。</p><p>我觉得接口用不用复数取决于数据表名称用不用，如果数据表就是users，那接口最好也用，保持一致。</p><p>至于说数据表名称该不该用复数，很多人都有不同的看法，在项目开始的时候统一明确的一个规范就行了，如果你要问我，我建议使用单数。</p><p>除此之外，RESTful还推荐通过HTTP状态码来区分结果，比如200表示正常，403表示没有权限，传统的接口往往统一使用200，然后在响应内容里面去区分。</p><p>简单说，RESTful的设计初衷是把这些数据实体当成一种资源，通过URL去定位资源，用HTTP描述操作，达到以下几个目标：</p><ul><li>看URL就知道要什么</li><li>看HTTP方法就知道干什么</li><li>看HTTP状态码就知道结果如何</li></ul><p>在我的职业生涯里面见过很多看似的RESTful的风格，但是实际上也不完全是，比如说PUT、DELETE的时候id是以http query string的方式传递，而不是path：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">删除用户    DELETE    /user?id=1</span><br></pre></td></tr></table></figure><p>还有接口是直接把所有参数放到body里面传递，虽然说这些方式都可以，也不麻烦，因为现在很多Web框架都这些参数的处理都很方便，但是感觉还是不伦不类，当然有时候也是逼不得已，比如有些表的设计没有单一id主键，只有一个复合主键，这就很难使用path这种形式。</p><h2 id="并非完美"><a href="#并非完美" class="headerlink" title="并非完美"></a>并非完美</h2><p>RESTful的设计规范很理想，但是并不适合所有的项目，因为很多接口不是纯粹的CURD操作，它不仅仅是对一张表的增删改查，这导致很难通过URL去表达含义。</p><p>对于单纯的获取数据、修改数据的接口RESTful比较合适，但是当你的接口里面包含非常复杂的业务逻辑的时候，RESTful就非常麻烦了，根本无处下手。</p><p>举个例子，一个根据用户的住址获取附近的超市的接口，这样的接口如果使用传统的方式可能是这样：</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">/getUserNearbySupermarket?user_id=1</span><br></pre></td></tr></table></figure><p>如果采用RESTful就存在一个问题，这到底是围绕着用户还是超市来设计呢？</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">/user/:id/nearby_supermarket</span><br><span class="line">/supermarket/nearby?user_id=1</span><br><span class="line">/user_nearby_supermarket?user_id=1</span><br></pre></td></tr></table></figure><p>以上这3个例子你们觉得哪个好？对此我也纠结了很久，最后在参考了很多云厂商的API之后，个人觉得第一种比较合适。</p><p>有些时候RESTful API定义的GET、POST、PUT、DELETE4个常用方式并不能满足所有的应用场景，或者不够清晰，比如说业务有个场景是要清空用户黑名单，这属于PUT还是DELETE？</p><p>从用户的角度来说说PUT，因为我是修改了用户的附属属性，但是从黑名单的角度是删除了数据，都有一定的道理。</p><p>或许，我们可以这么设计</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">POST    /user/:id/blacklist/empty</span><br></pre></td></tr></table></figure><p>我们可以认为blacklist是用户的属性，empty是一种动作，是不是更加清晰一点，至于方法是用POST还是PUT，区别都不大。</p><h2 id="最佳实践"><a href="#最佳实践" class="headerlink" title="最佳实践"></a>最佳实践</h2><p>对于开放平台的接口，比如说一个云厂商，需要提供对应的sdk或者api给用户使用，推荐使用RESTful，实际上国外很多大厂的api是非常标准的RESTful风格，国内某大厂除外。</p><p>云服务本身就是提供资源，无论是服务器、IP、域名，它都是一种网络资源，而RESTful天生就是为了描述资源，并且云服务的操作逻辑简单，没有复杂的业务逻辑，对于服务器无法就是创建、查看、修改、删除、重启等。</p><p>以前用过vultr的API，就是非常规范的RESTful风格，看着很舒服：</p><table><thead><tr><th>用途</th><th>请求方法</th><th>命名</th></tr></thead><tbody><tr><td>获取实例</td><td>GET</td><td>&#x2F;instances</td></tr><tr><td>获取单个实例</td><td>GET</td><td>&#x2F;instances&#x2F;:id</td></tr><tr><td>新增实例</td><td>POST</td><td>&#x2F;instances</td></tr><tr><td>更新实例</td><td>PUT</td><td>&#x2F;instances&#x2F;:id</td></tr><tr><td>删除实例</td><td>DELETE</td><td>&#x2F;instances&#x2F;:id</td></tr><tr><td>启动实例</td><td>POST</td><td>&#x2F;instances&#x2F;:id&#x2F;start</td></tr><tr><td>批量启动实例</td><td>POST</td><td>&#x2F;instances&#x2F;start</td></tr></tbody></table><p>值得一说的是启动这个操作，它有2种形式，一个是操作单个实例，参数放在path里面，还有一个是批量操作，是有多个id，这时候参数是放在json body里面。</p><p>但是对于很多公司内部系统的接口，RESTful往往并不是最佳的选择，如果为了简单方便，不考虑这些乱七八糟的规范，我建议除了获取数据使用GET，其它一律使用POST。</p><table><thead><tr><th>用途</th><th>请求方法</th><th>命名</th></tr></thead><tbody><tr><td>获取用户列表</td><td>GET</td><td>&#x2F;user</td></tr><tr><td>新增用户</td><td>POST</td><td>&#x2F;user</td></tr><tr><td>更新用户</td><td>POST</td><td>&#x2F;user</td></tr><tr><td>删除用户</td><td>POST</td><td>&#x2F;user</td></tr></tbody></table><p>比如上面这个写法，第一个GET接口要支持多个参数过滤，比如说可以这么写&#x2F;user?id&#x3D;1,相当于获取单个用户，除了GET之外的接口参数全部放到body里面，而且统一采用JSON形式。</p><p>还有一个响应状态码的问题，个人觉得还是统一200比较合适，在响应体内部另外定义code来表达结果，也就是说不依赖HTTP状态码，虽然有点不符合RESTful的规范，但是兼容性好，因为你不知道哪个中间件会怎么处理。</p><p><strong>最后总结一下</strong>：</p><p>对于要不要使用RESTful风格的API这个问题没有明确的答案，如果你的应用逻辑简单，大多数是对一些数据表的增删改查，可以考虑使用RESTful风格的接口，相反如果你的应用业务逻辑复杂，也可以不用。</p><p>如果考虑到这是一个对外公开的OpenAPI，建议使用RESTful风格，毕竟这也算是一种业界共识，你整个什么getXXXByXXX显的确实有点low啊。</p><p>其实我们也可以2者混用，根据实际情况来决定，毕竟这只是一个接口路由，没必要纠结太多，大部分Web框架都支持各种形式的路由参数，对于简单的CURD可以使用RESTful，对于复杂的业务逻辑则特殊处理。</p>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;从事Web开发的童鞋对RESTful API很多人都不陌生，不同项目的接口形式千奇百怪，有些声称使用RESTful，但是实际上并不完全符合规范，不禁让我思考，到底要不要使用RESTful API的风格？&lt;/p&gt;
&lt;p&gt;举个例子，相信大家见过不少下面风格的API：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;用途&lt;/th&gt;
&lt;th&gt;请求方法&lt;/th&gt;
&lt;th&gt;命名&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td&gt;获取用户列表&lt;/td&gt;
&lt;td&gt;GET&lt;/td&gt;
&lt;td&gt;&amp;#x2F;getUserList&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;获取单个用户信息&lt;/td&gt;
&lt;td&gt;GET&lt;/td&gt;
&lt;td&gt;&amp;#x2F;getUserInfo?id&amp;#x3D;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;新增用户&lt;/td&gt;
&lt;td&gt;POST&lt;/td&gt;
&lt;td&gt;&amp;#x2F;createUser&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;更新用户&lt;/td&gt;
&lt;td&gt;POST&lt;/td&gt;
&lt;td&gt;&amp;#x2F;updateUser&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;删除用户&lt;/td&gt;
&lt;td&gt;POST&lt;/td&gt;
&lt;td&gt;&amp;#x2F;deleteUser&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;这是一个最简单的CURD接口，说实话，我并不反对这种形式的API，虽然说看上去俗了点，但是至少见名知意，除了获取数据使用GET，其它一律POST，简单粗暴，没什么大问题。&lt;/p&gt;
    
    </summary>
    
    
      <category term="Coding" scheme="https://wangbenjun.site/categories/Coding/"/>
    
    
      <category term="编程" scheme="https://wangbenjun.site/tags/%E7%BC%96%E7%A8%8B/"/>
    
  </entry>
  
  <entry>
    <title>终极免费“白嫖”科学上网攻略</title>
    <link href="https://wangbenjun.site/2023/linux/free-v2ray-proxy.html"/>
    <id>https://wangbenjun.site/2023/linux/free-v2ray-proxy.html</id>
    <published>2023-06-22T03:28:27.000Z</published>
    <updated>2024-12-29T03:02:14.814Z</updated>
    
    <content type="html"><![CDATA[<p>在国内搞计算机网络开发基本上必备梯子，不仅仅是为了使用Google，其实很多海外网站，不是打开慢就是偶尔彻底打不开，还有类似最近大火的ChatGPT，有梯子还不一定能用上。</p><p>为了解决这个问题，必须需要一个VPN，也就是俗称的梯子，梯子哪里来呢？</p><ul><li>免费，网上有很多免费梯子，网速慢也不稳定，凑合用一下没问题，不过存在信息泄露风险，毕竟天下没有免费的午餐，不建议长期使用。</li><li>收费，网上卖梯子的也很多，可用节点多，网速好，但是很多都是有流量限制，比如一个月100GB大概是20-30块钱，不过同样存在安全风险，另外购买包年的话小心商家跑路。</li><li>自建，网上搭建教程一大把，很多搞技术开发的童鞋对于Linux都不陌生，看着教程搭建一个并不难，但是依然需要花钱购买服务器、域名等</li></ul><p>作为一个资深互联网爱好者，早在Google退出中国之前就注册了账号，一直延用至今，这些年也没少折腾，从shadowsocks到v2ray，各式各样的梯子技术都尝试过，也算是稍微有点经验，今天就来分享一个永久免费的方案，一分钱不花享受高速梯子服务。</p><a id="more"></a><p>其实在我发现这个方案之前，我也都是在花钱购买VPS，一直用的vutlr的服务，至今账号里面还有10多刀没花完，vultr的话其实还可以，一个月大概是5刀，最便宜的是2.5刀，对比国内的云云服务器来说算是便宜了。现在不仅是vultr，很多海外的小VPS厂商的IP可用的真不多，而且容易被封，因为被拿来搭梯子用的太多了，网速也比较慢。</p><p>不过vultr有一些好处，首先它支持支付宝付款，这个方便了很多国内用户，众所周知，很多海外的平台都需要外币信用卡，难倒了不少人。其次它是按秒计费的，实例删除就不计费了，此外vultr还提供了api，可以利用这个实现一个自动化操作，我之前就写了一个程序自动检测IP可用性，如果发现被封后重建实例。</p><img src="/images/2023/2023-06-221201.png"><p>另外还提供了免费的DNS服务，我目前就在用，有人回好奇为什么不用阿里云或者dnspod等来解析，我想说，我的目标是不使用任何国内云服务厂商的东西，原因你们都懂。</p><p>如果有人需要自费购买的话，可以通过我的邀请码注册一波：<a href="https://www.vultr.com/?ref=6891627" target="_blank" rel="noopener">https://www.vultr.com/?ref=6891627</a></p><p>言归正传，今天我想说的是一套完全免费的方案，包括以下几个方面：</p><h2 id="1-免费服务器"><a href="#1-免费服务器" class="headerlink" title="1.免费服务器"></a>1.免费服务器</h2><p>其实国外很多云服务厂商都有免费试用期，比如微软云、Google云、亚马逊云通常都会有一个免费6个月-12个月的试用期，在此期间会提供一定免费额度可用拿来使用，比如说可以创建一个2核4G内存的实例。</p><p>虽然还是有时间限制的，不过你都白嫖一遍的话应该也够你用好几年了，只需要你拥有一个外币信用卡，因为在国外很多平台都流行信用消费，通常都需要你绑定信用卡，然后先消费后付账单。</p><p>注册账号之后需要绑定信用卡，第一次会扣取你0.1刀的钱，然后返还给你，这是为了验证你的信用卡的有效性。</p><p>但是我今天要说的是Oracle甲骨文云的<strong>【永久免费】</strong>服务，对，你没听错是永久免费，只是存在一个使用量的限制，这个使用量对于个人来说绰绰有余，商业服务那肯定不够用的。</p><img src="/images/2023/2023-06-222235.png"><p>简单说，就是Oracle云免费提供一个双核1G内存的x86实例或者一个16核24G内存的arm实例，10G的存储，还有很多其它的服务，只不过咱们都用不到。</p><p>但是也不是完全没有限制的，Oracle不允许你浪费，所谓不允许浪费就是你不能把实例闲置了，系统会检测你的实例负载，如果过低就会暂停实例，比如说在7天内满足以下条件的实例：</p><ul><li>95%时间的CPU使用率不足10%</li><li>网络利用率低于10%</li><li>内存利用率低于10%（仅适用于A1 ARM免费实例）</li></ul><p>如果你的服务器只是拿来搭梯子的话，用的不多的话这个负载还真达不到要求，但是可以用一些手段故意把负载提上来，Github上面有人写了“保活”脚本运行去一些命令来加大负载。其实万一真的被检测到问题也不大，系统并不会删除实例，只是会把你实例停止了，然后给你发一封邮件告诉你，你只需登录到控制台里面再启动就行了。</p><p>具体的话可以参考其官网介绍：<a href="https://www.oracle.com/cloud/free/" target="_blank" rel="noopener">https://www.oracle.com/cloud/free/</a></p><h2 id="2-免费的CDN加速"><a href="#2-免费的CDN加速" class="headerlink" title="2.免费的CDN加速"></a>2.免费的CDN加速</h2><p>cdn加速并不是必须的，但有免费的不用白不用，cdn加速有很多好处的，第一个是提高网速和稳定性，虽然亚马逊云在国内没有cdn节点，但是可以用到亚洲的节点，比如韩国、日本、新加坡的加速节点，这比你直接连美国的服务器还是要快的吧。</p><p>另外，使用cdn加速之后，我们可以用cdn提供的加速域名来配置，意味着不用对外暴露真实的服务器IP，这样被封禁的可能性就很小。GFW一般情况下不会去封这些云服务器的加速域名，因为这样误杀的就太多了，毕竟国内很多互联网公司出海还是需要用到海外这些云厂商的服务的。</p><img src="/images/2023/2023-06-222240.png"><p>亚马逊云提供了每个月免费1TB流量的cdn服务，只要不超过这个流量都不要钱，放心用。说实话，自己一个人用，就算你一天到晚看油管也用不了这么多。。。这个额度已经很豪横了。</p><p>具体的话可以参考其官网介绍：<a href="https://aws.amazon.com/cn/cloudfront/?nc2=h_ql_prod_nt_cf" target="_blank" rel="noopener">https://aws.amazon.com/cn/cloudfront/?nc2=h_ql_prod_nt_cf</a></p><p>有一点需要大家注意，据说这个1TB流量只是cdn下载，如果是上传的话量大了可能要收费，因为平时咱们流量网页上传的流量是极少的，除非你有特殊需求，比如说在油管上传视频，这可能会消耗大量的上传流量然后产生账单，具体情况我也不清楚，因为我也不上传视频，所以用了很久没有产生过账单。</p><h2 id="3-免费的域名和证书"><a href="#3-免费的域名和证书" class="headerlink" title="3.免费的域名和证书"></a>3.免费的域名和证书</h2><p>我们的方案需要需要域名和ssl证书，这2个其实自己买也不要多少钱，很多垃圾域名只要几块钱1年，但是我还是不推荐使用国内的云厂商的服务，所以选择从国外平台申请免费的。</p><p>免费域名的平台很多，我是从freenom上面申请的，可以免费用1年，另外ssh证书的话可以从Let’s Encrypt申请免费的，一般也都是1年有效期，所以就是稍微有点麻烦，至少1年得维护一次。</p><p>对于证书，如果你是使用一些v2ray搭建脚本的话，像caddy是可以自动申请免费的证书的，都不用你操心。。。</p><hr><p>最后说说搭建方案，那自然是基于v2ray的<strong>vmess+ws+tls+cdn</strong>组合，这套组合安全性非常高，我用了这么久没被封过，虽然说延迟高一点，但是你又不是拿来打游戏，看看网页问题不大。</p><p>具体的搭建教程网上很多我就不多说了，Github上面还有那种一键脚本，已经非常简单方便了，只是cdn这块需要单独配置一下。这套下来基本上不用花一分钱了，甚至还可以共享给自己熟悉的朋友一起用，一个月省它5刀不香吗？一年下来还能省几百块呢，哈哈。</p><p>最后说个缺点，这套方案大概率是无法解锁Netfix、迪士尼这种区域限制的流媒体服务，因为涉及到版权因素，这些服务每个国家的运营政策都不一样，而通常这些公司都是通过访问的IP来判断用户所在的国家。</p><p>但是由于咱们使用的是云服务器的IP，一看就知道不是正常用户，所以这些公司把很多VPS、云厂商的IP都封禁了，虽然不是所有，但是很多都封了，包括ChatGPT也是这样，据说ChatGPT只有微软云的IP可用，其它的都不行。</p><p>所以不是说完全不行，有时候多试试多换IP可能会找到可用的，这就很尴尬了，如果你对这些服务有需求的话这时候购买收费的梯子的好处就体现了，不过也有一些技术手段来解除这个限制，只是比较复杂这里就不多说了。</p>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;在国内搞计算机网络开发基本上必备梯子，不仅仅是为了使用Google，其实很多海外网站，不是打开慢就是偶尔彻底打不开，还有类似最近大火的ChatGPT，有梯子还不一定能用上。&lt;/p&gt;
&lt;p&gt;为了解决这个问题，必须需要一个VPN，也就是俗称的梯子，梯子哪里来呢？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;免费，网上有很多免费梯子，网速慢也不稳定，凑合用一下没问题，不过存在信息泄露风险，毕竟天下没有免费的午餐，不建议长期使用。&lt;/li&gt;
&lt;li&gt;收费，网上卖梯子的也很多，可用节点多，网速好，但是很多都是有流量限制，比如一个月100GB大概是20-30块钱，不过同样存在安全风险，另外购买包年的话小心商家跑路。&lt;/li&gt;
&lt;li&gt;自建，网上搭建教程一大把，很多搞技术开发的童鞋对于Linux都不陌生，看着教程搭建一个并不难，但是依然需要花钱购买服务器、域名等&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;作为一个资深互联网爱好者，早在Google退出中国之前就注册了账号，一直延用至今，这些年也没少折腾，从shadowsocks到v2ray，各式各样的梯子技术都尝试过，也算是稍微有点经验，今天就来分享一个永久免费的方案，一分钱不花享受高速梯子服务。&lt;/p&gt;
    
    </summary>
    
    
      <category term="Linux" scheme="https://wangbenjun.site/categories/Linux/"/>
    
    
      <category term="Linux" scheme="https://wangbenjun.site/tags/Linux/"/>
    
  </entry>
  
  <entry>
    <title>不要再用Spintf拼Sql了</title>
    <link href="https://wangbenjun.site/2022/coding/golang/sprintf-sql-inject.html"/>
    <id>https://wangbenjun.site/2022/coding/golang/sprintf-sql-inject.html</id>
    <published>2022-06-28T03:10:00.000Z</published>
    <updated>2024-12-29T03:02:14.810Z</updated>
    
    <content type="html"><![CDATA[<p>说到代码安全，虽然很多时候大家都不当回事，只要实现业务功能就行了，但是一旦出现安全问题都不是小事，突然想起来之前项目有很多类似的写法：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">sql := <span class="string">"select * from xxx where 1=1"</span></span><br><span class="line"><span class="keyword">if</span> name != <span class="string">""</span> &#123;</span><br><span class="line">    sql += fmt.Sprintf(<span class="string">" and name = '%s'"</span>, name)</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">if</span> address != <span class="string">""</span> &#123;</span><br><span class="line">    sql += fmt.Sprintf(<span class="string">" and address = '%s'"</span>, name)</span><br><span class="line">&#125;</span><br><span class="line">err := db.Raw(sql).Find(&amp;results).Error</span><br><span class="line"><span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="literal">nil</span>, err</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>为什么要这么写？主要是为了实现一些复杂的列表查询，默认情况下查询所有，但是有很多可选的参数，所以图省事选择了拼Sql，虽然功能上没有什么问题。</p><p>但是经过我的测试，上面这种写法存在SQL注入风险，非常危险！！！</p><a id="more"></a><p>Sprintf这个函数只是帮你格式化，实际上并不会像MySql的预处理一样对特殊字符加以转义等处理，所以当有心之人构造特殊参数输入的话就会产生意外的结果。</p><p>比如，当name内容是<code>&#39; union select 1,2,3,4,5,version(),user(),database()&#39;</code>的时候，就会执行一条注入的语句，获取数据库的一些信息为进一步的渗透做准备。</p><h2 id="解决方法"><a href="#解决方法" class="headerlink" title="解决方法"></a>解决方法</h2><p>最正确的做法是Prepare预处理，如果你没有使用ORM的话，基本上都需要对所有的SQL做预处理操作防止注入，比较麻烦！</p><p>但是现在一般都会使用ORM来操作数据库，ORM本质上也是拼SQL，只不过底层都会统一使用SQL预处理，对注入这种攻击都有防备，所以尽可能的使用ORM封装好的方法，比如上面这种需求在Gorm里面其实可以使用结构体参数或者map参数的方式查询，Gorm会自动忽略零值的参数。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// NOTE When querying with struct, GORM will only query with non-zero fields, </span></span><br><span class="line"><span class="comment">// that means if your field’s value is 0, '', false or other zero values, </span></span><br><span class="line"><span class="comment">// it won’t be used to build query conditions, for example:</span></span><br><span class="line">db.Where(&amp;User&#123;Name: <span class="string">"jinzhu"</span>, Age: <span class="number">0</span>&#125;).Find(&amp;users)</span><br><span class="line"><span class="comment">// SELECT * FROM users WHERE name = "jinzhu";</span></span><br></pre></td></tr></table></figure><p>千万不要再偷懒手动拼SQL，安全问题无小事，有人可能说我对字符参数校验了，不允许出现那些特殊字符，对于整形参数都会先转int，当然这也是一些参数层面预防方法，但是都不彻底。</p>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;说到代码安全，虽然很多时候大家都不当回事，只要实现业务功能就行了，但是一旦出现安全问题都不是小事，突然想起来之前项目有很多类似的写法：&lt;/p&gt;
&lt;figure class=&quot;highlight go&quot;&gt;&lt;table&gt;&lt;tr&gt;&lt;td class=&quot;gutter&quot;&gt;&lt;pre&gt;&lt;span class=&quot;line&quot;&gt;1&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;2&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;3&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;4&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;5&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;6&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;7&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;8&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;9&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;10&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;11&lt;/span&gt;&lt;br&gt;&lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;code&quot;&gt;&lt;pre&gt;&lt;span class=&quot;line&quot;&gt;sql := &lt;span class=&quot;string&quot;&gt;&quot;select * from xxx where 1=1&quot;&lt;/span&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;if&lt;/span&gt; name != &lt;span class=&quot;string&quot;&gt;&quot;&quot;&lt;/span&gt; &amp;#123;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;    sql += fmt.Sprintf(&lt;span class=&quot;string&quot;&gt;&quot; and name = &#39;%s&#39;&quot;&lt;/span&gt;, name)&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&amp;#125;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;if&lt;/span&gt; address != &lt;span class=&quot;string&quot;&gt;&quot;&quot;&lt;/span&gt; &amp;#123;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;    sql += fmt.Sprintf(&lt;span class=&quot;string&quot;&gt;&quot; and address = &#39;%s&#39;&quot;&lt;/span&gt;, name)&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&amp;#125;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;err := db.Raw(sql).Find(&amp;amp;results).Error&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;if&lt;/span&gt; err != &lt;span class=&quot;literal&quot;&gt;nil&lt;/span&gt; &amp;#123;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;    &lt;span class=&quot;keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;literal&quot;&gt;nil&lt;/span&gt;, err&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&amp;#125;&lt;/span&gt;&lt;br&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/figure&gt;
&lt;p&gt;为什么要这么写？主要是为了实现一些复杂的列表查询，默认情况下查询所有，但是有很多可选的参数，所以图省事选择了拼Sql，虽然功能上没有什么问题。&lt;/p&gt;
&lt;p&gt;但是经过我的测试，上面这种写法存在SQL注入风险，非常危险！！！&lt;/p&gt;
    
    </summary>
    
    
      <category term="Golang" scheme="https://wangbenjun.site/categories/Golang/"/>
    
    
      <category term="Golang" scheme="https://wangbenjun.site/tags/Golang/"/>
    
      <category term="Mysql" scheme="https://wangbenjun.site/tags/Mysql/"/>
    
  </entry>
  
  <entry>
    <title>在北京租房的那些坑</title>
    <link href="https://wangbenjun.site/2022/life/beijing-zufang.html"/>
    <id>https://wangbenjun.site/2022/life/beijing-zufang.html</id>
    <published>2022-06-19T09:00:00.000Z</published>
    <updated>2024-12-29T03:02:14.813Z</updated>
    
    <content type="html"><![CDATA[<p>作为一名在北京工作了5年多，几乎1年1换房子的人也算是经验丰富了，特此总结一下北京租房的几个途径和一些坑。</p><p>如果简单的划分，可以认为租房有2种主要方式，一种是和直接和房东签合同，另一种不是直接和房东签，俗称二房东。</p><p>下面，我就主要从租房途径这个层面来讲一讲，以北京为例和个人经历为主：</p><h2 id="1、房屋中介"><a href="#1、房屋中介" class="headerlink" title="1、房屋中介"></a>1、房屋中介</h2><p>中介的水很深，中介靠不靠谱也得看情况，好在这几年房屋中介相关的管理监管加强了，幺蛾子没那么多了，但是也不能掉以轻心。</p><p>在北京，大家经常都在街上看见他们的店铺的中介就那几家，就不用我说了，但是那些不知名的小中介、区域性中介就太多了，据统计北京大概有上百家房屋中介，有人说存在即合理，在我看来，是存在既有利润。</p><p>这些大中介的中介费大多是1个月，最多给你打9折，也就是0.9个月，很多小中介比较便宜，可以谈到0.5个月，从成本的角度来说，这些小中介在广告投入上比较少，费用低一些是可以理解的，但不是说大中介贵就一定不会翻车，小中介便宜一定有问题。</p><a id="more"></a><p>虽然说大中介管理更规范，但是管不规范最终还是得看人，中介员工流动性非常大，门槛又低，不管是哪个中介，你都得留个心眼，不可全信中介的话，但是从最终合同上面来说，通过中介租房一般有2种形式：</p><h3 id="1-和房东签合同"><a href="#1-和房东签合同" class="headerlink" title="1.和房东签合同"></a>1.和房东签合同</h3><p>这一种很好理解，是中介最基础的功能，它只负责撮合交易，把你和房东拉到一起签一个合同，拿了中介费就完事了，后续的一切事务都是和你房东之间，比如交房租也是交给房东、后续续租等事宜也是你自己和房东谈。</p><p>这种的合同大部分情况下没有问题，负责任的中介还会帮你核实信息，比如房产证和身份证等，保证你签的房子没问题，别又签到二房东了。</p><h3 id="2-和中介签合同"><a href="#2-和中介签合同" class="headerlink" title="2.和中介签合同"></a>2.和中介签合同</h3><p>这个很多人可能很难理解，我都找中介了，为什么还和中介签合同？这是因为很多房东为了省事会把房子全权委托给中介（时间都比较长，1-5年），中介会给房东房租，然后你交房租给中介，这样中介的利润就会多很多，因为租出去的价格是中介定。</p><p>这种房子靠不靠谱呢，正常情况下是靠谱的，但是也有不靠谱的，特别是一些小中介，半途跑路。。。两头吃。</p><p>我还见过有些和中介签的合同，到期续租又得交1个月中介费的。。。这一点如果你要续租的话务必提前问清楚。</p><p>而且请务必注意签合同的时候对面是中介，并不是个人，因为有很多中介的员工打着公司名义，自己干着二房东的勾当，个人跑路离职的风险可比公司又大很多。</p><h2 id="2、互联网直租"><a href="#2、互联网直租" class="headerlink" title="2、互联网直租"></a>2、互联网直租</h2><p>以自如、蛋壳为代表的互联网租房模式，虽然蛋壳已经凉了，但是这种模式依然存在，简单说，就是公司二房东，公司从房东手里拿房，一般都是签长期合同3-5年，然后会统一简单装修配备一些家具家电，再出租给个人。</p><p>通常情况下为了提高利润，都会采用合租这种方式，特别是在北京这种高房价的城市，整租费用太高，合租非常普遍。</p><p>说实话，这些APP最大的优点就是省事，可以方便的选房看房，下单签约，但是缺点就是贵啊，比如自如比你自己找传统中介要贵上10-20%左右，虽然没有中介费，但是多出一个每个月10%的服务费，所谓服务就是网络、维修和1个月1次的保洁服务，这个保洁服务你自己请也就50块钱1小时嘛，你算算账就知道值不值了，只不过是把中介费换一种形式。</p><p>如果你是合租的话，一个人住，图省事可以从这些APP上面找，整租的话不推荐。这些管家的服务质量也是层次不齐，网上对于这些公司的投诉也是不少，所以我住了2年就没继续住了。</p><h2 id="3、豆瓣、论坛、群等渠道"><a href="#3、豆瓣、论坛、群等渠道" class="headerlink" title="3、豆瓣、论坛、群等渠道"></a>3、豆瓣、论坛、群等渠道</h2><p>说实话，这些渠道能找到真正的房东直租几乎不可能，大多都是一些二房东、以及中介的员工发的帖子，因为很多他们没有钱去投送广告，只能注册一堆号去各大论坛、群发各种租房信息，引流拉客户，当然我也不是说这些信息就一定假，需要大家自己睁开双眼甄别。</p><h2 id="4、小区门卫、小卖部"><a href="#4、小区门卫、小卖部" class="headerlink" title="4、小区门卫、小卖部"></a>4、小区门卫、小卖部</h2><p>很多人可能都忽略了这个渠道，这个渠道其实非常有效，如果你确定要去某个小区租房，不妨先去打听一下，很多时候这里能真正找到房东直租的房子。</p><p>不过也可能也要中介费，但是一般都可以谈的，因为小区里面的人消息都很灵敏，比如业主群里面有房子要出租他们会第一时间得到消息，这比你去旁边中介的消息可能更准，有些中介不仅找你收中介费，也会找房东收中介费，如果房东能够省一笔中介费，谁会不愿意。</p><p>最后，说一说一些常见的租房问题和坑。</p><h2 id="5、那些坑"><a href="#5、那些坑" class="headerlink" title="5、那些坑"></a>5、那些坑</h2><h3 id="1、伪押一付三"><a href="#1、伪押一付三" class="headerlink" title="1、伪押一付三"></a>1、伪押一付三</h3><p>举个例子，你2月1号起租，付了3个月（2、3、4这3个月的）房租，那么下一次付房租应该是5月1号，但是有些二房东或者黑中介会在合同里面给你做文章，把下一次付房租的日子就提前了1个月，写成4月1号。</p><p>这就相当于押2付3了，如果你不注意或者不管，到时候他们就会拿合同说事让你交房租，不然算你违约，扣你这2个月押金。</p><h3 id="2、退租咋赔偿？"><a href="#2、退租咋赔偿？" class="headerlink" title="2、退租咋赔偿？"></a>2、退租咋赔偿？</h3><p>对于退租这件事，房东肯定不乐意，所以扣除1个月押金我能理解，大部分租房合同里面也会这么约定，说实话在北京搞互联网的换个工作很正常，所以在签合同的时候一定要看好，退租赔偿是不是1个月，而不是2个月。</p><p>但是有些二房东使了心眼，虽然写着1个月，但是就如第1条那样，实收你2个月押金，就看你能不能接受了，如果你住不满1年的话，2个月房租就没了。</p><h3 id="3、能不能转租？"><a href="#3、能不能转租？" class="headerlink" title="3、能不能转租？"></a>3、能不能转租？</h3><p>几乎所有的租房合同里面都写着不能转租，如果你仔细看的话，有些会写着转租会赔偿1个月房租，然后房东再和新租客签订新合同，实际上这就是退租了，非常不合理。</p><p>在我看来，比如我签了1年，但是只住了7个月，还有5个月租期，我自己找了一个租客把剩下5个月租给他，每个月租金不变，这才叫转租才对。</p><p>所以，如果你有转租计划，最好的办法就是不要告诉房东或者中介，自己找人完成交易，最好是熟人，可以不用交押金，后续押金你自己找房东退，千万不要和二房东或者中介说你要转租，一旦你这么说，他们就说你违约。</p><h3 id="4、房屋维修谁负责？"><a href="#4、房屋维修谁负责？" class="headerlink" title="4、房屋维修谁负责？"></a>4、房屋维修谁负责？</h3><p>正常人理解里面，租房租的不止是房子，而且包括屋里面的家电家具等设施，所以家电的维修理当房东承担，这没毛病吧？</p><p>但是真有房东居然不这么认为，他觉得我租给的你时候冰箱是好的，你还我的时候也得是好的，如果坏了你自己出钱维修，我无语，虽然最后并没坏过。</p><p>通常来说，这些大家电寿命都很长，但是万一真坏了还是很麻烦的，修一下几百块钱没了，所以一定要在合同里面写清楚谁负责维修。有些格式合同里面并没用写到维修这块，强烈建议一定要补上，这样如果后续房东不认你也有证据。</p><h3 id="5、物业费谁交？"><a href="#5、物业费谁交？" class="headerlink" title="5、物业费谁交？"></a>5、物业费谁交？</h3><p>通常来说，租房消费品比如水电燃气都是租客交，其它的物业费、取暖费等固定费用都是房东交，至少北京这块大部分都是这样。至少我目前没有交过一次物业费，但是这一点你必须在合同里面体现，因为有些地方，比如南方很多城市默认是租客交，为了避免纠纷，一定要写清楚。</p><h3 id="6、不退押金咋办？"><a href="#6、不退押金咋办？" class="headerlink" title="6、不退押金咋办？"></a>6、不退押金咋办？</h3><p>前几年，北京这种二房东或者黑中介特别多，会找各种理由扣你押金，少则几百，多则不给了，报警不管，起诉太耗时，一般人只能不了了之。</p><p>个人住过一次二房东的房子，退租的时候只扣了50块钱卫生费，算是良心了。</p><p>其实这几年这种情况少了，如果你问我有没有更好的办法完全避免这种情况，我只能说没有，中国对租房者的权益保障几乎没有，不然也不会这么多人拼命买房了。</p><p>我只能和你说，尽量和房东客客气气说话，有问题好好沟通，平时保持好屋内设施的整洁，退租的时候最好收拾一下，不要太过分了。另外，对于北京来说，你或许可以举报一下扫黄打黑办或者是群租房，很多二房东合租都会对房屋进行隔断，北京这几年都在打击群租房打隔断的情况，只要你举报，会有人管的。</p><h3 id="7、口头承诺不要相信"><a href="#7、口头承诺不要相信" class="headerlink" title="7、口头承诺不要相信"></a>7、口头承诺不要相信</h3><p>这一点，成年人应该都知道了，租房之前的承诺只要没写到合同里面等于没说，写到合同里面如果你真较真的话是可以起诉对方的，虽然说很麻烦，但是100%胜诉，有些人怕这个。</p><p>但是如果不写到合同里面，无论中介或者二房东和你怎么说都没用，比如答应你给你配一个新的冰箱，签完合同不承认了，你咋办？你不给房租算你违约，扣你押金，报警的话众所周知，这是民事纠纷，只要不出人命，警察不管的，你只能认了。</p>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;作为一名在北京工作了5年多，几乎1年1换房子的人也算是经验丰富了，特此总结一下北京租房的几个途径和一些坑。&lt;/p&gt;
&lt;p&gt;如果简单的划分，可以认为租房有2种主要方式，一种是和直接和房东签合同，另一种不是直接和房东签，俗称二房东。&lt;/p&gt;
&lt;p&gt;下面，我就主要从租房途径这个层面来讲一讲，以北京为例和个人经历为主：&lt;/p&gt;
&lt;h2 id=&quot;1、房屋中介&quot;&gt;&lt;a href=&quot;#1、房屋中介&quot; class=&quot;headerlink&quot; title=&quot;1、房屋中介&quot;&gt;&lt;/a&gt;1、房屋中介&lt;/h2&gt;&lt;p&gt;中介的水很深，中介靠不靠谱也得看情况，好在这几年房屋中介相关的管理监管加强了，幺蛾子没那么多了，但是也不能掉以轻心。&lt;/p&gt;
&lt;p&gt;在北京，大家经常都在街上看见他们的店铺的中介就那几家，就不用我说了，但是那些不知名的小中介、区域性中介就太多了，据统计北京大概有上百家房屋中介，有人说存在即合理，在我看来，是存在既有利润。&lt;/p&gt;
&lt;p&gt;这些大中介的中介费大多是1个月，最多给你打9折，也就是0.9个月，很多小中介比较便宜，可以谈到0.5个月，从成本的角度来说，这些小中介在广告投入上比较少，费用低一些是可以理解的，但不是说大中介贵就一定不会翻车，小中介便宜一定有问题。&lt;/p&gt;
    
    </summary>
    
    
      <category term="Life" scheme="https://wangbenjun.site/categories/Life/"/>
    
    
      <category term="Life" scheme="https://wangbenjun.site/tags/Life/"/>
    
  </entry>
  
  <entry>
    <title>那些年，那些事</title>
    <link href="https://wangbenjun.site/2022/life/remember-working-log.html"/>
    <id>https://wangbenjun.site/2022/life/remember-working-log.html</id>
    <published>2022-06-03T15:59:59.000Z</published>
    <updated>2024-12-29T03:02:14.813Z</updated>
    
    <content type="html"><![CDATA[<p>最近查看手机日记的时候发现以前写的旅行随记，里面记录了那一年我去过的所有地方，依稀记得那是2015年，刚过完春节！从3月到10月，我一个人从东南走到西北，走过十几个城市。这里记一下就当是留恋吧，别无他意！</p><a id="more"></a><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br><span class="line">94</span><br><span class="line">95</span><br><span class="line">96</span><br><span class="line">97</span><br><span class="line">98</span><br><span class="line">99</span><br><span class="line">100</span><br><span class="line">101</span><br><span class="line">102</span><br><span class="line">103</span><br><span class="line">104</span><br><span class="line">105</span><br><span class="line">106</span><br><span class="line">107</span><br><span class="line">108</span><br><span class="line">109</span><br><span class="line">110</span><br><span class="line">111</span><br><span class="line">112</span><br><span class="line">113</span><br><span class="line">114</span><br><span class="line">115</span><br><span class="line">116</span><br><span class="line">117</span><br><span class="line">118</span><br><span class="line">119</span><br><span class="line">120</span><br><span class="line">121</span><br><span class="line">122</span><br><span class="line">123</span><br><span class="line">124</span><br><span class="line">125</span><br><span class="line">126</span><br><span class="line">127</span><br><span class="line">128</span><br><span class="line">129</span><br><span class="line">130</span><br><span class="line">131</span><br><span class="line">132</span><br><span class="line">133</span><br><span class="line">134</span><br><span class="line">135</span><br><span class="line">136</span><br><span class="line">137</span><br><span class="line">138</span><br><span class="line">139</span><br><span class="line">140</span><br><span class="line">141</span><br><span class="line">142</span><br><span class="line">143</span><br><span class="line">144</span><br><span class="line">145</span><br><span class="line">146</span><br><span class="line">147</span><br><span class="line">148</span><br><span class="line">149</span><br><span class="line">150</span><br><span class="line">151</span><br><span class="line">152</span><br><span class="line">153</span><br><span class="line">154</span><br><span class="line">155</span><br><span class="line">156</span><br><span class="line">157</span><br><span class="line">158</span><br><span class="line">159</span><br><span class="line">160</span><br><span class="line">161</span><br><span class="line">162</span><br><span class="line">163</span><br><span class="line">164</span><br><span class="line">165</span><br><span class="line">166</span><br><span class="line">167</span><br><span class="line">168</span><br><span class="line">169</span><br><span class="line">170</span><br><span class="line">171</span><br><span class="line">172</span><br><span class="line">173</span><br><span class="line">174</span><br><span class="line">175</span><br><span class="line">176</span><br><span class="line">177</span><br><span class="line">178</span><br><span class="line">179</span><br><span class="line">180</span><br><span class="line">181</span><br><span class="line">182</span><br><span class="line">183</span><br><span class="line">184</span><br><span class="line">185</span><br><span class="line">186</span><br><span class="line">187</span><br><span class="line">188</span><br><span class="line">189</span><br><span class="line">190</span><br><span class="line">191</span><br><span class="line">192</span><br><span class="line">193</span><br><span class="line">194</span><br><span class="line">195</span><br><span class="line">196</span><br><span class="line">197</span><br><span class="line">198</span><br><span class="line">199</span><br><span class="line">200</span><br><span class="line">201</span><br><span class="line">202</span><br><span class="line">203</span><br><span class="line">204</span><br><span class="line">205</span><br><span class="line">206</span><br><span class="line">207</span><br><span class="line">208</span><br><span class="line">209</span><br><span class="line">210</span><br><span class="line">211</span><br><span class="line">212</span><br><span class="line">213</span><br></pre></td><td class="code"><pre><span class="line">旅行随记</span><br><span class="line">3月:</span><br><span class="line">19号上午合肥8点15飞机去厦门，随后去鼓浪屿，下午逛了一圈！</span><br><span class="line">20号逛了鼓浪屿五大景点！</span><br><span class="line">21号离开鼓浪屿，到达厦门，下午去曾厝垵，厦大逛了一圈！</span><br><span class="line">22号下午骑行厦门环岛路，风景如画！</span><br><span class="line">23号上午乘车前往仙游县，车费70，耗时2个半，下午采集</span><br><span class="line">24号在仙游采集</span><br><span class="line">25号在仙游采集</span><br><span class="line">26号正仙游采集</span><br><span class="line">27号中午乘车前往永春县，车费25 ，耗时1个半小时，下午采集</span><br><span class="line">28号在永春县采集</span><br><span class="line">29号在永春县采集</span><br><span class="line">30号早上前往漳平市，车费54，耗时2小时，下午采集</span><br><span class="line">31号在漳平市采集</span><br><span class="line"></span><br><span class="line">4月:</span><br><span class="line">01号上午7点汽车前往永安市，车费34，耗时2个半小时，下午采集</span><br><span class="line">02号在永安市采集</span><br><span class="line">03号凌晨3点火车前往古田县，火车费18，转费用11，耗时4小时，下午采集</span><br><span class="line">04号中午坐车前往屏南县，车费21，耗时2个小时，下午采集</span><br><span class="line">05号采集</span><br><span class="line">06号早上7点汽车前往政和县，车费32，耗时3小时，下午采集</span><br><span class="line">07号早上8点汽车前往松溪县，车费10，耗时1小时，下午采集</span><br><span class="line">08号上午7点半起床前往上饶市，车费68，耗时4小时。然后坐火车，车费19，去鹰潭市，晚11点火车去合肥，车费98。</span><br><span class="line">09号下午3点火车到合肥，下午5点多到家！</span><br><span class="line">10号在家休息</span><br><span class="line">11号在三十铺采集</span><br><span class="line">12号在三十铺采集</span><br><span class="line">13号在家休息</span><br><span class="line">14号在苏埠、毛坦厂采集</span><br><span class="line">15号在六安采集</span><br><span class="line">16号在家休息</span><br><span class="line">17号去合肥</span><br><span class="line">18号在庐江采集</span><br><span class="line">19号换手机，下雨</span><br><span class="line">20号在合肥采集</span><br><span class="line">21号在合肥采集，晚上火车去黄山，7小时车程</span><br><span class="line">22号到达黄山，下午游玩凤凰源</span><br><span class="line">23号游玩黄山</span><br><span class="line">24号上午游玩宏村，下午采集，去黄山市</span><br><span class="line">25号在黄山市采集</span><br><span class="line">26号在黄山市采集</span><br><span class="line">27号在黄山市采集</span><br><span class="line">28号上午去徽州区采集，车费5，车程半小时，采集一天</span><br><span class="line">29号上午去开化县，车费38，车程2小时，下午采集</span><br><span class="line">30号上午去江山市，车费25，车程1个多小时，采集一会去了浦城，车费41，车程2小时多</span><br><span class="line"></span><br><span class="line">5月份:</span><br><span class="line">1号上午去松溪，车费26，耗时2小时，转车去庆元，车费15，又转车去寿宁县，车费29</span><br><span class="line">2号上午前往泰顺县，车费26，耗时2小时，下午采集</span><br><span class="line">3号上午前往福鼎市，车费29 耗时2小时，下午采集</span><br><span class="line">4号在福鼎市采集</span><br><span class="line">5号在福鼎市采集</span><br><span class="line">6号上午火车前往霞浦县，后又上火车前往南靖县，路费145，下午采集2小时</span><br><span class="line">7号南靖县采集</span><br><span class="line">8号上午去田螺坑土楼，花费150，下午到平和县采集</span><br><span class="line">9号在平和县采集</span><br><span class="line">10号上午动车前往深圳，车费120，休息</span><br><span class="line">11号深圳休息</span><br><span class="line">12号前往香港，下午采集一点点</span><br><span class="line">13号在香港玩</span><br><span class="line">14号在香港玩</span><br><span class="line">15号离开香港返回深圳</span><br><span class="line">16号在深圳</span><br><span class="line">17号在深圳</span><br><span class="line">18号回家，行程20小时</span><br><span class="line">19号下午到达合肥</span><br><span class="line">20号在合肥休息</span><br><span class="line">21号在合肥休息</span><br><span class="line">22号在合肥休息</span><br><span class="line">23号回家</span><br><span class="line">24号在家休息</span><br><span class="line">25号在家休息</span><br><span class="line">26号在家休息</span><br><span class="line">27号在家休息</span><br><span class="line">28号在家休息</span><br><span class="line">29号在家休息</span><br><span class="line">30号在家休息</span><br><span class="line">31号在家休息</span><br><span class="line"></span><br><span class="line">6月</span><br><span class="line">1号在家休息</span><br><span class="line">2号在家休息</span><br><span class="line">3号在家休息</span><br><span class="line">4号去合肥，车费25</span><br><span class="line">5号火车去齐齐哈尔，车费260</span><br><span class="line">6号晚上到达齐齐哈尔市</span><br><span class="line">7号前往富裕县，车费15，下雨一天</span><br><span class="line">8号在富裕县采集</span><br><span class="line">9号在富裕县采集</span><br><span class="line">10号早上前往依安县，采集一天</span><br><span class="line">11号在依安县，采集一天</span><br><span class="line">12号前往克山县，车费5块钱，采集一天</span><br><span class="line">13号前往克东县，车费9块钱，采集一天</span><br><span class="line">14号前往拜泉县，车费14块钱，采集一天</span><br><span class="line">15号前往明水县，车费15块钱，采集一天</span><br><span class="line">16号在明水县，采集一天</span><br><span class="line">17号前往青冈县，车费17块钱，采集一天</span><br><span class="line">18号在青冈县，采集一天</span><br><span class="line">19号前往望奎县，车费13块钱，采集一天</span><br><span class="line">20号正望奎县，采集一天</span><br><span class="line">21号在望奎县，采集一天</span><br><span class="line">22号前往绥化，车费13块钱，采集半天</span><br><span class="line">23号前往抚远，车费180块钱，下午采集小半天</span><br><span class="line">24号在抚远，下午采集小半天</span><br><span class="line">25号在抚远，下午采集半天</span><br><span class="line">26号在抚远玩</span><br><span class="line">27号在抚远玩</span><br><span class="line">28号在抚远玩</span><br><span class="line">29号前往寒葱沟镇，车费11，下午采集500个</span><br><span class="line">30号前往前锋镇+前进镇，车费20，采集900个</span><br><span class="line"></span><br><span class="line">7月份</span><br><span class="line">1号在建三江采集一天，车费11，采集700个</span><br><span class="line">2号在前往南岔，再转往带岭，车费55块，在带岭，朗乡，金山屯采集1000个，最后晚上前往西林</span><br><span class="line">3号在西林，美溪，采集1050个</span><br><span class="line">4号前往伊春市，车费12，采集800个</span><br><span class="line">5号在伊春市采集1230个</span><br><span class="line">6号前往铁力市，车费34，采集900个</span><br><span class="line">7号在铁力市采集900个</span><br><span class="line">8号前往大连，车费450，大坑</span><br><span class="line">9号在大连采集100个，晚上火车回庆安县，车费260</span><br><span class="line">10号到达庆安，下午采集800</span><br><span class="line">11号在庆安县采集1300个</span><br><span class="line">12号前往绥棱县，车费20，采集1000个</span><br><span class="line">13号在绥棱县，采集700个</span><br><span class="line">14号前往北安市，采集930个</span><br><span class="line">15号前往五大连池一趟，路费50，回来接着采集，1000个</span><br><span class="line">16号前往齐齐哈尔市，车费50，采集300个</span><br><span class="line">17号在昂昂溪区采集200个</span><br><span class="line">18号在富拉尔基区采集800个</span><br><span class="line">19号前往龙江县，路费10块钱，采集900个</span><br><span class="line">20号前往满洲里，路费90块</span><br><span class="line">21号在满洲里闲逛，采集300个</span><br><span class="line">22号在满洲里闲逛，采集50个，晚上火车离开</span><br><span class="line">23号到达哈尔滨，短暂停留，火车前往天津</span><br><span class="line">24号到达天津，车费155</span><br><span class="line">25号在天津</span><br><span class="line">26号前往大同，车费63，后前往包头，车费65，采集100张</span><br><span class="line">27号前往五原县，采集600张</span><br><span class="line">28号前往乌拉特中旗，车费17，采集600张</span><br><span class="line">29号在乌拉特中旗采集1100个</span><br><span class="line">30号前往五原县，车费17+10块钱，采集950个</span><br><span class="line">31号前往磴口县，车费15块钱，采集1300个</span><br><span class="line"></span><br><span class="line">8月份:</span><br><span class="line">1号前往乌海西，车费20，采集900个</span><br><span class="line">2号前往乌拉特后旗，车费35，采集200个</span><br><span class="line">3号在乌拉特后旗，杭后车费20，采集1050个</span><br><span class="line">4号前往额济纳旗，车费95，采集300个</span><br><span class="line">5号在额济纳旗，采集700个</span><br><span class="line">6号在前往酒泉，嘉峪关，车费93+53</span><br><span class="line">7号在敦煌玩</span><br><span class="line">8号在敦煌玩，采集500个</span><br><span class="line">9号夜前往乌鲁木齐，车费130</span><br><span class="line">10号到达乌鲁木齐，闲玩一天</span><br><span class="line">11号前往玛纳斯县，车费32 采集100个</span><br><span class="line">12号前往石河子，车费5块钱，采集400个</span><br><span class="line">13号前往伊犁，车费69块钱</span><br><span class="line">14号在伊犁玩</span><br><span class="line">15号前往那拉提，车费50</span><br><span class="line">16号在那拉提玩</span><br><span class="line">17号前往新源县，车费20，采集650个</span><br><span class="line">18号前往和静县，车费117，采集200个</span><br><span class="line">19号在和静县采集，700个</span><br><span class="line">20号前往库尔勒市，车费25，做公交任务</span><br><span class="line">21号在库尔勒市</span><br><span class="line">22号在库尔勒市</span><br><span class="line">23号在库尔勒市</span><br><span class="line">24号前往格尔木市， 车费300，耗时24小时</span><br><span class="line">25号达到格尔木市</span><br><span class="line">26号前往拉萨，车费141，耗时11小时</span><br><span class="line">27号在拉萨</span><br><span class="line">28号在拉萨</span><br><span class="line">29号前往林芝地区，车费80，耗时9小时</span><br><span class="line">30号在林芝地区</span><br><span class="line">31号在林芝地区</span><br><span class="line"></span><br><span class="line">9月</span><br><span class="line">01号前往鲁朗，车费50，徒步5小时，到达东久</span><br><span class="line">02号前往排龙，徒步7小时</span><br><span class="line">03号在排龙</span><br><span class="line">04号在晚前往波密，搭车</span><br><span class="line">05号在波密休息</span><br><span class="line">06号前往然乌湖，途搭车费30</span><br><span class="line">07号前往邦达镇，途搭车费20</span><br><span class="line">08号前往左贡，芒康，途搭车费60</span><br><span class="line">09号前往德钦，途搭</span><br><span class="line">10号前往香格里拉，途搭车费25</span><br><span class="line">11号前往丽江，车费60</span><br><span class="line">12号在丽江玩</span><br><span class="line">13号在丽江玩</span><br><span class="line">14号在丽江玩</span><br><span class="line">15号在丽江玩</span><br><span class="line">16号前往双廊</span><br><span class="line">17号在双廊玩</span><br><span class="line">18号前往大理古城</span><br><span class="line">19号前往昆明</span><br><span class="line">20号在昆明采集300</span><br><span class="line">21号在昆明采集300</span><br><span class="line">22号在昆明玩</span><br><span class="line">23号在昆明玩</span><br><span class="line">24号坐飞机回合肥</span><br><span class="line">25号在合肥</span><br><span class="line">26号回家</span><br><span class="line">27号在家</span><br><span class="line">28号在家</span><br><span class="line">29号在家</span><br><span class="line">30号在家</span><br><span class="line">10月01号～10月19号在家</span><br><span class="line">10月20号～11月15号在合肥</span><br><span class="line">11月16号～11月30号在北京</span><br></pre></td></tr></table></figure>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;最近查看手机日记的时候发现以前写的旅行随记，里面记录了那一年我去过的所有地方，依稀记得那是2015年，刚过完春节！从3月到10月，我一个人从东南走到西北，走过十几个城市。这里记一下就当是留恋吧，别无他意！&lt;/p&gt;
    
    </summary>
    
    
      <category term="Life" scheme="https://wangbenjun.site/categories/Life/"/>
    
    
      <category term="Life" scheme="https://wangbenjun.site/tags/Life/"/>
    
  </entry>
  
  <entry>
    <title>Git合并|修改历史提交记录（rebase）</title>
    <link href="https://wangbenjun.site/2022/coding/git-rebase.html"/>
    <id>https://wangbenjun.site/2022/coding/git-rebase.html</id>
    <published>2022-05-16T09:00:00.000Z</published>
    <updated>2024-12-29T03:02:14.808Z</updated>
    
    <content type="html"><![CDATA[<p>Git的rebase命令是一个非常强大的命令，关于这个命令的介绍文章网上很多，但是大多理论性比较强，所以我这篇文章结合几个非常典型的应用场景来谈一下rebase命令的使用。</p><p>说实话，rebase这个操作极少用到，大部分时候老老实实merge就行了。说到rebase这个单词，有些文章翻译成“变基”，实在有些生硬，但是从字面意思上看base有着基础的含义，比如说我们每次修改代码的时候是基于上一次的基础，这是一个线性的过程，而rebase则是打破这个线性过程，改变变更的基础，达到一些特殊的目的。</p><a id="more"></a><h2 id="1-合并提交记录"><a href="#1-合并提交记录" class="headerlink" title="1.合并提交记录"></a>1.合并提交记录</h2><p>通常情况下，我们每次对代码进行一些修改都会commit一下，时间久了，提交的记录就非常多，比如下面图中：</p><img src="/images/2022/gitlog.png"/><p>个人对此并不反感，很多时候提交记录详细一点方便以后排查bug、甚至甩锅，谁的提交记录就是谁写的代码、做的修改，一目了然，这玩意也不占多少空间，但是有些特殊情况，比如对外开源的项目在发布的时候希望有一个干净的commit记录。</p><p>或者，纯粹是你的leader搞事情，觉得提交记录这么多看着不舒服。。。anyway，这时候使用rebase命令就可以轻松搞定。</p><p>整体过程可以分为几步：</p><h3 id="第一步，获取commit-id"><a href="#第一步，获取commit-id" class="headerlink" title="第一步，获取commit id"></a>第一步，获取commit id</h3><p>比如上图里面这些提交记录，我们想把前面4次记录合并到“重构”这次，那么我们就需要先获取“重构”这个提交记录 <strong>前面</strong> 那一次提交的commit id，就是那一串hash值，使用<code>git log</code>命令：</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">commit 01da1d7351dfea156eabc7ddf36a9e8eba227645 (tag: v1.1)</span><br><span class="line">Author: wangbenjun &lt;wangbenjun@gmail.com&gt;</span><br><span class="line">Date:   Fri Nov 19 22:00:16 2021 +0800</span><br><span class="line"></span><br><span class="line">    重构</span><br><span class="line"></span><br><span class="line">commit a0645baeb9938f34658f2766a2d4ec2855791da1 (tag: v1.0)</span><br><span class="line">Author: wangbenjun &lt;wangbenjun@gmail.com&gt;</span><br><span class="line">Date:   Wed Aug 18 20:47:34 2021 +0800</span><br><span class="line"></span><br><span class="line">    优化目录结构</span><br></pre></td></tr></table></figure><h3 id="第二步，git-rebase-i-a0645baeb"><a href="#第二步，git-rebase-i-a0645baeb" class="headerlink" title="第二步，git rebase -i a0645baeb"></a>第二步，git rebase -i a0645baeb</h3><p>git rebase这个命令参数很多，这里就不多介绍，不然又有点overwhelming了，感兴趣的童鞋自行研究，这里只说-i，参数是commit id，填一部分就行。</p><p>然后会自动打开一个编辑页面，可能是nano，也可能是vi，取决于你系统的配置。</p><img src="/images/2022/gitrebase.png"/><p>很多人一看到这个多内容可能就有点慌了，不知所措，其实大部分都是注释，只有最上面那几行是有用的，可以看到默认是 <code>pick xxx</code>，这个pick啥意思？</p><p>其实下面就有介绍，p表示use commit，意思就是采用这个提交，然后还有r、e、s、f等命令，这里咱也不多说，只看s，这个s表示采用提交并合并到上一次提交里面。</p><p>然后我们需要把重构后面的记录前面的pick全部改成s，意思是把后面4个提交合并到前面那一次提交里面。</p><h3 id="第三步，注释并保存"><a href="#第三步，注释并保存" class="headerlink" title="第三步，注释并保存"></a>第三步，注释并保存</h3><p>把pick改成s之后保存，又会自动弹出一个新的编辑页面，内容大概如下：</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#</span><span class="bash"> This is a combination of 5 commits.</span></span><br><span class="line"><span class="meta">#</span><span class="bash"> This is the 1st commit message:</span></span><br><span class="line"></span><br><span class="line">重构</span><br><span class="line"></span><br><span class="line"><span class="meta">#</span><span class="bash"> This is the commit message <span class="comment">#2:</span></span></span><br><span class="line"></span><br><span class="line">fix</span><br><span class="line"></span><br><span class="line"><span class="meta">#</span><span class="bash"> This is the commit message <span class="comment">#3:</span></span></span><br><span class="line"></span><br><span class="line">update</span><br><span class="line"></span><br><span class="line"><span class="meta">#</span><span class="bash"> This is the commit message <span class="comment">#4:</span></span></span><br><span class="line"></span><br><span class="line">fix</span><br></pre></td></tr></table></figure><p>其大意是这是这5次提交信息的合并，然后它会自动把每次提交的信息都给你列出来，这里你可以选择直接保存，也可以顺便修改一下，改成你想要的内容，然后保存。</p><h3 id="第四步，推送到远程"><a href="#第四步，推送到远程" class="headerlink" title="第四步，推送到远程"></a>第四步，推送到远程</h3><p>完成上面的操作之后，再次使用git log则会发现提交记录已经变了，但是目前还只是在本地，还需要推送到远程，而且推送的时候必须使用–force，如：</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">git push origin --force</span><br></pre></td></tr></table></figure><p>完成上述操作之后，整个git提交记录就会完全改变了，而且是不可逆操作，当然这也意味着rebase操作是有一定风险的，如果你不太清楚需要做什么的话就不要做。</p><hr><p>结合以上例子，大家可能会发现rebase是可以拥有修改提交记录的能力，所以基于此，我们可以衍生出以下应用场景：</p><h2 id="2-删除敏感信息"><a href="#2-删除敏感信息" class="headerlink" title="2.删除敏感信息"></a>2.删除敏感信息</h2><p>假如你们公司某个傻屌把一些敏感信息，比如账号密码上传到git上面去了，你的第一反应可能是删掉密码再提交一次。。。然而这有点掩耳盗铃的意思，历史提交记录里面还有呢</p><img src="/images/2022/gitpassword.png"/><p>这时候你就可以使用rebase修改提交记录，把密码出现过的地方全部抹除掉，相当于没有发生过，过程和上述一样。</p><h2 id="3-偷别人的代码"><a href="#3-偷别人的代码" class="headerlink" title="3.偷别人的代码"></a>3.偷别人的代码</h2><p>假如你们公司按代码提交量算绩效，你这周摸鱼没有写代码，但是你同事写了很多代码，你可以使用rebase把别人的提交记录修整一番，然后你同事的提交记录就没了，变成你的了…</p><p>哈哈，此招太贱，可能会挨打，注意使用场景！</p><h2 id="4-清理提交记录"><a href="#4-清理提交记录" class="headerlink" title="4.清理提交记录"></a>4.清理提交记录</h2><p>有些同学在使用git的时候不知道有stash这个功能，所以每次切换分支的时候都喜欢commit，导致这个commit记录超级多，而且很多无意义的信息，虽然无大雅，但是有些追求极致的leader看不下去，<br>比如：</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">fix bug</span><br><span class="line">fix bug</span><br><span class="line">fix bug</span><br><span class="line">fix bug</span><br><span class="line">...</span><br></pre></td></tr></table></figure><p>或者有时候在调试代码的时候，必须提交到代码库才能部署，也会导致出现很多重复无意义的提交，比较难看。</p><p>这时候也可以使用reabse来合并这部分提交，值得一说的是<code>rebase -i</code>支持区间，从某一个commit到另一个commit。</p><h2 id="5-合并代码"><a href="#5-合并代码" class="headerlink" title="5.合并代码"></a>5.合并代码</h2><p>rebase本身也可以用于合并代码，比如说你在一个开发分支dev上面开发，啪啪啪一堆提交记录，开发完成之后，你想往master合并，如果直接merge过去会导致主分支看上去也很杂乱，碰到一个事多的leader可能又不爽你了，这时候你咋办呢？</p><p>这时候你也可以使用rebase，只不过这时候的base是主分支，不是commit id了，比如：</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">git rebase -i master</span><br></pre></td></tr></table></figure><img src="/images/2022/gitpull.png"/><p>顺便说一下，git pull的时候默认自动merge，其实还可以选择rebase，这时候rebase的作用就是类似合并，只不过在时间线上面有差异。</p><p>最后，总结一下，那就是rebase很强很厉害，可以随意操纵历史提交记录，实现特殊的需求，不知道上面举的这些例子有没有你刚好用到的。</p>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;Git的rebase命令是一个非常强大的命令，关于这个命令的介绍文章网上很多，但是大多理论性比较强，所以我这篇文章结合几个非常典型的应用场景来谈一下rebase命令的使用。&lt;/p&gt;
&lt;p&gt;说实话，rebase这个操作极少用到，大部分时候老老实实merge就行了。说到rebase这个单词，有些文章翻译成“变基”，实在有些生硬，但是从字面意思上看base有着基础的含义，比如说我们每次修改代码的时候是基于上一次的基础，这是一个线性的过程，而rebase则是打破这个线性过程，改变变更的基础，达到一些特殊的目的。&lt;/p&gt;
    
    </summary>
    
    
      <category term="Coding" scheme="https://wangbenjun.site/categories/Coding/"/>
    
    
      <category term="Git" scheme="https://wangbenjun.site/tags/Git/"/>
    
  </entry>
  
  <entry>
    <title>阿里外包能转正吗？</title>
    <link href="https://wangbenjun.site/2022/life/ali-waibao-more.html"/>
    <id>https://wangbenjun.site/2022/life/ali-waibao-more.html</id>
    <published>2022-05-04T13:00:00.000Z</published>
    <updated>2024-12-29T03:02:14.813Z</updated>
    
    <content type="html"><![CDATA[<p>作为一名在阿里干了2年多外包的人应该具有一定的发言权，之前有写过几篇关于阿里外包的文章，感兴趣的童鞋可以往前翻一下。如今差不多2年过去了，突然想起来继续更新一下近况，首先告诉大家的是，我还在外包，没错，我还在干外包。</p><p>不过这篇文章我想谈一个话题，那就是“阿里外包是否能转正？” 因为转正这个事情发生在我身上过，但是却又没有成功。</p><p>不过在此之前，我先简单说一下这2年发生的一些事情，算是一些铺垫</p><h2 id="1-一些变化"><a href="#1-一些变化" class="headerlink" title="1.一些变化"></a>1.一些变化</h2><p>在这2年多内，我换过多个阿里主管，最早那个阿里主管跑路了，然后我就换了另一位主管了，但是没多久这个主管又转岗了，于是我又转移到另外一个主管下面。</p><p>在阿里这2年内，我认识的正式工里面离职的人1只手数不下来，后来得知在阿里一般3年多可以升到p7，再往上面升p8就非常困难了，而这个时候基本上股票该拿的也拿完了，所以想涨工资大多只能通过跳槽。</p><a id="more"></a><p>虽然换过几位主管，但是我负责的那个系统一直是我在维护开发，在我这2年里面部门里面找我做过东西的人不下十几个，也算是涉猎广泛，虽然并不是什么核心的功能。</p><p>而我和另外一个一起来的同事已经成为部门外包里面最老的员工，我们常常戏说再干1年的话，我们可能要成为比很多正式工还要老了。。。</p><p>2021年的时候部门里面走了很多外包，同时又招了很多外包新人，但是其实并没有太多的活要做，他们主管和他们说后面会有活的，阿里招这么多人只是为了备用，不可能需要人的时候再去招，那样来不及，其实说白了就是部门刚好有用人名额，不用白不用而已。</p><p>然而。。。最近我得知，部门开始裁员了，其实这就是外包的尴尬之处，当部门用人名额缩减的时候，第一个被裁员的就是外包，而且外包也不存在裁员的说法，因为对于阿里用人方来说，它只需要和外包公司说自己不需要人了，外包公司就会对你进行“下线”。</p><p>有人可能会好奇，外包裁员有没有赔偿？比如N+1。答案是没有的，这几乎是外包的潜规则，很多公司使用外包这种用工形式就是为了节省成本，这个裁员和阿里一点关系都没有。一般情况下外包公司会给你推荐其它外包岗位，让你去面试，因为外包岗位是一个萝卜一个坑，或者你自己去投简历面试，这个时间大概是1个月，期间只会给你发一个基本工资，大概只有几千块钱。</p><p>期间，我还涨过一次薪水，大概是我工作年限的月数*100，虽然不多，但是也算是有点涨幅，另外年终奖拿了差不多半个月左右。说句实话，经过我一些外包同事的交流，在阿里这些外包公司供应商里面，中软科技算是很良心了。</p><h2 id="2-转正的饼"><a href="#2-转正的饼" class="headerlink" title="2.转正的饼"></a>2.转正的饼</h2><p>其实2020年初我刚入职的时候，最早招我进来那个主管就和我说，部门招外包也是一种筛选人才的方式，如果你表现的好的话是可以有转正的机会的，听上去像是一个饼，但是确实发生了一些事情。</p><p>2021年中旬，我们部门老大，P8，找到我，直接开门见山问我想不想进阿里成为正式工？说实话，我有点受宠若惊，也很好奇，因为工作这么久我和他几乎没说过话，他可能都不知道我是谁，怎么会突然找到我，还让我转正，还有这种好事送上门？</p><p>他说我的主管和同事反馈说我工作表现很不错，认真负责。这种好事当然我不会拒绝（虽然后面事实证明没有什么事是真正的好事），但是我也把我的顾虑和他说了，我说阿里要求很高吧，我这也没准备面试怕过不了，他说没事，前面几面都是我们部门的人，让我尝试一下，即使不成功也没什么影响，还可以继续上班，话说他说的也没毛病。</p><p>当时很多人都这么认为，既然部门大老板推荐的我，大家应该都会给个面子，面试或许只是走一个流程而已，转正式只是时间问题。</p><p>然后我就收到了一个内推邀请邮件，开始了阿里的社招面试流程，前面几面都是部门内的人，因为平时也算是熟人，几乎也没怎么为难我，都给过了，最后一面是BU的技术老大，个人感觉发挥的不是很好。</p><p>后来过了几天之后，我专门问了一下他面试的情况，他说还没凉，在走流程，按理说下一步应该是HR这边吧。但是接下来就是漫长的等待了，我问过几次主管，每次都说让我耐心等待，但是从来没有透露到什么阶段，或者卡点在哪，我也不好意思细问，只能相信是阿里流程慢，期间还拒绝了以前同事抛来的橄榄枝，现在想想确实太naive了。。。</p><p>这一等直接等到快过年了，我们主管这时候和我说，年前hc锁了。。。得等年后了，这时候其实我已经大概猜到是什么情况了。</p><h2 id="3-直管用工"><a href="#3-直管用工" class="headerlink" title="3.直管用工"></a>3.直管用工</h2><p>其实我早就听说过阿里直管，直管用工是阿里最近在搞的一个新的用工模式，说白了就是高级外包，直管就是直接管理的意思，和普通外包区别就在于可以和阿里正式工坐在一起办公，享受一样的管理方式（PUA）。</p><p>除了在管理方式上面有一点区别之外，其它情况，比如集团权限和普通外包无异，毕竟连工号都是一样WB开头，签合同的公司是杭州xxxx。但是据阿里方说直管比普通外包好，因为可以学到的东西更多，而且转正的几率大大增大。。。这似乎又是另一个转正饼。</p><p>2022年开年之后又等了很久，阿里主管终于和我开门见山的谈了谈，告诉我转正这事现在有点难，大环境很差，公司hc不多，即使有hc可能也轮不到我，所以想让我先转直管，后面再看机会。其实我也理解他的说辞，现在就业环境确实很差，hc确实也不多，部门确实有招新人，但是并不是我，这么宝贵的名额我确实不配。</p><p>但是让我不爽的事情，这件事拖了很久很久，没有一丁点消息，其实可以早点和我说清楚。。。细思极恐，可能他们预谋很久了，也许我的面试早就被BU老大否了，但是他们怕我离职只好先拖着我，等着转直管这个事情推进，但也可能他们真的努力了很久，实在找不到机会。</p><p>事情经过的真相不得而知，但是这个结果我去年就已经猜想过，最终确实如我所料，但也谈不上什么惊讶，甚至还有点解脱的感觉，毕竟这件事悬而未决这么久总算落地了，也不用再去想了。</p><h2 id="4-到底能不能转正？"><a href="#4-到底能不能转正？" class="headerlink" title="4.到底能不能转正？"></a>4.到底能不能转正？</h2><p>说到这里，我突然想起来一件事，之前一个外包同事在去年底成功面试拿到一家大厂offer，上岸了，外包毕竟不是什么非常有前景的工作，想要更好的工作还是得看自己的努力，不要把希望寄托在别人身上。</p><p>如果你问我想不想转正，我当然想啊，毕竟转正最明显的变化就是福利待遇要好很多，而且相关权限也多了，可以接触更多东西了。</p><p>严格来说，不存在“转正”这个说法，因为外包并没有“转正式”这个操作，其实还是需要走社招面试流程，该有的流程都有，一个少不了，外包这段经历只能算是一个敲门砖，如果你的硬性条件不符合，那还是没戏，比如学历、背景，众所周知，阿里HR享有一票否决权。</p><p>如果你非要问是否能转正，答案是可以，但是这个所谓转正式工更像是给外包画的一个饼、一个诱饵，真实几率非常低，个人觉得不足5%，取决于天时地利人和，或许你运气好，碰到一个好主管，就是想提携你，力推你转正，帮你背书，事在人为，基本上都能成。或许你接手了部门核心业务项目，成为不可替代的一环，除了你没人会，那转正也只是时间问题。或许你解决了项目里面连正式工都无法解决的难点痛点，在大老板面前表现一番，那转正也只是分分钟钟的事情。</p><p>回到最初的问题，外包到底能不能转正，我的答案不要抱着转正的希望来干外包，其实最初我干外包也没想过转正这事，外包最大的意义还是图个轻松、不加班、不用背绩效，除此，无它，一个字：混就行了。</p><blockquote><p>2024年更新一下，由于经济和行业不景气，去年阿里外包也大裁员了，据说很多部门外包“裁员”比例都有50%了，留下来的都是老鸟，而且现在外包在考勤、任务量、代码量等方面要求很严格了，可没以前那么轻松了。前几年行情好的时候外包转正式可能还多一点，这几年正编都在裁员收缩…..</p></blockquote>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;作为一名在阿里干了2年多外包的人应该具有一定的发言权，之前有写过几篇关于阿里外包的文章，感兴趣的童鞋可以往前翻一下。如今差不多2年过去了，突然想起来继续更新一下近况，首先告诉大家的是，我还在外包，没错，我还在干外包。&lt;/p&gt;
&lt;p&gt;不过这篇文章我想谈一个话题，那就是“阿里外包是否能转正？” 因为转正这个事情发生在我身上过，但是却又没有成功。&lt;/p&gt;
&lt;p&gt;不过在此之前，我先简单说一下这2年发生的一些事情，算是一些铺垫&lt;/p&gt;
&lt;h2 id=&quot;1-一些变化&quot;&gt;&lt;a href=&quot;#1-一些变化&quot; class=&quot;headerlink&quot; title=&quot;1.一些变化&quot;&gt;&lt;/a&gt;1.一些变化&lt;/h2&gt;&lt;p&gt;在这2年多内，我换过多个阿里主管，最早那个阿里主管跑路了，然后我就换了另一位主管了，但是没多久这个主管又转岗了，于是我又转移到另外一个主管下面。&lt;/p&gt;
&lt;p&gt;在阿里这2年内，我认识的正式工里面离职的人1只手数不下来，后来得知在阿里一般3年多可以升到p7，再往上面升p8就非常困难了，而这个时候基本上股票该拿的也拿完了，所以想涨工资大多只能通过跳槽。&lt;/p&gt;
    
    </summary>
    
    
      <category term="Life" scheme="https://wangbenjun.site/categories/Life/"/>
    
    
      <category term="Life" scheme="https://wangbenjun.site/tags/Life/"/>
    
  </entry>
  
  <entry>
    <title>小米手机MIUI ROOT解除温控、火力全开</title>
    <link href="https://wangbenjun.site/2022/linux/fuck-miui.html"/>
    <id>https://wangbenjun.site/2022/linux/fuck-miui.html</id>
    <published>2022-04-02T13:00:00.000Z</published>
    <updated>2024-12-29T03:02:14.814Z</updated>
    
    <content type="html"><![CDATA[<p>最近愈发觉得我用了差不多2年的手机红米k20 Pro打开应用速度反应很慢，偶尔还会有卡顿，这款手机搭载了高通骁龙855plus，8核CPU，最高主频2.96GHz，即使放在今天性能也不是很差，我平时很少拿来玩游戏，只是日常使用。</p><p>虽说手机用久了都会变卡，毕竟电子元器件存在性能衰减，不过问题其实也不是这么简单。MIUI系统很早就有省电模式、均衡模式、性能模式，熟悉Linux系统的人都知道这和Linux的CPU调度策略非常类似，当然这是废话，因为安卓内核就是Linux。。。</p><p>手机系统为了平衡功耗以及温度，会有很多种CPU调度策略，MIUI在平时使用的时候会根据运行软件的不同去执行不同的调度策略，比如一些日常软件的话主频会限制到2GHz，但打游戏的时候往往火力全开，但是火力全开手机温度就会上升，达到一定程度就会触发温控策略导致CPU降频，从而引起卡顿。</p><p>我这个机型的系统版本还取消了性能模式，默认以均衡模式运行，再加上小米保守的温控策略，根本没法发挥全部实力，作为一个技术宅已经忍了很久了，既然没钱换新手机那就动手解除封印，释放全部潜能再战1年！所以接下来，我会把这台手机root、之后再安装Scene4解锁来CPU限制，火力全开，最后再删除温控策略，尽可能的释放手机性能，提高流畅度。</p><p>理论上，手机CPU和GPU也能超频，不过这个玩法就太高端了，而且恐怕散热真的撑不住。。。</p><p>本人在折腾过程中也查看了不少教程，但是都比较零散细碎，所以这里总结了一下，以下教程适用于MIUI 12，虽然我的机型可能和大家不一样，但是大同小异，仅供参考。</p><a id="more"></a><h2 id="误区"><a href="#误区" class="headerlink" title="误区"></a>误区</h2><p>因为用了很多年小米手机，以前小米手机开发版默认自带root，所以印象中以为只要刷个开发版就可以了，谁知道没那么简单，还需要解锁boot才行。</p><p>然后才发现稳定版也可以root，只是方法和开发版不太一样。</p><p>所以本人建议使用稳定版root，因为小米的开发版不是很稳，很可能有bug，而且更新比较频繁，更新的时候还可能会丢掉root权限。</p><h2 id="1-解锁bootloader"><a href="#1-解锁bootloader" class="headerlink" title="1.解锁bootloader"></a>1.解锁bootloader</h2><p>以前小米手机是不锁boot的，比如小米3、小米4可谓刷机神器，那些年也没少折腾，但是时过境迁，现在小米为了安全考虑，都开始锁boot了，需要解锁之后才能刷机。</p><p>小米手机解锁boot还算比较简单，官网有明确的教程和工具提供下载，建议大家移步这里： <a href="https://www.miui.com/unlock/index.html" target="_blank" rel="noopener">https://www.miui.com/unlock/index.html</a></p><img src="/images/2022/unlock.png" /> <p>注意事项： 官网上面写了很多，请查看FAQ，这里简单提几句，比如在售的新机型一般需要等待，用户账号安全评分较低的需要等待，等待时间目前是7天起，如果本年度解锁手机数超过2台，等待时间会相应增长、一个小米账号每月限制解锁一台设备、一个小米账号每年限制解锁4台不同设备。</p><p>另外，这个解锁工具不支持win11…，建议使用win7或者win10。</p><p>先说几句题外话，很多教程会让你先刷第三方的recovery，其实可刷也可以不刷，如果你经常刷机折腾，可以刷一下，如果你像我一样只是为了root，那就没必要刷，小米自带的recovery也能凑合用。</p><h2 id="2-下载官方原版镜像"><a href="#2-下载官方原版镜像" class="headerlink" title="2.下载官方原版镜像"></a>2.下载官方原版镜像</h2><p>这里需要用到官方原版的系统镜像，有2种方式获取，一种是去MIUI官网选择对应机型下载，另一种是打开手机设置-我的设备-MIUI版本-右上角-下载最新完整包，下载完成后会保存在 <strong>Download&#x2F;downloaded_rom</strong> 文件夹。</p><p>使用系统自带的文件管理器找到镜像文件，是一个zip文件，将其解压，你会发现里面有一个名字为 <strong>boot.img</strong> 的文件，不要问我如果没有怎么办，没有那就裂开，这种方法不适合你，Game Over…</p><h2 id="3-安装Magisk"><a href="#3-安装Magisk" class="headerlink" title="3.安装Magisk"></a>3.安装Magisk</h2><p>建议去Github下载最新的版本，或者你可以使用我这保存的版本： <a href="/file/Magisk-v24.3.apk">点击下载 Magisk-v24.3.apk</a></p><p>下载安装到手机，打开APP，你会发现下面这个界面，有一个安装按钮，点击会发现有2种方法，一种是选择并修补一个文件，另一种是直接安装。</p><img width="40%" src="/images/2022/magisk.jpg" /> <p>这里面我们使用选择并修补一个文件，选择刚才那个 <strong>boot.img</strong> 文件，经过一段时间处理之后会在同文件夹生成一个新的文件，名字类似 <strong>magisk_patched-24300_uVoAh.img</strong></p><p>这里多说几句，第二种直接安装可以用于已经root的设备，比如说你是使用MIUI开发版获取的权限。（不推荐这种方式）</p><h2 id="4-使用电脑刷机"><a href="#4-使用电脑刷机" class="headerlink" title="4.使用电脑刷机"></a>4.使用电脑刷机</h2><blockquote><p>说到这，我先展开说一下刷机的2种方式：卡刷和线刷。所谓线刷就是下面我说到这种方式，需要连接电脑来执行命令。而卡刷则是在手机先刷一个第三方recovery然后再刷机，其实recovery就是相当于电脑的BIOS界面，是一个更加底层，用来管理系统的系统。</p></blockquote><blockquote><p>如果采用卡刷的方法的话，那么这里就是先用电脑刷一个recovery，然后再卡刷其它系统来实现root，具体步骤感兴趣的可以研究，关键字提示：k20 Pro recovery。</p></blockquote><p>用一根官方原版数据线连接电脑，不要用那些劣质的线，同时在手机上面打开USB调试，这个我就不多说了，大概就是快速点击MIUI版本，然后开发者模式-USB调试即可，如果这个都不会的话，说明你从未有过刷机经验，建议不要折腾了。</p><p>用电脑打开手机usb存储，找到上面生成的那个patched文件，复制到电脑上面一个文件夹里面，你得记住在哪，后面会用到。</p><p>当你打开usb调试的时候，手机上面会弹一个确认框，请务必同意，如果没有看见，那就拔了线再插上试试，接下来就需要使用adb工具刷机了，如果你是Linux或者Mac的话，直接安装一个adb和fastboot命令行工具就行。</p><p>以Ubuntu为例：</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">sudo apt install adb</span><br><span class="line">sudo apt install fastboot</span><br></pre></td></tr></table></figure><p>如果是Windows电脑，你可以下载我这里的工具包： <a href="/file/ADBTools.zip">点击下载 ADBTools.zip</a></p><p>这里我假设你有终端使用经验，打开终端，cd到刚才保存那个patched文件的文件夹，执行 <code>adb reboot bootloader</code>重启到fastboot界面，你也可以在关机状态下长按开机键和音量上键。</p><p>然后执行<code>fastboot flash boot magisk_patched-24300_uVoAh.img</code>命令开始刷机，如果没有报错的话，看到Ok信息之后重启手机即可。</p><p>开机之后打开刚才安装的magisk app，如果看到下面这个界面就说明已经成功了，其中 <strong>超级用户</strong> 菜单就是管理root权限的地方。</p><img width="40%" src="/images/2022/success.jpg" /> <h2 id="5-安装Scene4和MIUI温控"><a href="#5-安装Scene4和MIUI温控" class="headerlink" title="5.安装Scene4和MIUI温控"></a>5.安装Scene4和MIUI温控</h2><p>建议大家官网下载，这里就不提供了安装包下载了</p><p>这个软件的功能很多，但是主要用来修改CPU调度，直接点击<strong>极速</strong>即可火力拉满，然后打开MIUI温控，点击 <strong>淦</strong> 解除封印。</p><img width="40%" src="/images/2022/scene4.jpg" /> <img width="40%" src="/images/2022/wenkong.jpg" /><p>其它更多功能自行研究。。。</p><h2 id="6-安全隐患"><a href="#6-安全隐患" class="headerlink" title="6.安全隐患"></a>6.安全隐患</h2><p>很多软件检测到你的设备root之后就会提示你也风险，有些甚至直接不给你使用（比如建设银行APP），站在公司开发者的角度来说确实可能有风险，但是安全这个事情主要还是靠个人，比如即使不root也不能随便安装一些来源未知的APP。</p><p>不过root权限确实非常重要，我们对于root权限的授权管理则要非常慎重，只有你明确知道需要root权限的APP才同意，其它的一律拒绝。</p><p>针对Scene4和MIUI温控这些搞机软件，我还完全禁用了其联网权限，其它权限也最小化。。。</p><img width="40%" src="/images/2022/zygisk.jpg" /> <p>另外，很多银行APP检测到手机root之后无法使用的情况，这里建议使用Magisk自带的 <strong>Zygisk</strong> 排除软件即可。</p><p>最后，说一下我的实际体验，在k20 Pro上面经过上述的操作之后流畅度提升很多，有一种丝滑的感觉，毕竟CPU主频提高了1个G，应用的打开速度也提升了很多，个人觉得值得一试，友情提示：折腾之前备份好数据。</p>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;最近愈发觉得我用了差不多2年的手机红米k20 Pro打开应用速度反应很慢，偶尔还会有卡顿，这款手机搭载了高通骁龙855plus，8核CPU，最高主频2.96GHz，即使放在今天性能也不是很差，我平时很少拿来玩游戏，只是日常使用。&lt;/p&gt;
&lt;p&gt;虽说手机用久了都会变卡，毕竟电子元器件存在性能衰减，不过问题其实也不是这么简单。MIUI系统很早就有省电模式、均衡模式、性能模式，熟悉Linux系统的人都知道这和Linux的CPU调度策略非常类似，当然这是废话，因为安卓内核就是Linux。。。&lt;/p&gt;
&lt;p&gt;手机系统为了平衡功耗以及温度，会有很多种CPU调度策略，MIUI在平时使用的时候会根据运行软件的不同去执行不同的调度策略，比如一些日常软件的话主频会限制到2GHz，但打游戏的时候往往火力全开，但是火力全开手机温度就会上升，达到一定程度就会触发温控策略导致CPU降频，从而引起卡顿。&lt;/p&gt;
&lt;p&gt;我这个机型的系统版本还取消了性能模式，默认以均衡模式运行，再加上小米保守的温控策略，根本没法发挥全部实力，作为一个技术宅已经忍了很久了，既然没钱换新手机那就动手解除封印，释放全部潜能再战1年！所以接下来，我会把这台手机root、之后再安装Scene4解锁来CPU限制，火力全开，最后再删除温控策略，尽可能的释放手机性能，提高流畅度。&lt;/p&gt;
&lt;p&gt;理论上，手机CPU和GPU也能超频，不过这个玩法就太高端了，而且恐怕散热真的撑不住。。。&lt;/p&gt;
&lt;p&gt;本人在折腾过程中也查看了不少教程，但是都比较零散细碎，所以这里总结了一下，以下教程适用于MIUI 12，虽然我的机型可能和大家不一样，但是大同小异，仅供参考。&lt;/p&gt;
    
    </summary>
    
    
      <category term="Linux" scheme="https://wangbenjun.site/categories/Linux/"/>
    
    
      <category term="Android" scheme="https://wangbenjun.site/tags/Android/"/>
    
  </entry>
  
  <entry>
    <title>升级Ubuntu的Linux内核版本</title>
    <link href="https://wangbenjun.site/2022/linux/upgrade-linux-kernel.html"/>
    <id>https://wangbenjun.site/2022/linux/upgrade-linux-kernel.html</id>
    <published>2022-03-19T05:00:00.000Z</published>
    <updated>2024-12-29T03:02:14.815Z</updated>
    
    <content type="html"><![CDATA[<p>众所周知，Ubuntu的发行版默认的Linux内核版本都不是最新的，但是通常比较稳定，是和发行版契合的，一般情况下我不建议升级，除非说你有明确的需求。</p><p>比如说 Ubuntu16.04 目前的内核版本还是4.15，确实比较老，像我个人比较喜欢unity桌面，所以也一直没有去升级到20.04的发行版（新发行版使用5.0以上版本），但是我们可以单独升级内核。</p><img src="/images/2022/screenfetch.png" /> <p>本人升级内核主要是折腾、尝鲜，个人感觉升级版本之后感觉确实流畅了一点，其它没有什么变化。</p><p>先说结论：目前 Ubuntu16.04桌面版本，可以完美支持的Linux内核版本是5.4，更新一点的版本在安装的时候会有一些headers依赖错误，虽然不致命，但是总感觉有问题，这些底层依赖库自己想要去升级版本的话是非常麻烦的，必须从源码编译，而且可能会有导致系统崩溃的风险。</p><a id="more"></a><h2 id="1-Longterm-版本"><a href="#1-Longterm-版本" class="headerlink" title="1.Longterm 版本"></a>1.Longterm 版本</h2><p>Linux内核更新还是非常快，小版本一个接一个，我们可以找一个相对稳定持续更新的版本，打开 <code>https://www.kernel.org/</code> 官方主页，我们会看到最近几个 <strong>longterm</strong> 的版本：</p><img src="/images/2022/longterm.png" /> <p>建议升级的适合优先选择这些版本，因为这些版本会持续更新很久。。。当然也不是说其它小版本就不能用了。</p><p>这些我们只是看一下版本号，备用，除非你选择自己编译内核，这将非常复杂和耗时，接下来我们将去下载已经编译的好的内核。</p><h2 id="2-预编译的内核"><a href="#2-预编译的内核" class="headerlink" title="2.预编译的内核"></a>2.预编译的内核</h2><p>在 <code>https://kernel.ubuntu.com/~kernel-ppa/mainline/</code> 这里我们可以下载到编译的适合Ubuntu的内核，每一个版本都有，从其命名格式上面也能看出，前面2个数字是大版本号，最后一个数字代表是小版本，我们可以选择自己需求的版本：</p><img src="/images/2022/ubuntu-ppa.png" /> <p>点击链接会打开一个新的页面，可以看到上面还分为amd64、arm64、i386等不同架构，这里以64位系统为例，里面有多个deb安装包文件，下载的时候需要注意Linux内核还分为 <strong>lowlatency 和 generic</strong>，也就是通用版本和低延迟版本，据我了解低延迟版本一般适用于对延迟要求比较低的需求场景，比如录音。</p><p>这里咱们就选择下载名字里面包含generic的几个安装包，全部下载下来放到一个文件夹里面，然后使用<code>sudo dpkg -i *.deb</code>命令安装即可。</p><img src="/images/2022/ubuntu-amd64.png" /> <p>如果不报错，顺利安装成功的话，会默认启用该内核，重启生效。</p><p>卸载的话，只需要使用<code>apt purge</code>删除这几个软件即可，以5.4内核为例则是下面这个软件：</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">linux-headers-5.4.185-0504185</span><br><span class="line">linux-headers-5.4.185-0504185-generic</span><br><span class="line">linux-image-unsigned-5.4.185-0504185-generic</span><br><span class="line">linux-modules-5.4.185-0504185-generic</span><br></pre></td></tr></table></figure><p>最后不要忘了，使用<code>sudo update-grub2</code>生成新的启动image文件。</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">jwang@jwang:~$ sudo update-grub2</span><br><span class="line">Generating grub configuration file ...</span><br><span class="line">Found linux image: /boot/vmlinuz-5.4.185-0504185-generic</span><br><span class="line">Found initrd image: /boot/initrd.img-5.4.185-0504185-generic</span><br><span class="line">Found linux image: /boot/vmlinuz-4.15.0-171-generic</span><br><span class="line">Found initrd image: /boot/initrd.img-4.15.0-171-generic</span><br><span class="line">Found linux image: /boot/vmlinuz-4.15.0-169-generic</span><br><span class="line">Found initrd image: /boot/initrd.img-4.15.0-169-generic</span><br><span class="line">Found Windows Boot Manager on /dev/nvme0n1p1@/EFI/Microsoft/Boot/bootmgfw.efi</span><br><span class="line">Adding boot menu entry for EFI firmware configuration</span><br><span class="line">done</span><br></pre></td></tr></table></figure><p>最后说一个副作用，我在升级的过程中发现之前的 <strong>virtualbox</strong> 用不了，提示使用 &#x2F;sbin&#x2F;vboxconfig 命令配置也没有成功，看日志是因为系统里面缺少合适的header文件。</p><p>个人猜测是因为virtualbox这种虚拟机软件比较特殊，它依赖了内核里面的某种机制，可能官方的安装包是针对Ubuntu16.04的内核版本设置的，尝试了多种方法，没有解决。如果你比较依赖virtualbox虚拟机的话，可能要谨慎一点。</p>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;众所周知，Ubuntu的发行版默认的Linux内核版本都不是最新的，但是通常比较稳定，是和发行版契合的，一般情况下我不建议升级，除非说你有明确的需求。&lt;/p&gt;
&lt;p&gt;比如说 Ubuntu16.04 目前的内核版本还是4.15，确实比较老，像我个人比较喜欢unity桌面，所以也一直没有去升级到20.04的发行版（新发行版使用5.0以上版本），但是我们可以单独升级内核。&lt;/p&gt;
&lt;img src=&quot;/images/2022/screenfetch.png&quot; /&gt; 

&lt;p&gt;本人升级内核主要是折腾、尝鲜，个人感觉升级版本之后感觉确实流畅了一点，其它没有什么变化。&lt;/p&gt;
&lt;p&gt;先说结论：目前 Ubuntu16.04桌面版本，可以完美支持的Linux内核版本是5.4，更新一点的版本在安装的时候会有一些headers依赖错误，虽然不致命，但是总感觉有问题，这些底层依赖库自己想要去升级版本的话是非常麻烦的，必须从源码编译，而且可能会有导致系统崩溃的风险。&lt;/p&gt;
    
    </summary>
    
    
      <category term="Linux" scheme="https://wangbenjun.site/categories/Linux/"/>
    
    
      <category term="Ubuntu" scheme="https://wangbenjun.site/tags/Ubuntu/"/>
    
  </entry>
  
  <entry>
    <title>Golang泛型入门了解</title>
    <link href="https://wangbenjun.site/2022/coding/golang/generic.html"/>
    <id>https://wangbenjun.site/2022/coding/golang/generic.html</id>
    <published>2022-03-17T14:27:00.000Z</published>
    <updated>2024-12-29T03:02:14.809Z</updated>
    
    <content type="html"><![CDATA[<h2 id="1-什么是泛型？"><a href="#1-什么是泛型？" class="headerlink" title="1.什么是泛型？"></a>1.什么是泛型？</h2><p>最近Go官方刚刚发布了很多人期待已久的1.18版本，其中最大的特性就是正式支持泛型。</p><blockquote><p>泛型程序设计（generic programming）是程序设计语言的一种风格或范式。泛型允许程序员在强类型程序设计语言中编写代码时使用一些以后才指定的类型，在实例化时作为参数指明这些类型。</p></blockquote><p>上面这段描述比较专业，通俗的说泛型就是广泛的类型。。。很多语言都有实现类似特性，比如在Java里面泛型应用非常广泛，在C&#x2F;C++里面也有宏、函数模板这样的东西可以实现类似的效果。</p><a id="more"></a><p>举个例子，假设我们需要实现一个数字相加的函数，因为Go是强类型语言，所以必须申明函数参数类型和返回值，如下：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">sum</span><span class="params">(param []<span class="keyword">int</span>)</span> <span class="title">int</span></span> &#123;</span><br><span class="line">    s := <span class="number">0</span></span><br><span class="line">    <span class="keyword">for</span> _, v := <span class="keyword">range</span> param &#123;</span><br><span class="line">        s += v</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> s</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>但是上面这个函数只能实现int类型的数字相加，如果我们还有一些浮点型数字需要相加呢？在Go支持泛型之前，我们只能再写一个类型申明为float64的“重复”函数，但是这样显的很呆。。。怎么办呢？</p><h2 id="2-简单示例"><a href="#2-简单示例" class="headerlink" title="2.简单示例"></a>2.简单示例</h2><p>下面咱们就看一下如果使用泛型来简化上面这个sum函数，用一个函数解决int和float64这2种数值类型的相加。</p><p>其实在泛型诞生之前，很多人经常利用 <strong>interface{}</strong> 类型来实现类似泛型的作用，<strong>interface{}</strong> 有点像是泛型，只不过在使用的时候需要断言，如果使用 <strong>interface{}</strong> 实现的话，我们可以使用反射获取参数的真实类型，然后再断言进行相加。</p><p>Go的泛型使用 [] 来申明类型，比如 func sum[v int|float64] 表示v可以是int或者float64类型。</p><p>示例如下：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">    fmt.Printf(<span class="string">"%v\n"</span>, sum([]<span class="keyword">int</span>&#123;<span class="number">1</span>, <span class="number">5</span>&#125;))</span><br><span class="line">    fmt.Printf(<span class="string">"%v\n"</span>, sum([]<span class="keyword">float64</span>&#123;<span class="number">1</span>, <span class="number">2.5</span>&#125;))</span><br><span class="line">    fmt.Printf(<span class="string">"%v\n"</span>, sum([]<span class="keyword">string</span>&#123;<span class="string">"a"</span>, <span class="string">"b"</span>, <span class="string">"cdef"</span>&#125;))</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">sum</span>[<span class="title">v</span> <span class="title">int</span> | <span class="title">float64</span> | <span class="title">string</span>]<span class="params">(param []v)</span> <span class="title">v</span></span> &#123;</span><br><span class="line">    <span class="keyword">var</span> s v</span><br><span class="line">    <span class="keyword">for</span> _, v := <span class="keyword">range</span> param &#123;</span><br><span class="line">        s += v</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> s</span><br><span class="line">&#125;</span><br><span class="line"><span class="comment">//运行结果</span></span><br><span class="line"><span class="number">6</span></span><br><span class="line"><span class="number">3.5</span></span><br><span class="line">abcdef</span><br></pre></td></tr></table></figure><p>官方这次还新增了3个泛型试验包，其中有针对slice和map的包，主要是实现了一些通用函数操作，比如查找、比较、排序等，但它们的 API 不在 Go1兼容性承诺的保证范围内:</p><ul><li>golang.org&#x2F;x&#x2F;exp&#x2F;constraints:对通用代码有用的约束，例如constraints.Ordered</li><li>golang.org&#x2F;x&#x2F;exp&#x2F;slices:对任何元素类型的切片进行操作的通用函数集合</li><li>golang.org&#x2F;x&#x2F;exp&#x2F;maps:对任何键或元素类型的映射进行操作的通用函数集合</li></ul><p>不过目前来看，Go的泛型特性还在继续完善中，暂时不建议使用在生产环境上，估计还得再等等，其实泛型的最大意义在于实现一些复杂又灵活的功能，比如一些框架。</p><p>俗话说：“动态语言一时爽，代码重构火葬场”，为什么这么说呢？ </p><p>因为动态语言一般又叫做弱类型语言，比如PHP和JS，它们的特点就是简单灵活，函数不需要申明类型，比如int和float类型可以直接相加，因为解释器会在运行时做自动类型转换。</p><p>而Go、Java这样的则被称作静态语言，又叫强类型语言，虽然性能高，但是缺乏足够灵活性，泛型就是为了弥补这个缺陷，尽可能的在性能和灵活性之间取一个平衡。</p>]]></content>
    
    <summary type="html">
    
      &lt;h2 id=&quot;1-什么是泛型？&quot;&gt;&lt;a href=&quot;#1-什么是泛型？&quot; class=&quot;headerlink&quot; title=&quot;1.什么是泛型？&quot;&gt;&lt;/a&gt;1.什么是泛型？&lt;/h2&gt;&lt;p&gt;最近Go官方刚刚发布了很多人期待已久的1.18版本，其中最大的特性就是正式支持泛型。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;泛型程序设计（generic programming）是程序设计语言的一种风格或范式。泛型允许程序员在强类型程序设计语言中编写代码时使用一些以后才指定的类型，在实例化时作为参数指明这些类型。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;上面这段描述比较专业，通俗的说泛型就是广泛的类型。。。很多语言都有实现类似特性，比如在Java里面泛型应用非常广泛，在C&amp;#x2F;C++里面也有宏、函数模板这样的东西可以实现类似的效果。&lt;/p&gt;
    
    </summary>
    
    
      <category term="Golang" scheme="https://wangbenjun.site/categories/Golang/"/>
    
    
      <category term="Golang" scheme="https://wangbenjun.site/tags/Golang/"/>
    
  </entry>
  
  <entry>
    <title>浅谈Golang并发编程</title>
    <link href="https://wangbenjun.site/2021/coding/golang/concurrence-programing.html"/>
    <id>https://wangbenjun.site/2021/coding/golang/concurrence-programing.html</id>
    <published>2021-12-30T13:00:00.000Z</published>
    <updated>2024-12-29T03:02:14.808Z</updated>
    
    <content type="html"><![CDATA[<p>说到并发和并行，很多人容易混淆，虽然在大部分时候他们俩的意思都差不多，但实际上还是有很大区别。如果非得说明白，我觉得先从并行和串行这个2个单词说起更加容易理解，因为这2个单词很多人都熟悉，在编程里面所谓串行就是一个一个去执行，而并行则是指同时去执行。</p><p>举个例子：如果你需要调5个接口获取数据，每个接口耗时1s，那么串行总共需要5s来执行，但是并行顾名思义同时去执行，则可以同时去调用，那么总共只需要1s时间。所以相对于串行来说，并行可以节省很大时间，提高效率。</p><p>但是并发的概念和并行有所不同，并发更强调的是“同时”发生，它可以是并行的，也可以是“假”并行。</p><p>举个例子：早期CPU只有单核，单核意味着CPU同时一时间只能执行一段代码，那电脑是怎么做到在上网的同时还可以听歌呢？这是因为操作系统是支持并发的，它会把CPU执行时间分为很多小的时间片分给不同的应用程序，然后交替执行代码，所以看上去好像是在同时进行。如今，电脑CPU都是多核的，操作系统可以把一个或者多个核单独分配给一个应用去使用，对于这种情况我们可以认为他是并行。</p><p>从理论上说并行只是并发的一种实现方式，并发是更高维度的概念，说白了，并发只要你能实现“同时”执行的效果，至于物理层面是否是同时并不重要。所以我们网上看到大部分文章都会说并发编程，很少有人会说并行编程。</p><p>其实在如今，很多语言依然不支持并发编程，比如PHP；还有很多语言通过多进程、多线程的方式来实现，比如C++、Java；而Go则是通过协程的方式实现了并发编程。</p><a id="more"></a><h2 id="1-示例"><a href="#1-示例" class="headerlink" title="1.示例"></a>1.示例</h2><p>咱们先看一个串行的例子，一个简单的函数调用，这里用sleep模拟操作耗时1s，然后打印当前时间，可以看到执行完这3次操作必须耗时3s</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">    hello()</span><br><span class="line">    hello()</span><br><span class="line">    hello()</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">hello</span><span class="params">()</span></span> &#123;</span><br><span class="line">    time.Sleep(time.Second)</span><br><span class="line">    fmt.Printf(<span class="string">"Hello World: %v\n"</span>, time.Now().Unix())</span><br><span class="line">&#125;</span><br><span class="line"><span class="comment">/** 运行结果</span></span><br><span class="line"><span class="comment">Hello World: 1640766013</span></span><br><span class="line"><span class="comment">Hello World: 1640766014</span></span><br><span class="line"><span class="comment">Hello World: 1640766015</span></span><br><span class="line"><span class="comment"> */</span></span><br></pre></td></tr></table></figure><p>下面再看Go协程并发的版本：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">    <span class="keyword">go</span> hello()</span><br><span class="line">    <span class="keyword">go</span> hello()</span><br><span class="line">    <span class="keyword">go</span> hello()</span><br><span class="line">    time.Sleep(time.Second * <span class="number">2</span>)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">hello</span><span class="params">()</span></span> &#123;</span><br><span class="line">    time.Sleep(time.Second)</span><br><span class="line">    fmt.Printf(<span class="string">"Hello World: %v\n"</span>, time.Now().Unix())</span><br><span class="line">&#125;</span><br><span class="line"><span class="comment">/** 运行结果</span></span><br><span class="line"><span class="comment">Hello World: 1640766082</span></span><br><span class="line"><span class="comment">Hello World: 1640766082</span></span><br><span class="line"><span class="comment">Hello World: 1640766082</span></span><br><span class="line"><span class="comment"> */</span></span><br></pre></td></tr></table></figure><p>从打印结果来看，这3次调用几乎是同时执行的，虽然严格意义上说并不是同时开始的，因为协程的启动也有开销的，需要一点时间的，不过也是毫秒级别，从秒的维度看是同时。</p><h2 id="2-等待协程结束（同步）"><a href="#2-等待协程结束（同步）" class="headerlink" title="2.等待协程结束（同步）"></a>2.等待协程结束（同步）</h2><p>可能你看过很大文章说在Go里面启用协程只需要使用go关键字就行，但是实际使用中你还要解决几个问题，其中第一个就是如何等待协程执行完成。</p><p>当你使用go启动一个协程之后，它是异步非阻塞的，它会在”后台“默默执行协程里面的代码，同时主进程会继续往下执行，在上面这个例子里面，main里面没有其它代码了，只有一个sleep，之所以要加这个sleep就是为了等待那3个协程执行完，如果去掉这个sleep你会发现没有任何结果输出，因为协程还没来得及运行主进程就结束了。</p><p>所以当你在使用协程的时候需要确保所有协程都有足够的时间去执行，当所有协程都结束后才退出。当然如果应用本身就是常驻进程的话，比如HTTP服务，就不需要关心这个问题。</p><p>上面这个例子的写法只是为了方便测试，在实际上开发中，肯定不能用sleep，你不可能准确预测到协程执行需要多久时间，官方为了解决这个问题专门弄了包，在sync包里面有一个WaitGroup就是拿来同步不同协程的操作。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// A WaitGroup waits for a collection of goroutines to finish.</span></span><br><span class="line"><span class="comment">// The main goroutine calls Add to set the number of</span></span><br><span class="line"><span class="comment">// goroutines to wait for. Then each of the goroutines</span></span><br><span class="line"><span class="comment">// runs and calls Done when finished. At the same time,</span></span><br><span class="line"><span class="comment">// Wait can be used to block until all goroutines have finished.</span></span><br><span class="line"><span class="comment">//</span></span><br><span class="line"><span class="comment">// A WaitGroup must not be copied after first use.</span></span><br><span class="line"><span class="keyword">type</span> WaitGroup <span class="keyword">struct</span> &#123;</span><br><span class="line">    noCopy noCopy</span><br><span class="line">    state1 [<span class="number">3</span>]<span class="keyword">uint32</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>使用方法非常简单，它只有3个方法，Add、Done、Wait，最开始那个例子如果使用WaitGroup改造的话可以这么写：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">    wg := <span class="built_in">new</span>(sync.WaitGroup)</span><br><span class="line">    wg.Add(<span class="number">3</span>)</span><br><span class="line"></span><br><span class="line">    <span class="keyword">go</span> hello(wg)</span><br><span class="line">    <span class="keyword">go</span> hello(wg)</span><br><span class="line">    <span class="keyword">go</span> hello(wg)</span><br><span class="line"></span><br><span class="line">    wg.Wait()</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">hello</span><span class="params">(wg *sync.WaitGroup)</span></span> &#123;</span><br><span class="line">    <span class="keyword">defer</span> wg.Done()</span><br><span class="line">    fmt.Printf(<span class="string">"Hello World: %v\n"</span>, time.Now().Unix())</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>其中Add(3)表示有3个协程需要执行，同时我们以指针的形式把wg传入函数内部，在函数结束之前调用Done，最后在主进程里面我们调用Wait，这个函数是阻塞的，它会等待所有协程结束。</p><p>这个库源码的话比较简单，简单说就是一个原子计数操作，Add就是+1，Done就是-1，然后开启一个for循环阻塞，在循环里面等待值变为0就退出。</p><p>除此之外，针对单个协程的话也可以利用Go管道的特性来实现类似功能，但是针对多个协程之间的同步还是WaitGroup更好好用。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">    ch := <span class="built_in">make</span>(<span class="keyword">chan</span> <span class="keyword">int64</span>)</span><br><span class="line">    <span class="keyword">go</span> hello(ch)</span><br><span class="line">    &lt;-ch</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">hello</span><span class="params">(ch <span class="keyword">chan</span> <span class="keyword">int64</span>)</span></span> &#123;</span><br><span class="line">    <span class="keyword">defer</span> <span class="function"><span class="keyword">func</span><span class="params">()</span></span> &#123;</span><br><span class="line">        ch &lt;- <span class="number">1</span></span><br><span class="line">    &#125;()</span><br><span class="line">    fmt.Printf(<span class="string">"Hello World: %v\n"</span>, time.Now().Unix())</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="3-协程通信"><a href="#3-协程通信" class="headerlink" title="3.协程通信"></a>3.协程通信</h2><p>在Go里面channel是协程之间通信的桥梁，在Go编程里面有一句非常知名的话：”不要使用共享内存去通信，而应该通过通信来共享内存“，这里面的通信指的就是channel这种方式。</p><p>即使你使用多进程或者多线程这种并发编程模型，都存在在不同进程或者线程之间通信的需求，我们很容易就想到可以使用全局变量+锁这种方式，这种方式本质上就是共享内存，Go的编程哲学并不推荐，虽然Go也能这么写。</p><p>以上面的例子为基础，我们现在假设现在hello这个函数不再打印一个结果，我们需要“返回”当前时间戳作为结果，如果你直接return，主进程是没法接收返回值的。</p><p>如果你使用共享内存这种方式，可以先定义一个全局的slice来存储结果，同时需要一把全局锁来解决并发数据竞争问题，比如下面这个例子：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">var</span> (</span><br><span class="line">    ts     []<span class="keyword">int64</span></span><br><span class="line">    locker = sync.RWMutex&#123;&#125;</span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">    wg := <span class="built_in">new</span>(sync.WaitGroup)</span><br><span class="line">    wg.Add(<span class="number">3</span>)</span><br><span class="line"></span><br><span class="line">    <span class="keyword">go</span> hello(wg)</span><br><span class="line">    <span class="keyword">go</span> hello(wg)</span><br><span class="line">    <span class="keyword">go</span> hello(wg)</span><br><span class="line"></span><br><span class="line">    wg.Wait()</span><br><span class="line"></span><br><span class="line">    fmt.Printf(<span class="string">"%v\n"</span>, ts)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">hello</span><span class="params">(wg *sync.WaitGroup)</span></span> &#123;</span><br><span class="line">    <span class="keyword">defer</span> wg.Done()</span><br><span class="line">    <span class="keyword">defer</span> locker.Unlock()</span><br><span class="line">    locker.Lock()</span><br><span class="line">    ts = <span class="built_in">append</span>(ts, time.Now().Unix())</span><br><span class="line">&#125;</span><br><span class="line"><span class="comment">/** 运行结果</span></span><br><span class="line"><span class="comment">[1640854306 1640854306 1640854306]</span></span><br><span class="line"><span class="comment"> */</span></span><br></pre></td></tr></table></figure><p>虽然Go官方并不推荐这种写法，但是这么写是完全没有问题的，但如果使用channel的话更好，下面我们再看看使用channel的写法：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">    ch := <span class="built_in">make</span>(<span class="keyword">chan</span> <span class="keyword">int64</span>)</span><br><span class="line"></span><br><span class="line">    <span class="keyword">go</span> hello(ch)</span><br><span class="line">    <span class="keyword">go</span> hello(ch)</span><br><span class="line">    <span class="keyword">go</span> hello(ch)</span><br><span class="line"></span><br><span class="line">    <span class="keyword">for</span> &#123;</span><br><span class="line">        fmt.Printf(<span class="string">"%v\n"</span>, &lt;-ch)</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">hello</span><span class="params">(ch <span class="keyword">chan</span> <span class="keyword">int64</span>)</span></span> &#123;</span><br><span class="line">    ch &lt;- time.Now().Unix()</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>我们先创建一个int64类似的channel，然后作为参数传入函数内部，在函数里面把结果放到管道里面，同时在主进程里面使用for…range获取channel里面的数据，这里大家可以注意到可以不用WaitGroup了，因为从channel里面获取数据也是阻塞的。</p><p>但是运行的话你会发现不仅打印出了3个正确结果，还报错了: <code>fatal error: all goroutines are asleep - deadlock!</code></p><p>这个死锁问题在使用channel的时候需要注意的问题，详细说起来还比较复杂，简单说就是如果你要从一个channel里面取数据，那么必须有一个地方会往里面放数据，在这个例子里面，Go运行时检测到了你取完3个数据之后，没有其它地方会往channel里面放数据了，这样for循环会一直等下去。。。</p><p>想要解决这个问题，就必须在合理的时候关闭channel，在上面这个例子，我们可以明确知道只有3个结果，所以我们可以在for循环里面加一个判断，主动close channel，结束for循环，继续执行后面的代码逻辑。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">i := <span class="number">0</span></span><br><span class="line"><span class="keyword">for</span> &#123;</span><br><span class="line">    fmt.Printf(<span class="string">"%v\n"</span>, &lt;-ch)</span><br><span class="line">    i++</span><br><span class="line">    <span class="keyword">if</span> i == <span class="number">3</span> &#123;</span><br><span class="line">        <span class="built_in">close</span>(ch)</span><br><span class="line">        <span class="keyword">break</span></span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="4-协程调度"><a href="#4-协程调度" class="headerlink" title="4.协程调度"></a>4.协程调度</h2><p>Go协程调度相关的东西非常多，这里只是抛砖引玉说一点，话说在计算机系统里面，但凡是涉及到资源（CPU、GPU、内存、存储）的使用都需要调度，所谓调度无非是如何去分配资源，因为资源是有限的，但是使用方却很多，给谁用，用多少是一个问题。</p><p>举个例子，电脑CPU只有8核，但是上面跑的进程可能有几十上百个，那操作系统是如何分配CPU资源的呢？是按照先后顺序还是平均分配？ 这就涉及到操作系统调度的问题了，说起来也非常复杂，一个成熟的调度算法必须兼顾公平和效率。</p><p>回到Go这边，Go的协程也被称为是用户态线程，在多线程并发模型里面，进程和线程是直接参与CPU调度的，是由操作系统来完成调度这件事，但是Go的协程并不直接参与系统调度，那问题来了，如果我开了100个协程，但是我的CPU只有8核，Go是怎么做到“同时”运行这些协程的呢？</p><img src="/images/2021/gmp.jpeg" /><p>这个问题的答案就是Go协程的调度所用到的GMP模型，这个调度模型也是经过多次版本迭代后的结果，网上也很多深度解析的文章介绍，感兴趣的可以找找看一看，这里只借用一张图。</p><h2 id="5-控制协程数量"><a href="#5-控制协程数量" class="headerlink" title="5.控制协程数量"></a>5.控制协程数量</h2><p>虽然Go协程的开销很小，但是如果不加以控制可能会产生一些意想不到的结果，举个例子，你需要调用一个接口，使用1000个不同的参数，每次调用接口需要1s时间，聪明的你肯定想到如果串行的调用则需要1000s时间，太慢了，但是使用Go协程开启1000个协程很快就搞定了。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">    wg := <span class="built_in">new</span>(sync.WaitGroup)</span><br><span class="line">    <span class="keyword">for</span> i := <span class="number">0</span>; i &lt; <span class="number">1000</span>; i++ &#123;</span><br><span class="line">        wg.Add(<span class="number">1</span>)</span><br><span class="line">        <span class="keyword">go</span> hello(wg)</span><br><span class="line">    &#125;</span><br><span class="line">    wg.Wait()</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">hello</span><span class="params">(wg *sync.WaitGroup)</span></span> &#123;</span><br><span class="line">    <span class="keyword">defer</span> wg.Done()</span><br><span class="line">    resp, err := http.Get(<span class="string">"https://www.baidu.com"</span>)</span><br><span class="line">    <span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">        <span class="keyword">return</span></span><br><span class="line">    &#125;</span><br><span class="line">    fmt.Printf(<span class="string">"resp status: %s\n"</span>, resp.Status)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>思路没问题，但是你有没有想过这个接口的能力能不能支持这么大并发量，如果不支持，轻则导致你调用报错、重则导致对方服务挂掉。所以从安全可用的角度来说，我们需要控制一下并发数量，比如说100个。</p><p>只要稍加改造，可以利用Go管道的阻塞特性实现这一功能，控制协程的并发数量：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">    wg := <span class="built_in">new</span>(sync.WaitGroup)</span><br><span class="line">    ch := <span class="built_in">make</span>(<span class="keyword">chan</span> <span class="keyword">bool</span>, <span class="number">100</span>)</span><br><span class="line"></span><br><span class="line">    <span class="keyword">for</span> i := <span class="number">0</span>; i &lt; <span class="number">1000</span>; i++ &#123;</span><br><span class="line">        wg.Add(<span class="number">1</span>)</span><br><span class="line">        <span class="keyword">go</span> hello(wg, ch)</span><br><span class="line">    &#125;</span><br><span class="line">    wg.Wait()</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">hello</span><span class="params">(wg *sync.WaitGroup, ch <span class="keyword">chan</span> <span class="keyword">bool</span>)</span></span> &#123;</span><br><span class="line">    <span class="keyword">defer</span> wg.Done()</span><br><span class="line">    <span class="keyword">defer</span> <span class="function"><span class="keyword">func</span><span class="params">(ch <span class="keyword">chan</span> <span class="keyword">bool</span>)</span></span> &#123;</span><br><span class="line">        &lt;-ch</span><br><span class="line">    &#125;(ch)</span><br><span class="line">    </span><br><span class="line">    ch &lt;- <span class="literal">true</span></span><br><span class="line">    resp, err := http.Get(<span class="string">"https://www.baidu.com"</span>)</span><br><span class="line">    <span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">        <span class="keyword">return</span></span><br><span class="line">    &#125;</span><br><span class="line">    fmt.Printf(<span class="string">"resp status: %s\n"</span>, resp.Status)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这段代码的原理就是有缓冲管道是可以缓存一定数据的，我们先创建一个缓存大小为100的channel，把它作为参数传递到函数内部，在执行函数逻辑之前往channel里面放一个数据，在执行完逻辑之后取出一个数据。因为缓存大小是100，所以最多可以有100个协程同时执行。</p><p>不过这种方案的缺点就是在一开始就已经启用了1000个协程，只不过这些协程卡在等待执行逻辑的过程中，如果协程数据量超级大的话可能有一些内存开销。</p><h2 id="6-ErrGroup"><a href="#6-ErrGroup" class="headerlink" title="6.ErrGroup"></a>6.ErrGroup</h2><p>这是一个Go官方提供的一个同步扩展库，它可以将一个大任务拆分成几个小任务并发执行，提高程序效率。其代码非常简洁，主要有三个方法，WithContext、Go、Wait。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">    <span class="keyword">var</span> g errgroup.Group</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 启动第一个子任务,它执行成功</span></span><br><span class="line">    g.Go(<span class="function"><span class="keyword">func</span><span class="params">()</span> <span class="title">error</span></span> &#123;</span><br><span class="line">        time.Sleep(<span class="number">5</span> * time.Second)</span><br><span class="line">        fmt.Println(<span class="string">"exec #1"</span>)</span><br><span class="line">        <span class="keyword">return</span> <span class="literal">nil</span></span><br><span class="line">    &#125;)</span><br><span class="line">    <span class="comment">// 启动第二个子任务，它执行失败</span></span><br><span class="line">    g.Go(<span class="function"><span class="keyword">func</span><span class="params">()</span> <span class="title">error</span></span> &#123;</span><br><span class="line">        time.Sleep(<span class="number">10</span> * time.Second)</span><br><span class="line">        fmt.Println(<span class="string">"exec #2"</span>)</span><br><span class="line">        <span class="keyword">return</span> errors.New(<span class="string">"failed to exec #2"</span>)</span><br><span class="line">    &#125;)</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 启动第三个子任务，它执行成功</span></span><br><span class="line">    g.Go(<span class="function"><span class="keyword">func</span><span class="params">()</span> <span class="title">error</span></span> &#123;</span><br><span class="line">        time.Sleep(<span class="number">15</span> * time.Second)</span><br><span class="line">        fmt.Println(<span class="string">"exec #3"</span>)</span><br><span class="line">        <span class="keyword">return</span> <span class="literal">nil</span></span><br><span class="line">    &#125;)</span><br><span class="line">    <span class="comment">// 等待三个任务都完成</span></span><br><span class="line">    <span class="keyword">if</span> err := g.Wait(); err == <span class="literal">nil</span> &#123;</span><br><span class="line">        fmt.Println(<span class="string">"Successfully exec all"</span>)</span><br><span class="line">    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">        fmt.Println(<span class="string">"failed:"</span>, err)</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>本质上这是一个对WaitGroup的封装，使用起来更加方便，它不仅可以等待所有协程执行完成，还可以返回第一个错误，同时还可以实现当其中某一个协程出错，取消其它协程的功能。</p><p>其源码非常简单，总共只有几十行代码，包含非常丰富的注释，贴上来大家一起欣赏一下：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// Copyright 2016 The Go Authors. All rights reserved.</span></span><br><span class="line"><span class="comment">// Use of this source code is governed by a BSD-style</span></span><br><span class="line"><span class="comment">// license that can be found in the LICENSE file.</span></span><br><span class="line"></span><br><span class="line"><span class="comment">// Package errgroup provides synchronization, error propagation, and Context</span></span><br><span class="line"><span class="comment">// cancelation for groups of goroutines working on subtasks of a common task.</span></span><br><span class="line"><span class="keyword">package</span> errgroup</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> (</span><br><span class="line">    <span class="string">"context"</span></span><br><span class="line">    <span class="string">"sync"</span></span><br><span class="line">)</span><br><span class="line"></span><br><span class="line"><span class="comment">// A Group is a collection of goroutines working on subtasks that are part of</span></span><br><span class="line"><span class="comment">// the same overall task.</span></span><br><span class="line"><span class="comment">//</span></span><br><span class="line"><span class="comment">// A zero Group is valid and does not cancel on error.</span></span><br><span class="line"><span class="keyword">type</span> Group <span class="keyword">struct</span> &#123;</span><br><span class="line">    cancel <span class="function"><span class="keyword">func</span><span class="params">()</span></span></span><br><span class="line"></span><br><span class="line">    wg sync.WaitGroup</span><br><span class="line"></span><br><span class="line">    errOnce sync.Once</span><br><span class="line">    err     error</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// WithContext returns a new Group and an associated Context derived from ctx.</span></span><br><span class="line"><span class="comment">//</span></span><br><span class="line"><span class="comment">// The derived Context is canceled the first time a function passed to Go</span></span><br><span class="line"><span class="comment">// returns a non-nil error or the first time Wait returns, whichever occurs</span></span><br><span class="line"><span class="comment">// first.</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">WithContext</span><span class="params">(ctx context.Context)</span> <span class="params">(*Group, context.Context)</span></span> &#123;</span><br><span class="line">    ctx, cancel := context.WithCancel(ctx)</span><br><span class="line">    <span class="keyword">return</span> &amp;Group&#123;cancel: cancel&#125;, ctx</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// Wait blocks until all function calls from the Go method have returned, then</span></span><br><span class="line"><span class="comment">// returns the first non-nil error (if any) from them.</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(g *Group)</span> <span class="title">Wait</span><span class="params">()</span> <span class="title">error</span></span> &#123;</span><br><span class="line">    g.wg.Wait()</span><br><span class="line">    <span class="keyword">if</span> g.cancel != <span class="literal">nil</span> &#123;</span><br><span class="line">        g.cancel()</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> g.err</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// Go calls the given function in a new goroutine.</span></span><br><span class="line"><span class="comment">//</span></span><br><span class="line"><span class="comment">// The first call to return a non-nil error cancels the group; its error will be</span></span><br><span class="line"><span class="comment">// returned by Wait.</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(g *Group)</span> <span class="title">Go</span><span class="params">(f <span class="keyword">func</span>()</span> <span class="title">error</span>)</span> &#123;</span><br><span class="line">    g.wg.Add(<span class="number">1</span>)</span><br><span class="line"></span><br><span class="line">    <span class="keyword">go</span> <span class="function"><span class="keyword">func</span><span class="params">()</span></span> &#123;</span><br><span class="line">        <span class="keyword">defer</span> g.wg.Done()</span><br><span class="line"></span><br><span class="line">        <span class="keyword">if</span> err := f(); err != <span class="literal">nil</span> &#123;</span><br><span class="line">            g.errOnce.Do(<span class="function"><span class="keyword">func</span><span class="params">()</span></span> &#123;</span><br><span class="line">                g.err = err</span><br><span class="line">                <span class="keyword">if</span> g.cancel != <span class="literal">nil</span> &#123;</span><br><span class="line">                    g.cancel()</span><br><span class="line">                &#125;</span><br><span class="line">            &#125;)</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;()</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;说到并发和并行，很多人容易混淆，虽然在大部分时候他们俩的意思都差不多，但实际上还是有很大区别。如果非得说明白，我觉得先从并行和串行这个2个单词说起更加容易理解，因为这2个单词很多人都熟悉，在编程里面所谓串行就是一个一个去执行，而并行则是指同时去执行。&lt;/p&gt;
&lt;p&gt;举个例子：如果你需要调5个接口获取数据，每个接口耗时1s，那么串行总共需要5s来执行，但是并行顾名思义同时去执行，则可以同时去调用，那么总共只需要1s时间。所以相对于串行来说，并行可以节省很大时间，提高效率。&lt;/p&gt;
&lt;p&gt;但是并发的概念和并行有所不同，并发更强调的是“同时”发生，它可以是并行的，也可以是“假”并行。&lt;/p&gt;
&lt;p&gt;举个例子：早期CPU只有单核，单核意味着CPU同时一时间只能执行一段代码，那电脑是怎么做到在上网的同时还可以听歌呢？这是因为操作系统是支持并发的，它会把CPU执行时间分为很多小的时间片分给不同的应用程序，然后交替执行代码，所以看上去好像是在同时进行。如今，电脑CPU都是多核的，操作系统可以把一个或者多个核单独分配给一个应用去使用，对于这种情况我们可以认为他是并行。&lt;/p&gt;
&lt;p&gt;从理论上说并行只是并发的一种实现方式，并发是更高维度的概念，说白了，并发只要你能实现“同时”执行的效果，至于物理层面是否是同时并不重要。所以我们网上看到大部分文章都会说并发编程，很少有人会说并行编程。&lt;/p&gt;
&lt;p&gt;其实在如今，很多语言依然不支持并发编程，比如PHP；还有很多语言通过多进程、多线程的方式来实现，比如C++、Java；而Go则是通过协程的方式实现了并发编程。&lt;/p&gt;
    
    </summary>
    
    
      <category term="Golang" scheme="https://wangbenjun.site/categories/Golang/"/>
    
    
      <category term="Golang" scheme="https://wangbenjun.site/tags/Golang/"/>
    
  </entry>
  
  <entry>
    <title>Golang面向对象接口编程</title>
    <link href="https://wangbenjun.site/2021/coding/golang/golang-oop-interface.html"/>
    <id>https://wangbenjun.site/2021/coding/golang/golang-oop-interface.html</id>
    <published>2021-12-15T14:00:00.000Z</published>
    <updated>2024-12-29T03:02:14.809Z</updated>
    
    <content type="html"><![CDATA[<p>说起面向对象（OOP），很多人都听说过封装、继承、多态这些特性，从本质上说面向对象只是一种软件编程思想。但由此衍生出面向对象语言这个概念，其中Java是最典型的代表，是一种完全面向对象的语言，表现在语言层面就有类和对象的设计。</p><p>严格来说Go不是一门完全面向对象的语言，但是在某种层面上实现了面向对象的部分特性，毕竟任何软件工程的主要目标都是为了实现重用性、灵活性和扩展性，Go也不例外。</p><p>举个例子：假设你需要把一个大象放到冰箱里面，需要几步？</p><ul><li><p>第一步：打开冰箱</p></li><li><p>第二步：把大象放进冰箱</p></li><li><p>第三步：关上冰箱</p></li></ul><p>这3个步骤用面向过程的方式去实现可能就是3个函数，比如openFridge、placeElephant、closeFridge，我们只需要依次调用即可。</p><p>但是从面向对象的思维来看，冰箱作为一个对象，它应该有2个函数：open、close，而大象作为一个对象应该有一个函数：walk，我们只需要组合这2个对象的函数就完成这些步骤。</p><a id="more"></a><h2 id="1-Go的面向对象"><a href="#1-Go的面向对象" class="headerlink" title="1.Go的面向对象"></a>1.Go的面向对象</h2><p>Go里面没有类这个概念，只有结构体struct，结构体可以有属性，如：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> Fridge <span class="keyword">struct</span> &#123;</span><br><span class="line">    Name   <span class="keyword">string</span></span><br><span class="line">    Status <span class="keyword">string</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>虽然结构体里面并不能定义函数，但是我们可以给这个结构体定义方法，通过这种形式：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(i Fridge)</span> <span class="title">Open</span><span class="params">()</span></span>  &#123;</span><br><span class="line">    <span class="comment">// open</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(i Fridge)</span> <span class="title">Close</span><span class="params">()</span></span>  &#123;</span><br><span class="line">    <span class="comment">// close</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>通过这种方式我们认为Open和Close是属于Fridge这个结构体的方法，这些加在一起可以比作是面向对象语言里面类、类变量、类方法的概念。非常简单易懂，没有其它面向对象语言里面比如静态类、静态属性等等其它特性。</p><h2 id="2-函数还是方法？"><a href="#2-函数还是方法？" class="headerlink" title="2.函数还是方法？"></a>2.函数还是方法？</h2><p>函数英文是function，方法英文是method，很多人对这2个词概念没有区分，往往都是混着叫，函数方法不分，虽然本质上都是一段代码块，但是在不同的环境下还是略有不同。</p><p>严格来说，方法是面向对象语言里面的概念，它必须属于一个对象，比如Java是一门完全面向对象的语言，所以在Java里面只有方法，没有函数。而函数则是很传统的概念，比如在C语言里面函数是一等公民，所以C里面只有函数。</p><p>回到Go里面，其实也应该区分一下，一般我们说函数，指的是这种不属于任何结构体的函数，可以直接通过包名调用：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">Open</span><span class="params">()</span></span>  &#123;</span><br><span class="line">    <span class="comment">// open</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>而方法则是属于某个结构体的，不能直接调，你得先New一个对象出来，然后再调用这个对象的方法。</p><p>很多语言，比如PHP，既有函数，也有方法，相对来说更加灵活，但是最好还是区分一下，虽然意思大家都懂。</p><h2 id="3-Go的接口"><a href="#3-Go的接口" class="headerlink" title="3.Go的接口"></a>3.Go的接口</h2><p>这里说的接口不是指API接口，而是面向对象里面的接口，也叫interface，如上所说Go虽然不是一个完全面向对象的语言，但是依然提供了接口，虽然Go的接口和其它语言接口不太一样。</p><p>Go的接口被称为是鸭子类型<code>Duck Type</code>，当你看到一只鸟走起来像鸭子、游泳起来像鸭子、叫起来也像鸭子，那么这只鸟就可以被称为鸭子。在鸭子类型中，关注点在于对象的行为，能作什么，而不是关注对象所属的类型。</p><p>在很多面向对象的语言里面，如果你要实现一个接口你就必须实现其所有定义的抽象方法，这是强制要求，而Go则不是这样，Go甚至连implement这个关键字都没有，你不能“实现”接口！</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> Duck <span class="keyword">interface</span> &#123;</span><br><span class="line">    Walk()</span><br><span class="line">    Swim()</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>但只要一个结构体实现了接口定义的所有方法，我们就认为实现了这个接口</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> Dog <span class="keyword">struct</span> &#123;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(i Dog)</span> <span class="title">Walk</span><span class="params">()</span></span>  &#123;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(i Dog)</span> <span class="title">Swim</span><span class="params">()</span></span>  &#123;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="4-为什么需要接口？"><a href="#4-为什么需要接口？" class="headerlink" title="4.为什么需要接口？"></a>4.为什么需要接口？</h2><p>其实这个问题也困扰我很久，很多时候我们在写业务代码几乎用不到接口，大多数都是一些方法和函数的调用，但是在看一些底层库源码的时候却发现处处是接口。</p><p>到底什么时候该用接口? 这是一个非常值得思考的问题</p><p>因为接口这种设计，本质上还是为了灵活性和扩展性，什么时候去用还是得看具体情况，比如配置文件库，往往需要支持json、yaml、ini等多种格式，而一个日志库需要支持console、file、api各种输出方法。这时候就需要利用接口去灵活设计结构，可以实现非常容易的扩展更多类型的目的。</p><p>而过多的使用interface也会导致代码过于冗余，阅读难度增加，变相增加了后续维护成本，实际工作中，公司开发人员水平层次不齐，最简单直白的代码反而更容易被其它人接手维护。</p><p>在我看来，在业务开发代码中，接口最实际的意义其实在于方便写单测，在依赖注入这种实现模式配合下，可以分割不同层之间的依赖，单独对每一层做单测，从而提高代码质量。</p><p>比如在开发中，一个模块依赖另一个模块去实现功能，如果不使用接口做隔离，就很难单独的去做测试：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> ArticleService <span class="keyword">struct</span>&#123;&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">NewArticleService</span><span class="params">()</span> <span class="title">ArticleService</span></span> &#123;</span><br><span class="line">    <span class="keyword">return</span> ArticleService&#123;&#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(i *ArticleService)</span> <span class="title">GetArticles</span><span class="params">()</span> <span class="params">([]<span class="keyword">byte</span>, error)</span></span> &#123;</span><br><span class="line">    articles, err := NewApi().GetArticles()</span><br><span class="line">    <span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="literal">nil</span>, err</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> articles, err</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>在这段代码里面，ArticleService是依赖Api去获取结果的，他们之间是完全依赖耦合的，这样写就很难去单独测试ArticleService的逻辑。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> Api <span class="keyword">struct</span>&#123;&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">NewApi</span><span class="params">()</span> <span class="title">Api</span></span> &#123;</span><br><span class="line">    <span class="keyword">return</span> Api&#123;&#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(i Api)</span> <span class="title">GetArticles</span><span class="params">()</span> <span class="params">([]<span class="keyword">byte</span>, error)</span></span> &#123;</span><br><span class="line">    get, err := http.Get(<span class="string">"https://www.baidu.com"</span>)</span><br><span class="line">    <span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="literal">nil</span>, err</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">defer</span> get.Body.Close()</span><br><span class="line">    all, err := ioutil.ReadAll(get.Body)</span><br><span class="line">    <span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="literal">nil</span>, err</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> all, <span class="literal">nil</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>如果用依赖注入加上接口的方式去改造这些代码，可以这么写：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 定义一个接口</span></span><br><span class="line"><span class="keyword">type</span> ApiInterface <span class="keyword">interface</span> &#123;</span><br><span class="line">    GetArticles() ([]<span class="keyword">byte</span>, error)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">type</span> ArticleService <span class="keyword">struct</span> &#123;</span><br><span class="line">    api ApiInterface</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">NewArticleService</span><span class="params">(api ApiInterface)</span> <span class="title">ArticleService</span></span> &#123;</span><br><span class="line">    <span class="keyword">return</span> ArticleService&#123;api&#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(i *ArticleService)</span> <span class="title">GetArticles</span><span class="params">()</span> <span class="params">([]<span class="keyword">byte</span>, error)</span></span> &#123;</span><br><span class="line">    articles, err := i.api.GetArticles()</span><br><span class="line">    <span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="literal">nil</span>, err</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> articles, err</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>我们定义一个接口，它有一个方法，然后ArticleService依赖这个接口，并且我们在New方法里面通过参数的方式注入这个依赖。</p><p>在使用的时候区别并不大，我们只需要先初始化Api对象，而且作为参数传入ArticleService内部，然后调用就行了。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">    articleService := service.NewArticleService(service.NewApi())</span><br><span class="line">    res, err := articleService.GetArticles()</span><br><span class="line">    <span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">        <span class="built_in">panic</span>(err)</span><br><span class="line">    &#125;</span><br><span class="line">    fmt.Printf(<span class="string">"%s\n"</span>, res)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>但是其实际意义也不仅如此，一个是ArticleService依赖的是一个接口，不是一个具体的对象，这就是所谓的“面向接口编程，而不是实现”。另外，我们可以单独针对ArticleService做测试，可以Mock一个Api对象，实现解耦。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> mockApi <span class="keyword">struct</span> &#123;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(mockApi)</span> <span class="title">GetArticles</span><span class="params">()</span> <span class="params">([]<span class="keyword">byte</span>, error)</span></span> &#123;</span><br><span class="line">    <span class="keyword">return</span> []<span class="keyword">byte</span>(<span class="string">""</span>), <span class="literal">nil</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">TestGetArticles</span><span class="params">(t *testing.T)</span></span> &#123;</span><br><span class="line">    service := NewArticleService(mockApi&#123;&#125;)</span><br><span class="line">    articles, err := service.GetArticles()</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">        t.Fatal(<span class="string">"should be nil"</span>)</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">if</span> <span class="built_in">len</span>(articles) &gt; <span class="number">0</span> &#123;</span><br><span class="line">        t.Fatal(<span class="string">"should be 0"</span>)</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这种写法可以屏蔽外部依赖对测试结果的影响，专注于自身逻辑的测试，这里只是简单的展示这种用法，实际开发中可以使用一些mock库更加方便的测试各种情况。</p>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;说起面向对象（OOP），很多人都听说过封装、继承、多态这些特性，从本质上说面向对象只是一种软件编程思想。但由此衍生出面向对象语言这个概念，其中Java是最典型的代表，是一种完全面向对象的语言，表现在语言层面就有类和对象的设计。&lt;/p&gt;
&lt;p&gt;严格来说Go不是一门完全面向对象的语言，但是在某种层面上实现了面向对象的部分特性，毕竟任何软件工程的主要目标都是为了实现重用性、灵活性和扩展性，Go也不例外。&lt;/p&gt;
&lt;p&gt;举个例子：假设你需要把一个大象放到冰箱里面，需要几步？&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;第一步：打开冰箱&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;第二步：把大象放进冰箱&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;第三步：关上冰箱&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;这3个步骤用面向过程的方式去实现可能就是3个函数，比如openFridge、placeElephant、closeFridge，我们只需要依次调用即可。&lt;/p&gt;
&lt;p&gt;但是从面向对象的思维来看，冰箱作为一个对象，它应该有2个函数：open、close，而大象作为一个对象应该有一个函数：walk，我们只需要组合这2个对象的函数就完成这些步骤。&lt;/p&gt;
    
    </summary>
    
    
      <category term="Golang" scheme="https://wangbenjun.site/categories/Golang/"/>
    
    
      <category term="Golang" scheme="https://wangbenjun.site/tags/Golang/"/>
    
  </entry>
  
  <entry>
    <title>ubuntu添加开机启动</title>
    <link href="https://wangbenjun.site/2021/linux/ubuntu-add-startup.html"/>
    <id>https://wangbenjun.site/2021/linux/ubuntu-add-startup.html</id>
    <published>2021-12-13T12:30:00.000Z</published>
    <updated>2024-12-29T03:02:14.815Z</updated>
    
    <content type="html"><![CDATA[<p>简单说一下Linux设置开机自启的几种方式，以前用Windows很多软件巴不得开机启动，导致开机巨慢，所以每次安装完系统都要各自优化，禁用启动项。</p><p>但是Linux下大部分软件都比较守规矩，没有设置自启，对于一些后台进程类应用，还需要自己手动添加开机自启，下面我就针对Ubuntu桌面演示一下。</p><p>举个例子，Ubuntu桌面下的鼠标移动速度太快，很灵敏，即使在鼠标设置里面把移动速度拉到最低还是很快，这时候只能祭出神器: <strong>xinput</strong>。</p><p>假设最终解决方式是需要使用命令行设置属性，如下：</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">xinput set-prop 10 "Device Accel Velocity Scaling" 1.2</span><br></pre></td></tr></table></figure><p>不过这个命令的效果不是永久的，每次电脑重启就失效了。如果你不嫌麻烦你就每次开机的时候执行一下，但是这显的很蠢。</p><p>在Ubuntu下实现这种开机自启、自动执行的效果有很多种方式，下面咱就一一介绍。</p><a id="more"></a><h2 id="1-rc-local"><a href="#1-rc-local" class="headerlink" title="1.rc.local"></a>1.rc.local</h2><p>这是我认为最简单的一种方式，rc.local是属于systemctl系统里面的一个配置文件，其完整路径是 <strong>&#x2F;etc&#x2F;rc.local</strong>。systemctl这个东西讲起来就有点复杂了，这里就不多说了，你只需要知道，但凡是写在这个文件里面的命令行在开机启动的时候都会被执行。</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#</span><span class="bash">!/bin/sh -e</span></span><br><span class="line"><span class="meta">#</span></span><br><span class="line"><span class="meta">#</span><span class="bash"> rc.local</span></span><br><span class="line"><span class="meta">#</span></span><br><span class="line"><span class="meta">#</span><span class="bash"> This script is executed at the end of each multiuser runlevel.</span></span><br><span class="line"><span class="meta">#</span><span class="bash"> Make sure that the script will <span class="string">"exit 0"</span> on success or any other</span></span><br><span class="line"><span class="meta">#</span><span class="bash"> value on error.</span></span><br><span class="line"><span class="meta">#</span></span><br><span class="line"><span class="meta">#</span><span class="bash"> In order to <span class="built_in">enable</span> or <span class="built_in">disable</span> this script just change the execution</span></span><br><span class="line"><span class="meta">#</span><span class="bash"> bits.</span></span><br><span class="line"><span class="meta">#</span></span><br><span class="line"><span class="meta">#</span><span class="bash"> By default this script does nothing.</span></span><br><span class="line"></span><br><span class="line">exit 0</span><br></pre></td></tr></table></figure><p>默认情况下，这个文件是空的，只有一些注释，我们只需要把要执行的命令写在 exit 0 之前就可以了。</p><h2 id="2-开机自启应用"><a href="#2-开机自启应用" class="headerlink" title="2.开机自启应用"></a>2.开机自启应用</h2><p>上面所说的方式很多Linux发行版都支持，而这条是针对Ubuntu桌面而言的，系统自带了一个名为“启动应用程序”的软件，打开这个软件会发现里面列出了一些启动程序。</p><img src="/images/2021/2021-12-13_20-31.png" /><p>在右边还有3个按钮，添加、编辑、删除，功能十分简单，只要在命令里面填入想要执行的命令即可，或者选择需要运行的脚本文件（先把命令写到脚本文件里面）。</p><p>可能有人好奇，这个保存到哪里去了？我研究了一下发现是保存到 <strong>~&#x2F;.config&#x2F;autostart</strong> 目录里面了，是一个desktop文件，你甚至可以手动创建一个这样的文件放进去。</p><h2 id="3-systemctl"><a href="#3-systemctl" class="headerlink" title="3.systemctl"></a>3.systemctl</h2><p>systemctl是一个非常复杂的Linux管理系统，咱们也不用了解太多。如果想实现开机自启，只需要自己创建一个自定义的service，添加到系统里面即可。</p><p>这种方式不仅可以实现开机自启，还可以实现 <strong>service xxx re\start</strong>、<strong>service xxx stop</strong> 这种形式的管理，非常方便。</p><p>在Ubuntu里面，service文件有2个存放的地方，一个是 <strong>&#x2F;etc&#x2F;systemd&#x2F;system</strong>，另一个是 <strong>&#x2F;lib&#x2F;systemd&#x2F;system</strong>，如果你打开看一下，会发现里面已经有很多服务在了，不少老熟人。其实Linux里面大多数系统级别的服务都是通过这种形式启动的。</p><p>咱们先看一个案例，比如 <strong>nginx.service</strong> 文件的内容：</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line">[Unit]</span><br><span class="line">Description=A high performance web server and a reverse proxy server</span><br><span class="line">After=network.target</span><br><span class="line"></span><br><span class="line">[Service]</span><br><span class="line">Type=forking</span><br><span class="line">PIDFile=/run/nginx.pid</span><br><span class="line">ExecStartPre=/usr/sbin/nginx -t -q -g 'daemon on; master_process on;'</span><br><span class="line">ExecStart=/usr/sbin/nginx -g 'daemon on; master_process on;'</span><br><span class="line">ExecReload=/usr/sbin/nginx -g 'daemon on; master_process on;' -s reload</span><br><span class="line">ExecStop=-/sbin/start-stop-daemon --quiet --stop --retry QUIT/5 --pidfile /run/nginx.pid</span><br><span class="line">TimeoutStopSec=5</span><br><span class="line">KillMode=mixed</span><br><span class="line"></span><br><span class="line">[Install]</span><br><span class="line">WantedBy=multi-user.target</span><br></pre></td></tr></table></figure><p>这个service文件是由3个部分组成，其中Unit里面字段主要是描述服务以及声明服务的依赖关系，而Install里面只是声明一个安装的时候的依赖，最核心的是Service里面定义的字段。</p><p>篇幅有限，这里咱也不一一介绍了，如果你想知道这些字段的详细含义，请参考一份专业的文档：<a href="http://www.jinbuguo.com/systemd/systemd.service.html" target="_blank" rel="noopener">http://www.jinbuguo.com/systemd/systemd.service.html</a></p><p>总之，如果你只想实现开机自动执行这么一个最简单的功能，只需要这么写：</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">[Unit]</span><br><span class="line">Description=简单的Foo服务</span><br><span class="line"></span><br><span class="line">[Service]</span><br><span class="line">ExecStart=/usr/sbin/foo-daemon</span><br><span class="line"></span><br><span class="line">[Install]</span><br><span class="line">WantedBy=multi-user.target</span><br></pre></td></tr></table></figure><p>创建一个”foo.service”文件，写入上面的内容，然后把这个文件移动到 <strong>&#x2F;etc&#x2F;systemd&#x2F;system</strong> 目录里面，再执行enable命令开启服务，不出问题的话，应该会有以下信息：</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">jwang@jwang:~$ systemctl enable foo.service</span><br><span class="line">Created symlink from /etc/systemd/system/multi-user.target.wants/foo.service to /etc/systemd/system/foo.service.</span><br></pre></td></tr></table></figure><p>其实大多数情况下使用service这种方式更多的是为了更改方便的启动、停止、重启应用，而不仅仅是为了实现自启，单纯为了实现自启我还是推荐前面2种更加简单的方式。</p>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;简单说一下Linux设置开机自启的几种方式，以前用Windows很多软件巴不得开机启动，导致开机巨慢，所以每次安装完系统都要各自优化，禁用启动项。&lt;/p&gt;
&lt;p&gt;但是Linux下大部分软件都比较守规矩，没有设置自启，对于一些后台进程类应用，还需要自己手动添加开机自启，下面我就针对Ubuntu桌面演示一下。&lt;/p&gt;
&lt;p&gt;举个例子，Ubuntu桌面下的鼠标移动速度太快，很灵敏，即使在鼠标设置里面把移动速度拉到最低还是很快，这时候只能祭出神器: &lt;strong&gt;xinput&lt;/strong&gt;。&lt;/p&gt;
&lt;p&gt;假设最终解决方式是需要使用命令行设置属性，如下：&lt;/p&gt;
&lt;figure class=&quot;highlight shell&quot;&gt;&lt;table&gt;&lt;tr&gt;&lt;td class=&quot;gutter&quot;&gt;&lt;pre&gt;&lt;span class=&quot;line&quot;&gt;1&lt;/span&gt;&lt;br&gt;&lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;code&quot;&gt;&lt;pre&gt;&lt;span class=&quot;line&quot;&gt;xinput set-prop 10 &quot;Device Accel Velocity Scaling&quot; 1.2&lt;/span&gt;&lt;br&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/figure&gt;
&lt;p&gt;不过这个命令的效果不是永久的，每次电脑重启就失效了。如果你不嫌麻烦你就每次开机的时候执行一下，但是这显的很蠢。&lt;/p&gt;
&lt;p&gt;在Ubuntu下实现这种开机自启、自动执行的效果有很多种方式，下面咱就一一介绍。&lt;/p&gt;
    
    </summary>
    
    
      <category term="Linux" scheme="https://wangbenjun.site/categories/Linux/"/>
    
    
      <category term="Linux" scheme="https://wangbenjun.site/tags/Linux/"/>
    
      <category term="Ubuntu" scheme="https://wangbenjun.site/tags/Ubuntu/"/>
    
  </entry>
  
  <entry>
    <title>ubuntu unity桌面常见应用</title>
    <link href="https://wangbenjun.site/2021/linux/ubuntu-unity-application.html"/>
    <id>https://wangbenjun.site/2021/linux/ubuntu-unity-application.html</id>
    <published>2021-12-11T10:00:00.000Z</published>
    <updated>2025-01-24T13:37:52.234Z</updated>
    
    <content type="html"><![CDATA[<p>这篇文章是承接上面那篇文章：<a href="https://wangbenjun.site/2021/os/ubuntu-unity-setup.html">ubuntu unity桌面设置攻略</a> 的后续，主要内容是讲一下我个人在使用Ubuntu桌面系统的时候常用的软件和设置。</p><p>这里记录一下也是方便以后自己查阅，或者他人参考之用，仅此而已。</p><p>不过在开始看文章之前，我先说一个大的前提，很多人都知道Linux下安装软件，大部分时候只需要使用命令行即可，比如：“sudo apt install git”。</p><p>使用命令行没问题，但是由于Ubuntu16.04的版本比较老，其源里面的很多软件版本已经比较旧了，我比较喜欢最新的版本，很多老牌的知名开源软件都有官网，上面会有最新的下载地址，有些还会提供一个PPA源，只要添加到系统里面，以后还可以收到更新。</p><p>所以我一般遵循以下步骤：</p><ol><li>如果是简单的软件，比如git、vim、htop等，我直接使用命令行安装</li><li>如果是复杂的软件，比如浏览器、下载工具，我一般去其官网或者官方文档寻找PPA源、或者最新的deb安装包</li></ol><a id="more"></a><h2 id="1-浏览器"><a href="#1-浏览器" class="headerlink" title="1.浏览器"></a>1.浏览器</h2><p>浏览器我只推荐Chrome，而且必须得搞一个梯子，本人的梯子是自建的，VPS从vutlr上面买的，5美元一个月，网上有很多v2ray的一键脚本，整一下也花不了多少时间，自己搞很稳定也安全。</p><p>Chrome配上Google账号开启同步巨好用，可以自动记录网站密码，每次都不用手动输了，方便。</p><p>Ubuntu自带的火狐浏览器也是一个选择，现在连Edge、360都也出了Linux版本，其实都是套的chromium内核。</p><p>Ubuntu的unity桌面有一个好处就是Chrome的菜单栏可以和系统融为一体，鼠标右键菜单选择“使用系统菜单栏和标题”即可开启。</p><img src="/images/2021/2021-12-11_18-05.png" /><p>其实在如今Web大行其道的情况下，几乎90%以上的操作都可以在浏览器上面完成，我每天用的最多的就是浏览器，好在Chrome对Linux的支持非常完美。</p><p>下载的话建议从 <a href="https://www.google.cn/chrome/" target="_blank" rel="noopener">Chrome官网</a> 下载，这个地址现在没有并屏蔽，不需要FQ就可以下载。</p><h2 id="2-输入法"><a href="#2-输入法" class="headerlink" title="2.输入法"></a>2.输入法</h2><p>Linux下的输入法架构分为2大体系：IBus 和 Fcitx。Ubuntu默认是Fcitix体系，自带一个中文输入法不是非常好用，我推荐安装搜狗输入法，相比较于Windows版本来说没有任何广告弹窗，登录同步、皮肤基本功能都有，很良心了。</p><img src="/images/2021/2021-12-11_18-14.png" /><p>建议从搜狗输入法官网下载deb安装包，地址如下：<a href="https://pinyin.sogou.com/linux/?r=pinyin" target="_blank" rel="noopener">https://pinyin.sogou.com/linux/?r=pinyin</a></p><h2 id="3-办公文档"><a href="#3-办公文档" class="headerlink" title="3.办公文档"></a>3.办公文档</h2><p>Ubuntu系统默认自带的LibreOffice着实难用，前面我已经卸载了，所以需要安装一个替代品，只有一个选择：WPS</p><p>话说Linux版本的wps也是相当良心，没有任何广告和弹窗，我发现很多国产软件到了Linux平台都变的良心了，可能是用户基数太少，不屑于通过广告方式盈利吧</p><p>如果不是文档重度用户，一般情况下WPS还是够用的，实在不行，还有一些网页在线的文档内平台可以使用。安装的话建议从其官网下载，这里就不过多介绍了。</p><h2 id="4-下载工具"><a href="#4-下载工具" class="headerlink" title="4.下载工具"></a>4.下载工具</h2><p>主要有2个，一个是 uget，一个是 qBittorrent，前面那个用于http下载，后面这个主要用于BT下载，该有的功能都有，还是挺全的。</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">sudo apt install uget</span><br><span class="line">sudo add-apt-repository ppa:qbittorrent-team&#x2F;qbittorrent-stable</span><br><span class="line">sudo apt-get update &amp;&amp; sudo apt-get install qbittorrent</span><br></pre></td></tr></table></figure><img src="/images/2021/2021-12-11_18-35.png" /><p>然而，经常下载电影的同学可能知道，很多网站的链接都是迅雷专用的，使用这些工具都无法成功下载、还有一些是百度云的，百度云实际上已经有Linux的客户端了，大家可以去官网下载。</p><p>但是迅雷几乎是无解的，这里我一般采用虚拟机解决，我用 <strong>Oracle VirtualBox</strong>，这是一招釜底抽薪，专治各种不服，不过我经常拿来下载用，也还可以用来安装一些Windows的软件。</p><img src="/images/2021/2021-12-11_18-39.png" /><p>注意，如果你是拿来下载，我一般是在虚拟机设置里面设置一个共享文件夹，然后用迅雷直接下载到宿主机的共享文件夹里面，不用来回拷贝了。同时开启剪切板共享，在浏览器复制的地址直接就可用在虚拟机里面使用，还是很方便的。</p><h2 id="5-聊天工具"><a href="#5-聊天工具" class="headerlink" title="5.聊天工具"></a>5.聊天工具</h2><p>国外的telegram还真有Linux原生客户端，国内的聊天工具，微信、QQ、钉钉无一例外是没有Linux客户端的，但是也有解决方案，那就是deepin的移植版。</p><p>相信大家都听说过Wine，它可以实现在Linux、Mac上面跑Windows的软件，但是实际应用中还存在很多问题，直接跑的话很多软件都有问题，但是有一个国产的Linux发行版deepin解决了这些问题，由于deepin也是基于Debian的，所以Ubuntu拿来可以直接用，有大神把这些软件移植过来了。</p><p>Github地址： <a href="https://github.com/wszqkzqk/deepin-wine-ubuntu" target="_blank" rel="noopener">https://github.com/wszqkzqk/deepin-wine-ubuntu</a></p><p>通过这种方式可以运行的国产软件还挺多的，不过我用的最多只有微信，毕竟目前也只有微信是强需求，其它无所谓了，个人使用感觉还是非常流畅的，95%功能都正常，建议大家尝试。</p><img src="/images/2021/2021-12-11_18-51.png" /><h2 id="6-影音播放"><a href="#6-影音播放" class="headerlink" title="6.影音播放"></a>6.影音播放</h2><p>Ubuntu自带的2个软件之前已经卸载了，所以这里使用了我觉得体验比较好的替代品，视频播放这块推荐vlc或smplayer，这2个都是老牌的跨平台开源软件，功能都很强大。</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">sudo add-apt-repository ppa:rvm/smplayer </span><br><span class="line">sudo apt-get update </span><br><span class="line">sudo apt-get install smplayer smplayer-themes smplayer-skins </span><br><span class="line"></span><br><span class="line">sudo apt install vlc</span><br></pre></td></tr></table></figure><p>我一般使用smplayer，简单设置一下皮肤，还是挺好看的，播放视频也很流畅</p><img src="/images/2021/2021-12-11_19-01.png" /><p>音乐播放器，针对国内的话，网易云音乐官方有Ubuntu版本，但是还是有问题，最新版本不能在Ubuntu16.04上面跑，只能用早期的1.0版本，可以凑合用一下。</p><img src="/images/2021/2021-12-11_19-31.png" /><p>在Github上面，有一些大神开发的第三方客户端，比如这个：<a href="https://github.com/trazyn/ieaseMusic" target="_blank" rel="noopener">https://github.com/trazyn/ieaseMusic</a></p><p>如今听歌不是一个播放器的问题，而是歌的问题，版权都在平台手里，你不可能都下载下来使用本地播放器播放，我一般都用在线的听，没啥追求，也没什么好的音响设备，其实就是听个响。</p><h2 id="7-远程工具"><a href="#7-远程工具" class="headerlink" title="7.远程工具"></a>7.远程工具</h2><p>偶尔需要给朋友远程一下解决一些电脑问题，跨平台的我只推荐 <strong>Teamviewer</strong>，没有之一，简单易用，跨平台，而且还免费，比QQ远程好的太多了，即使不是Linux我也推荐使用。</p><img src="/images/2021/2021-12-11_19-51.jpeg" /><p>有需求的同学可以试一试</p><h2 id="8-影音视频编辑"><a href="#8-影音视频编辑" class="headerlink" title="8.影音视频编辑"></a>8.影音视频编辑</h2><p>这一块其实比较小众了，大部分人其实都用不到，这里列出来只是给大家看一下Linux的软件生态，其实并不差，特别是国外的软件很多的，只是国内很多软件厂商不支持。</p><p>比如视频剪切编辑可以用 <strong>Kdenlive</strong>、音频编辑软件 <strong>audacity</strong>、图片编辑软件 <strong>GIMP</strong>、视频特效软件 <strong>DaVinci</strong>（达芬奇）、还有3D建模渲染软件<strong>blender</strong>等等，很多都是工业级的软件。</p><img src="/images/2021/2021-12-11_19-46.png" /><img src="/images/2021/2021-12-11_20-51.jpeg" /><p>平时大家用的更多的可能是Adobe系列软件，所以Linux平台下这些软件几乎没人用，也不会用，但是不代表没有。。。</p><h2 id="9-截图工具"><a href="#9-截图工具" class="headerlink" title="9.截图工具"></a>9.截图工具</h2><p>Ubuntu系统自带一个截图工具，但是不是非常好用，这里推荐一个Github上面几万Star的开源截图工具: <a href="https://github.com/flameshot-org/flameshot" target="_blank" rel="noopener">flameshot</a></p><p>这个截图工具个人感觉非常好用，但是最新版本已经不支持Ubuntu16.04了，可以下载一个早期的版本，然后设置一下快捷键，如图所示，我设置成 <strong>Ctrl+Alt+A</strong>。</p><img src="/images/2021/2021-12-11_20-44.png" /><p>这样设置完成之后，我无论在哪里只要按下快捷键就可以区域截图，有一点不好的是这个软件不支持应用窗口截图，原因好像是系统限制，微信自带的截图工具也不支持，个人觉得影响不大，只是每次需要自己手动框一下。我这篇文章里面的大部分截图都是这个工具完成的。</p><p>另外，这个软件在4k屏幕缩放下会有问题，如果你设置了QT缩放的环境变量的话，只能截一部分，这个软件的界面是使用QT开发的，不知道为什么会出现这种情况。</p><p>不过好在我找到了解决方案，只要unset掉QT那些缩放比例设置就行，由于我那个缩放设置是全局的，所以我单独写了个脚本重新设置了这个比例，然后再启动flameshot，解决了这个问题</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">export QT_DEVICE_PIXEL_RATIO&#x3D;1 &amp;&amp; flameshot &amp;</span><br></pre></td></tr></table></figure><h2 id="10-软件编程开发"><a href="#10-软件编程开发" class="headerlink" title="10.软件编程开发"></a>10.软件编程开发</h2><p>众所周知，大部分人使用Linux都是为了编程，本人也是，但是我现在日常生活也使用Ubuntu了，习惯了，所以咱们自然少不了编程开发用到软件。</p><p>首先，先说一下开发环境的问题，虽然Linux下使用命令行就可以安装绝大部分语言的运行环境，比如PHP、Node、Golang等，但是在Ubuntu16.04上面你会发现这个源的软件好像有点老了。。。</p><p>所以我现在基本上都是去官网下载二进制包，然后配置一下环境变量即可，Linux下配置环境变量的方式非常多：</p><ul><li>直接在命令行 export PATH&#x3D;$PATH:&#x2F;env&#x2F;path ，只在当前命令行窗口有效，退出就失效</li><li>修改 ～&#x2F;.bashrc 文件，把上面那行命令加到最后就行，这是针对每个用户的</li><li>修改 ~&#x2F;.profile文件，和上面一样</li><li>修改 &#x2F;etc&#x2F;profile文件，这是全局的，这样做会影响所有登录用户</li><li>修改 &#x2F;etc&#x2F;environment, 和上面一样</li></ul><p>我一般喜欢在后面2个&#x2F;etc配置文件里面改，把那些语言的解释器或者编译器放到PATH里面。</p><p>此外，在编程工具这块，我强烈建议使用 <strong>JetBrains</strong>全家桶，如果你还在纠结正版问题，可以看一下这篇文章：<a href="https://wangbenjun.site/2020/os/free-jetbrains-ide.html">如何免费使用正版Jetbrains</a></p><img src="/images/2021/2021-12-11_20-09.png" /><p>可能有人觉得JetBrains家的IDE过于重量级，确实，它们最大的缺点就是使用了Java开发，导致非常耗资源，机器配置低的话会比较卡。</p><p>不过你还可以选择另一个神器<strong>VScode</strong>，微软出品的编辑器，装上一堆扩展，功能不亚于IDE</p><p>此外，还有 Atom、Sublime Text这类轻量级编辑器供你选择，总之，Linux下的编程工具绝对够用，就看你怎么选了。</p>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;这篇文章是承接上面那篇文章：&lt;a href=&quot;https://wangbenjun.site/2021/os/ubuntu-unity-setup.html&quot;&gt;ubuntu unity桌面设置攻略&lt;/a&gt; 的后续，主要内容是讲一下我个人在使用Ubuntu桌面系统的时候常用的软件和设置。&lt;/p&gt;
&lt;p&gt;这里记录一下也是方便以后自己查阅，或者他人参考之用，仅此而已。&lt;/p&gt;
&lt;p&gt;不过在开始看文章之前，我先说一个大的前提，很多人都知道Linux下安装软件，大部分时候只需要使用命令行即可，比如：“sudo apt install git”。&lt;/p&gt;
&lt;p&gt;使用命令行没问题，但是由于Ubuntu16.04的版本比较老，其源里面的很多软件版本已经比较旧了，我比较喜欢最新的版本，很多老牌的知名开源软件都有官网，上面会有最新的下载地址，有些还会提供一个PPA源，只要添加到系统里面，以后还可以收到更新。&lt;/p&gt;
&lt;p&gt;所以我一般遵循以下步骤：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;如果是简单的软件，比如git、vim、htop等，我直接使用命令行安装&lt;/li&gt;
&lt;li&gt;如果是复杂的软件，比如浏览器、下载工具，我一般去其官网或者官方文档寻找PPA源、或者最新的deb安装包&lt;/li&gt;
&lt;/ol&gt;
    
    </summary>
    
    
      <category term="Linux" scheme="https://wangbenjun.site/categories/Linux/"/>
    
    
      <category term="Linux" scheme="https://wangbenjun.site/tags/Linux/"/>
    
      <category term="Ubuntu" scheme="https://wangbenjun.site/tags/Ubuntu/"/>
    
  </entry>
  
  <entry>
    <title>ubuntu unity桌面设置攻略</title>
    <link href="https://wangbenjun.site/2021/linux/ubuntu-unity-setup.html"/>
    <id>https://wangbenjun.site/2021/linux/ubuntu-unity-setup.html</id>
    <published>2021-12-11T04:00:00.000Z</published>
    <updated>2025-01-24T13:37:52.381Z</updated>
    
    <content type="html"><![CDATA[<p>难道只有我一个人觉得Ubuntu的unity桌面非常好用吗？</p><p>最近把台式机上面的Ubuntu 16.04格式化了，装了黑苹果用了一周，不得不说，MacOS确实很精美，软件生态比Linux丰富很多，比Windows简单安全很多，但是我最终还是不想用了，原因很多，究其原因，我觉得是因为Mac是一个为触摸操作优化的系统，没有触摸板就像是断条腿。</p><p>所以，我决定重回Linux，继续用Ubuntu，期间尝试了最新的Ubuntu 20.04版本，发现Gnome桌面模仿Mac系统的痕迹太明显了，扁平化的UI设计真的欣赏不来，最要命的是我总感觉一卡一卡，没有unity桌面流畅，真的不是我的错觉，我之前也尝试过18.04版本，那会感觉也是很卡。</p><p>最后想了想，还是回到unity桌面吧，不再折腾了，奈何unity已经被抛弃，只能死守16.04这个版本，虽然看起来有点老了，但是用起来完全没有任何问题。装个系统对于我来说是非常轻松的事情，但最麻烦的是我个人的一些特殊设定、常用软件的安装配置比较耗时，所以这篇文章的主要目的就在于记录整个过程，结合我之前写的一些文章，作为一个记录，也可以供需要的参考一下，如果你喜欢unity桌面的话。</p><blockquote><p>以下文章的内容仅针对Ubuntu的unity桌面，准确的说是16.04版本，因为之后的版本都是Gnome桌面，基本上不适用了，Only For Ubuntu16.04。</p></blockquote><a id="more"></a><h2 id="1-系统安装"><a href="#1-系统安装" class="headerlink" title="1.系统安装"></a>1.系统安装</h2><p>其实系统的安装非常简单，网上教程也很多，这里就不再赘述了，简单总结一下过程：</p><ul><li>下载ISO镜像，从Ubuntu官网下载即可</li><li>制作U盘启动盘，需要一个至少4GB的U盘，在Windows下我一般使用ultraIso软碟通制作，Ubuntu下的话可以用系统自带的启动盘工具</li><li>BIOS设定，如果启动失败可以尝试修改一下，比如关闭安全启动</li><li>成功引导进入安装界面后，一步步按照引导步骤来就可以</li><li>安装完成后，拔掉U盘重启即可</li></ul><p>如果你是安装双系统的话，现在的Ubuntu安装程序非常智能，可以检测到你已有系统，需要注意在安装的时候选择”与现有系统共存“即可。</p><img src="/images/2021/2021-12-11_12-03.png" /><h2 id="2-卸载多余软件"><a href="#2-卸载多余软件" class="headerlink" title="2.卸载多余软件"></a>2.卸载多余软件</h2><p>在Ubuntu 16.04的安装程序里面还没有最小安装的选项，所以默认情况下还会安装很多自带的软件应用，我一般都会选择卸载掉，节省空间，系统也更加简洁，建议先从这里开始，主要包括以下软件：</p><ul><li>文档套件libreOffice，可以使用WPS替代</li><li>视频播放器Totem，可以使用smplayer或者vlc替代</li><li>音乐播放器Rhythmbox，不好用</li><li>邮件软件Thunderbird，我一般都用网页版</li><li>图片管理器ShotWell，用不上，卸载不影响图片浏览</li><li>摄像头软件Cheese，用不上，我电脑没摄像头</li><li>BT下载软件Transmission，可以使用QTbittorrent替代</li></ul><p>以上软件都是我个人觉得不好用、用不上的，我一般是通过命令行卸载，我把Shell命令总结了一下，需要的可以直接复制：</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#</span><span class="bash">!/bin/bash</span></span><br><span class="line">sudo apt purge libreoffice-*</span><br><span class="line">sudo apt purge totem</span><br><span class="line">sudo apt purge rhythmbox</span><br><span class="line">sudo apt purge thunderbird</span><br><span class="line">sudo apt purge shotwell</span><br><span class="line">sudo apt purge cheese</span><br><span class="line">sudo apt purge transmission-gtk</span><br></pre></td></tr></table></figure><p>卸载完之后建议重启一下，因为有些软件的图标缓存没刷新。</p><h2 id="3-更新系统脚本"><a href="#3-更新系统脚本" class="headerlink" title="3.更新系统脚本"></a>3.更新系统脚本</h2><p>你从官网下载的镜像，一般都不是最新的，所以有时候安装完成之后就需要更新，可能已经弹出提示让你更新了，我一般喜欢写一个脚本文件来实现一键更新、卸载、清理，脚本内容如下：</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#</span><span class="bash">!/bin/bash</span></span><br><span class="line">sudo apt -y update</span><br><span class="line">sudo apt -y upgrade</span><br><span class="line">sudo apt -y dist-upgrade</span><br><span class="line">sudo apt -y autoremove</span><br><span class="line">sudo apt -y autoclean</span><br></pre></td></tr></table></figure><p>这个几个命令主要是更新仓库缓存、升级软件、删除无用的依赖包、自动清理。</p><p>我一般会把这几个命令放到一个文件，比如名字叫update，然后放到一个文件夹里面比如 <strong>&#x2F;home&#x2F;jwang&#x2F;Documents&#x2F;MyBin</strong></p><p>然后在设置一下环境变量，把这个文件夹目录放到PATH里面，我一般喜欢改 <strong>&#x2F;etc&#x2F;environment</strong> 这个文件，这样以后我就可以直接在终端里面用<strong>update</strong>命令去更新系统。</p><p>说到更新，不得不说Ubuntu的更新源的问题，如果你安装的时候选择的是中文，并且时区也没问题，默认情况下应该是cn的中文源，这个源的速度还行，但是有时候可能也很慢，这时候就得换第三方源了，比如阿里云的源、清华大学的源等等，国内有很多。</p><img src="/images/2021/2021-12-10_21-00.png" /><p>网上有很多教程是直接修改source文件的，比较麻烦，而且容易出错，这里告诉大家一起最简单的方式，打开软件和更新设置，里面有一个选择更新源的选项，选择其它源就可以看到国内的所有认证过的第三方源，mirrors开头的都是，选择一个就行，也可以点击右边的按钮，自动选择的一个最快的源。</p><h2 id="4-sudo免密设置"><a href="#4-sudo免密设置" class="headerlink" title="4.sudo免密设置"></a>4.sudo免密设置</h2><p>这个主要是为了方便，毕竟咱是作为个人电脑，天天输密码也没啥意义，看个人需求，如果为了安全考虑就别改了，但我每次都会改。</p><p>方法很简单，在命令行输入 <strong>sudo visudo</strong> 会打开一个文件的编辑界面，在最后的位置增加一行：</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">your_user_name   ALL=(ALL:ALL) NOPASSWD:ALL</span><br></pre></td></tr></table></figure><p>your_user_name代表你自己的登录用户名，不要照抄。同时我建议注释掉上方3个default开头的配置项，这块主要是为了解决使用sudo找不到命令的问题，详细的原因可以参考我之前的文章</p><p>有很多人不会用nano这个编辑器，修改完成之后，只要按ctrl+x，选择y，回车保存即可。</p><h2 id="5-屏幕缩放比例"><a href="#5-屏幕缩放比例" class="headerlink" title="5.屏幕缩放比例"></a>5.屏幕缩放比例</h2><p>这一块是针对高分屏用户的，比如4k分辨率的显示器，我用的就是，默认情况下字体很小，得设置一下缩放。</p><p>这里先教大家一个最简单的方式，只需要在显示设置里面，设置一下”菜单和标题缩放比例“即可，根据我的感觉，4k屏幕设置成1.75比较合适，大家根据自己的情况调节，适合自己最好。</p><img src="/images/2021/2021-12-10_21-05.png" /><p>这里调节的话基本上能够解决90%软件的缩放问题，但是还是有很多软件不生效，比如QT开发的、Wine下的软件，这里推荐大家看一下我以前总结的文章：<a href="https://wangbenjun.site/2019/linux/ubuntu-4k-scale.html">Ubuntu 4K显示器缩放设置</a></p><h2 id="6-默认设置调节"><a href="#6-默认设置调节" class="headerlink" title="6.默认设置调节"></a>6.默认设置调节</h2><p>这里是针对系统默认设置的一些调节，主要是根据我个人习惯，大家随意，适合自己就好，并不是必须的设置，主要包括以下：</p><ul><li><p>安全和隐私。关闭最近文件使用记录、关闭休眠后需要密码选项，因为本人在家使用，图个方便。</p></li><li><p>外观。在行为里面开启自动隐藏启动器，把灵敏度调到最低，如果需要显示启动器只要按一下win键即可，个人感觉这样更简洁。关于窗口菜单这块，我喜欢在窗口标题显示，Ubuntu默认是在最上面，和Mac一样，个人感觉不方便，特别是当你有一个大屏幕，打开一些窗口化的小应用的时候会发现菜单离你好远。不过好在Ubuntu提供了设置选项，不像Mac直接定死了，不可修改。</p></li><li><p>时间和日期。可以设置显示月日以及星期，方便查看</p></li><li><p>账号里面可以设置一下自动登录，这样每次重启的时候就不用输密码，还是为了方便，我是家用，如果在公司可不要这么干。</p></li></ul><img src="/images/2021/2021-12-10_21-18.png" /><h2 id="7-快捷键调节"><a href="#7-快捷键调节" class="headerlink" title="7.快捷键调节"></a>7.快捷键调节</h2><p>在系统设置键盘设置里面可以设置一些快捷键，但是这些都无关精要，我实际上想实现一个<strong>Win+D</strong>显示桌面的快捷键，这里无法实现，必须得上神器<strong>compiz</strong></p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">sudo apt install compiz-plugins compiz-plugins-extra compizconfig-settings-manager</span><br></pre></td></tr></table></figure><p>安装完成后打开compiz设置管理器，这个软件功能十分强大，建议不懂的话不要乱动，我这里只需要改一丁点地方，找到<strong>Ubuntu Unity Plugin</strong>,在里面找到Show Deskto的设置项，其默认值是Ctrl+Super+D，其中Super就是Win键，我们这里给改成Win+D，这是Windows默认的快捷键，保持统一，约定大于规则。</p><p>其它快捷键有需要的自行修改，我一般还喜欢改一下启动器的快捷键，之前说过默认是Win键，我喜欢改成Alt+Q</p><h2 id="8-unity-tweak-tool"><a href="#8-unity-tweak-tool" class="headerlink" title="8.unity-tweak-tool"></a>8.unity-tweak-tool</h2><p>这里顺便说一下另一个神器<strong>unity-tweak-tool</strong>，功能十分强大，可以修改很多系统设置选项里面没有暴露出来的东西</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">sudo apt install unity-tweak-tool</span><br></pre></td></tr></table></figure><p>比如说工作区的一些调节选项，工作区是Ubuntu非常厉害的功能，也是Mac系统杀手锏，感兴趣的同学可以了解一下，这里不细说了。</p><img src="/images/2021/2021-12-10_21-36.png" /><p>这个软件还可以调节系统的主题、字体等设置，非常有用。</p><h2 id="9-网速监控插件"><a href="#9-网速监控插件" class="headerlink" title="9.网速监控插件"></a>9.网速监控插件</h2><p>Linux上面的网速监控插件有很多，但是能和Ubuntu系统完美结合的我只找到这个:</p><img src="/images/2021/2021-12-10_21-45.png" /><p>这个插件功能简单，2个箭头，一个上传一个下载，可以选择监控哪个网卡，也可以选择监控所有网卡，没有其它多余功能！安装的话需要加一个ppa源：</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">sudo add-apt-repository ppa:nilarimogard/webupd8</span><br><span class="line">sudo apt update</span><br><span class="line">sudo apt-get install indicator-netspeed</span><br></pre></td></tr></table></figure><p>安装重启即可，非常实用，对于我来说是必备</p><h2 id="10-鼠标灵敏度问题"><a href="#10-鼠标灵敏度问题" class="headerlink" title="10.鼠标灵敏度问题"></a>10.鼠标灵敏度问题</h2><p>默认情况下，你的鼠标可能会很飘…特别灵敏，就算你在鼠标设置里面把灵敏度拉到最低依然很快，如果是这样你就需要用命令行去调节一下了，Ubuntu有一个xinput命令 可以列出所有设备和其属性，找到你鼠标的id,然后使用下面命令设置灵敏度：</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">xinput --set-prop 10 "Device Accel Constant Deceleration" 1.2</span><br><span class="line">xinput --set-prop 10 "Device Accel Velocity Scaling" 1</span><br></pre></td></tr></table></figure><p>10是我的鼠标在系统里面的id，后面一个 1.2 和 1 这2个参数大家可以自己调整，找到适合自己的就行，为了每次开机自动设置，我们还需要配置一个开机自启脚本，这里面提供一个简单方法 Ubuntu自带一个名叫”启动应用程序“的软件，比较简单，我们可以添加自己编写的脚本：</p><p>另外，你可能发现你的鼠标滚轮滚起来非常慢，特别是在浏览网页的时候，很可惜Ubuntu的鼠标设置界面并没有提供修改的地方，但是也有办法解决：</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">sudo apt install imwheel</span><br></pre></td></tr></table></figure><p>然后在用户目录下创建一个vim .imwheelrc配置文件，写入一下配置：</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">&quot;.*&quot;</span><br><span class="line">None,      Up,   Button4, 4</span><br><span class="line">None,      Down, Button5, 4</span><br><span class="line">Control_L, Up,   Control_L|Button4</span><br><span class="line">Control_L, Down, Control_L|Button5</span><br><span class="line">Shift_L,   Up,   Shift_L|Button4</span><br><span class="line">Shift_L,   Down, Shift_L|Button5</span><br></pre></td></tr></table></figure><p>最主要的就是前面2个行，后面几行可以不用管，其中“4”设置的就是滚动速度，大家可以根据自己的需要设置合适的值，保存之后可以通过<strong>killall imwheel &amp;&amp; imwheel</strong>重新加载配置。但是为了每次重启后还能生效，你还需要放到系统开机自启里面。</p>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;难道只有我一个人觉得Ubuntu的unity桌面非常好用吗？&lt;/p&gt;
&lt;p&gt;最近把台式机上面的Ubuntu 16.04格式化了，装了黑苹果用了一周，不得不说，MacOS确实很精美，软件生态比Linux丰富很多，比Windows简单安全很多，但是我最终还是不想用了，原因很多，究其原因，我觉得是因为Mac是一个为触摸操作优化的系统，没有触摸板就像是断条腿。&lt;/p&gt;
&lt;p&gt;所以，我决定重回Linux，继续用Ubuntu，期间尝试了最新的Ubuntu 20.04版本，发现Gnome桌面模仿Mac系统的痕迹太明显了，扁平化的UI设计真的欣赏不来，最要命的是我总感觉一卡一卡，没有unity桌面流畅，真的不是我的错觉，我之前也尝试过18.04版本，那会感觉也是很卡。&lt;/p&gt;
&lt;p&gt;最后想了想，还是回到unity桌面吧，不再折腾了，奈何unity已经被抛弃，只能死守16.04这个版本，虽然看起来有点老了，但是用起来完全没有任何问题。装个系统对于我来说是非常轻松的事情，但最麻烦的是我个人的一些特殊设定、常用软件的安装配置比较耗时，所以这篇文章的主要目的就在于记录整个过程，结合我之前写的一些文章，作为一个记录，也可以供需要的参考一下，如果你喜欢unity桌面的话。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;以下文章的内容仅针对Ubuntu的unity桌面，准确的说是16.04版本，因为之后的版本都是Gnome桌面，基本上不适用了，Only For Ubuntu16.04。&lt;/p&gt;
&lt;/blockquote&gt;
    
    </summary>
    
    
      <category term="Linux" scheme="https://wangbenjun.site/categories/Linux/"/>
    
    
      <category term="Linux" scheme="https://wangbenjun.site/tags/Linux/"/>
    
      <category term="Ubuntu" scheme="https://wangbenjun.site/tags/Ubuntu/"/>
    
  </entry>
  
  <entry>
    <title>基于RPC实现的Go插件机制</title>
    <link href="https://wangbenjun.site/2021/coding/golang/go-rpc-plugin.html"/>
    <id>https://wangbenjun.site/2021/coding/golang/go-rpc-plugin.html</id>
    <published>2021-12-08T12:10:00.000Z</published>
    <updated>2025-01-24T13:37:52.417Z</updated>
    
    <content type="html"><![CDATA[<p>最近项目里面有需要实现插件机制，所谓插件就是指可以在不用发布新的版本情况下实现对程序功能的扩展，这种插件机制应用特别广泛，常见的比如咱们浏览器的扩展、Nginx扩展、PHP扩展等等。</p><p>在Go语言里面，官方自带了一个插件扩展机制，这个我之前有文章介绍过，详情可以点击 <a href="https://wangbenjun.site/2021/coding/golang/go-plugin.html">Go Plugin实现插件化编程</a></p><p>但是Go官方自带这个插件机制很不成熟，更像是一个半成品，存在很多问题，比如插件无法卸载、各自版本依赖必须完全一致，其中对版本依赖必须一致这个要求非常致命，因为在一个大型的项目里面，很难限制插件开发者使用的依赖库版本，很难做到统一。</p><p>今天咱们就介绍一个基于RPC实现的插件机制，而且这种方式是经过大规模实践验证的，可用性非常高，值得尝试一下。</p><a id="more"></a><h2 id="1-简介"><a href="#1-简介" class="headerlink" title="1.简介"></a>1.简介</h2><p>这个是hashicorp公司开源的项目，Github地址： <a href="https://github.com/hashicorp/go-plugin" target="_blank" rel="noopener">https://github.com/hashicorp/go-plugin</a></p><p>官方的介绍如下：</p><blockquote><p>go-plugin 是一个基于 RPC 的 Go (golang) 插件系统。 它是 HashiCorp 工具使用了 4 年多的插件系统。 虽然最初是为 Packer 创建的，但它也被 Terraform、Nomad、Vault 和 Boundary 使用。 虽然插件系统基于 RPC，但它目前仅设计用于在本地 [可靠] 网络上工作。 不支持真实网络上的插件，并且会导致意外行为。 该插件系统已在许多不同项目的数百万台机器上使用，并已证明经得起考验，可用于生产。</p></blockquote><p>介绍中提到在多个项目使用过，但是这些我都不熟悉，不过有一个开源的项目很多人都应该听过，名字叫<a href="https://grafana.com/" target="_blank" rel="noopener">Grafana</a></p><p>Grafana是一个开源的监控可视化平台，刚好就是hashicorp公司的项目，目前也提供商业化服务，其后端就是Go写的，也算是Go语言里面非常热门的开源项目，有几十k的star，源码之前看过写的也很不错，其中就用到了上面所说的插件库，其中Grafana面板里面的数据源就是插件的形式，用户可以根据自己需求下载安装对应数据库的插件。</p><p>不过在Grafana这个项目里面，这个插件库被封装了很多层，代码非常多，不是那么简单，咱们先不看，首先先了解一下这个插件库所提供的功能，根据官方文档这个插件库有以下特性：</p><ol><li>插件是Go接口的实现：这让插件的编写、使用非常自然。对于插件的作者来说，他只需要实现一个Go接口即可；对于插件的用户来说，他只需要调用一个Go接口即可。go-plugin会处理好本地调用转换为gRPC调用的所有细节</li><li>跨语言支持：插件可以基于任何主流语言编写，同样可以被任何主流语言消费</li><li>支持复杂的参数、返回值：go-plugin可以处理接口、io.Reader&#x2F;Writer等复杂类型</li><li>双向通信：为了支持复杂参数，宿主进程能够将接口实现发送给插件，插件也能够回调到宿主进程</li><li>内置日志系统：任何使用log标准库的的插件，都会将日志信息传回宿主机进程。宿主进程会在这些日志前面加上插件二进制文件的路径，并且打印日志</li><li>协议版本化：支持一个简单的协议版本化，增加版本号后可以基于老版本协议的插件无效化。当接口签名变化时应当增加版本</li><li>标准输出&#x2F;错误同步：插件以子进程的方式运行，这些子进程可以自由的使用标准输出&#x2F;错误，并且打印的内容会被自动同步到宿主进程，宿主进程可以为同步的日志指定一个io.Writer</li><li>TTY Preservation：插件子进程可以链接到宿主进程的stdin文件描述符，以便要求TTY的软件能正常工作</li><li>宿主进程升级：宿主进程升级的时候，插件子进程可以继续允许，并在升级后自动关联到新的宿主进程</li><li>加密通信：gRPC信道可以加密</li><li>完整性校验：支持对插件的二进制文件进行Checksum</li><li>插件崩溃了，不会导致宿主进程崩溃</li><li>容易安装：只需要将插件放到某个宿主进程能够访问的目录即可</li></ol><p>这些特性看上去很多很丰富，功能很强大，然而根据我的了解，实际应用起来还是需要自己做一些封装的，从官方的demo来看，并没有体现出这些特性。</p><h2 id="2-示例"><a href="#2-示例" class="headerlink" title="2.示例"></a>2.示例</h2><p>官方仓库里面有几个example，咱们先看一个最简单的demo</p><h3 id="1-插件定义"><a href="#1-插件定义" class="headerlink" title="1.插件定义"></a>1.插件定义</h3><p>首先先看一下<strong>greeter_interface.go</strong>文件。第一部分咱们可以理解为，先定义这个插件的要实现的接口，约束插件的行为。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line"><span class="comment">// Greeter is the interface that we're exposing as a plugin.</span></span><br><span class="line"><span class="keyword">type</span> Greeter <span class="keyword">interface</span> &#123;</span><br><span class="line">    Greet() <span class="keyword">string</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>然后定义了一个实现这个插件接口的Greeter，这里是通过RPC去实现，所以需要一个rpc client</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// Here is an implementation that talks over RPC</span></span><br><span class="line"><span class="keyword">type</span> GreeterRPC <span class="keyword">struct</span>&#123; client *rpc.Client &#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(g *GreeterRPC)</span> <span class="title">Greet</span><span class="params">()</span> <span class="title">string</span></span> &#123;</span><br><span class="line">    <span class="keyword">var</span> resp <span class="keyword">string</span></span><br><span class="line">    err := g.client.Call(<span class="string">"Plugin.Greet"</span>, <span class="built_in">new</span>(<span class="keyword">interface</span>&#123;&#125;), &amp;resp)</span><br><span class="line">    <span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">        <span class="comment">// You usually want your interfaces to return errors. If they don't,</span></span><br><span class="line">        <span class="comment">// there isn't much other choice here.</span></span><br><span class="line">        <span class="built_in">panic</span>(err)</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> resp</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>紧接着，又定义了一个RPCServer包装了一遍</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// Here is the RPC server that GreeterRPC talks to, conforming to</span></span><br><span class="line"><span class="comment">// the requirements of net/rpc</span></span><br><span class="line"><span class="keyword">type</span> GreeterRPCServer <span class="keyword">struct</span> &#123;</span><br><span class="line">    <span class="comment">// This is the real implementation</span></span><br><span class="line">    Impl Greeter</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(s *GreeterRPCServer)</span> <span class="title">Greet</span><span class="params">(args <span class="keyword">interface</span>&#123;&#125;, resp *<span class="keyword">string</span>)</span> <span class="title">error</span></span> &#123;</span><br><span class="line">    *resp = s.Impl.Greet()</span><br><span class="line">    <span class="keyword">return</span> <span class="literal">nil</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>最后的最后，才是插件的实现，主要是Server和Client这2个方法：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> GreeterPlugin <span class="keyword">struct</span> &#123;</span><br><span class="line">    <span class="comment">// Impl Injection</span></span><br><span class="line">    Impl Greeter</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(p *GreeterPlugin)</span> <span class="title">Server</span><span class="params">(*plugin.MuxBroker)</span> <span class="params">(<span class="keyword">interface</span>&#123;&#125;, error)</span></span> &#123;</span><br><span class="line">    <span class="keyword">return</span> &amp;GreeterRPCServer&#123;Impl: p.Impl&#125;, <span class="literal">nil</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(GreeterPlugin)</span> <span class="title">Client</span><span class="params">(b *plugin.MuxBroker, c *rpc.Client)</span> <span class="params">(<span class="keyword">interface</span>&#123;&#125;, error)</span></span> &#123;</span><br><span class="line">    <span class="keyword">return</span> &amp;GreeterRPC&#123;client: c&#125;, <span class="literal">nil</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>RPC Plugin接口的定义：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// Plugin is the interface that is implemented to serve/connect to an</span></span><br><span class="line"><span class="comment">// inteface implementation.</span></span><br><span class="line"><span class="keyword">type</span> Plugin <span class="keyword">interface</span> &#123;</span><br><span class="line">    <span class="comment">// Server should return the RPC server compatible struct to serve</span></span><br><span class="line">    <span class="comment">// the methods that the Client calls over net/rpc.</span></span><br><span class="line">    Server(*MuxBroker) (<span class="keyword">interface</span>&#123;&#125;, error)</span><br><span class="line"></span><br><span class="line">    <span class="comment">// Client returns an interface implementation for the plugin you're</span></span><br><span class="line">    <span class="comment">// serving that communicates to the server end of the plugin.</span></span><br><span class="line">    Client(*MuxBroker, *rpc.Client) (<span class="keyword">interface</span>&#123;&#125;, error)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>最终，在层层包装之下，这个文件定义了一个插件的框架，以及要实现的方法，但是还缺一个实现，实现是在<strong>greeter_impl.go</strong>文件里面</p><h3 id="2-插件实现"><a href="#2-插件实现" class="headerlink" title="2.插件实现"></a>2.插件实现</h3><p>首先定义一个对象实现Greet方法，这个比较简单，这里用到库里面一个logger库：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// Here is a real implementation of Greeter</span></span><br><span class="line"><span class="keyword">type</span> GreeterHello <span class="keyword">struct</span> &#123;</span><br><span class="line">    logger hclog.Logger</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(g *GreeterHello)</span> <span class="title">Greet</span><span class="params">()</span> <span class="title">string</span></span> &#123;</span><br><span class="line">    g.logger.Debug(<span class="string">"message from GreeterHello.Greet"</span>)</span><br><span class="line">    <span class="keyword">return</span> <span class="string">"Hello!"</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>然后就是main里面的内容，这块的操作简单说就是设置一些参数，启动一个RPC服务，等待请求的到来：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">    logger := hclog.New(&amp;hclog.LoggerOptions&#123;</span><br><span class="line">        Level:      hclog.Trace,</span><br><span class="line">        Output:     os.Stderr,</span><br><span class="line">        JSONFormat: <span class="literal">true</span>,</span><br><span class="line">    &#125;)</span><br><span class="line"></span><br><span class="line">    greeter := &amp;GreeterHello&#123;</span><br><span class="line">        logger: logger,</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">// pluginMap is the map of plugins we can dispense.</span></span><br><span class="line">    <span class="keyword">var</span> pluginMap = <span class="keyword">map</span>[<span class="keyword">string</span>]plugin.Plugin&#123;</span><br><span class="line">        <span class="string">"greeter"</span>: &amp;example.GreeterPlugin&#123;Impl: greeter&#125;,</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    logger.Debug(<span class="string">"message from plugin"</span>, <span class="string">"foo"</span>, <span class="string">"bar"</span>)</span><br><span class="line"></span><br><span class="line">    plugin.Serve(&amp;plugin.ServeConfig&#123;</span><br><span class="line">        HandshakeConfig: plugin.HandshakeConfig&#123;</span><br><span class="line">            ProtocolVersion:  <span class="number">1</span>,</span><br><span class="line">            MagicCookieKey:   <span class="string">"BASIC_PLUGIN"</span>,</span><br><span class="line">            MagicCookieValue: <span class="string">"hello"</span>,</span><br><span class="line">        &#125;,</span><br><span class="line">        Plugins: pluginMap,</span><br><span class="line">    &#125;)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>最后别忘了编译插件，插件的编译实际上和普通Go程序没有什么区别，会得到一个二进制文件，后面会用到</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">go build -o ./plugin/greeter ./plugin/greeter_impl.go</span><br></pre></td></tr></table></figure><h3 id="3-使用插件"><a href="#3-使用插件" class="headerlink" title="3.使用插件"></a>3.使用插件</h3><p>使用插件的代码相对也简单，只需要New一个Client，设置相关参数，然后发起调用：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">    <span class="comment">// We're a host! Start by launching the plugin process.</span></span><br><span class="line">    client := plugin.NewClient(&amp;plugin.ClientConfig&#123;</span><br><span class="line">        HandshakeConfig: plugin.HandshakeConfig&#123;</span><br><span class="line">            ProtocolVersion:  <span class="number">1</span>,</span><br><span class="line">            MagicCookieKey:   <span class="string">"BASIC_PLUGIN"</span>,</span><br><span class="line">            MagicCookieValue: <span class="string">"hello"</span>,</span><br><span class="line">        &#125;,</span><br><span class="line">        Plugins: <span class="keyword">map</span>[<span class="keyword">string</span>]plugin.Plugin&#123;</span><br><span class="line">            <span class="string">"greeter"</span>: &amp;example.GreeterPlugin&#123;&#125;,</span><br><span class="line">        &#125;,</span><br><span class="line">        Cmd: exec.Command(<span class="string">"./plugin/greeter"</span>),</span><br><span class="line">        Logger: hclog.New(&amp;hclog.LoggerOptions&#123;</span><br><span class="line">            Name:   <span class="string">"plugin"</span>,</span><br><span class="line">            Output: os.Stdout,</span><br><span class="line">            Level:  hclog.Debug,</span><br><span class="line">        &#125;),</span><br><span class="line">    &#125;)</span><br><span class="line">    <span class="keyword">defer</span> client.Kill()</span><br><span class="line"></span><br><span class="line">    <span class="comment">// Connect via RPC</span></span><br><span class="line">    rpcClient, err := client.Client()</span><br><span class="line">    <span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">        log.Fatal(err)</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// Request the plugin</span></span><br><span class="line">    raw, err := rpcClient.Dispense(<span class="string">"greeter"</span>)</span><br><span class="line">    <span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">        log.Fatal(err)</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// We should have a Greeter now! This feels like a normal interface</span></span><br><span class="line">    <span class="comment">// implementation but is in fact over an RPC connection.</span></span><br><span class="line">    greeter := raw.(example.Greeter)</span><br><span class="line">    fmt.Println(greeter.Greet())</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这里面需要注意的是一个handshakeConfig里面的配置要和插件实现里面的一致，另外就是设置二进制执行文件的位置。</p><p>其次，Plugins是一个map里面存储了插件名和插件定义的映射关系。</p><h2 id="3-疑问"><a href="#3-疑问" class="headerlink" title="3.疑问"></a>3.疑问</h2><p>从上面的demo来看，这个库的插件系统本质上就是本地RPC调用，虽然性能可能低了一点，毕竟本地网络也有开销，但是确实不存在官方插件机制的一些问题。</p><p>但是我从这个demo里面并没有看出来是如何实现多个插件共存、以及插件升级更新等功能，实际上在我后续的研究中我发现，这个库并没有实现这些功能。。。</p><p>如果要实现这些功能可能得自己去实现，Grafana这个项目在用到这个库得时候就做了大量的封装。</p><h2 id="4-原理"><a href="#4-原理" class="headerlink" title="4.原理"></a>4.原理</h2><p>首先，咱们先看一下插件实现这块，主要就是<strong>plugin.Serve</strong>方法，它需要传入一个插件配置：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">Serve</span><span class="params">(opts *ServeConfig)</span></span> &#123;</span><br><span class="line">    <span class="comment">// 一些配置</span></span><br><span class="line">    ...</span><br><span class="line"></span><br><span class="line">    <span class="comment">// Register a listener so we can accept a connection</span></span><br><span class="line">    listener, err := serverListener()</span><br><span class="line">    <span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">        logger.Error(<span class="string">"plugin init error"</span>, <span class="string">"error"</span>, err)</span><br><span class="line">        <span class="keyword">return</span></span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// Close the listener on return. We wrap this in a func() on purpose</span></span><br><span class="line">    <span class="comment">// because the "listener" reference may change to TLS.</span></span><br><span class="line">    <span class="keyword">defer</span> <span class="function"><span class="keyword">func</span><span class="params">()</span></span> &#123;</span><br><span class="line">        listener.Close()</span><br><span class="line">    &#125;()</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// TLS的配置</span></span><br><span class="line">    ...</span><br><span class="line"></span><br><span class="line">    <span class="comment">// Create the channel to tell us when we're done</span></span><br><span class="line">    doneCh := <span class="built_in">make</span>(<span class="keyword">chan</span> <span class="keyword">struct</span>&#123;&#125;)</span><br><span class="line">    </span><br><span class="line">    <span class="comment">// Build the server type</span></span><br><span class="line">    <span class="keyword">var</span> server ServerProtocol</span><br><span class="line">    <span class="keyword">switch</span> protoType &#123;</span><br><span class="line">    <span class="keyword">case</span> ProtocolNetRPC:</span><br><span class="line">        <span class="comment">// If we have a TLS configuration then we wrap the listener</span></span><br><span class="line">        <span class="comment">// ourselves and do it at that level.</span></span><br><span class="line">        <span class="keyword">if</span> tlsConfig != <span class="literal">nil</span> &#123;</span><br><span class="line">            listener = tls.NewListener(listener, tlsConfig)</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="comment">// Create the RPC server to dispense</span></span><br><span class="line">        server = &amp;RPCServer&#123;</span><br><span class="line">            Plugins: pluginSet,</span><br><span class="line">            Stdout:  stdout_r,</span><br><span class="line">            Stderr:  stderr_r,</span><br><span class="line">            DoneCh:  doneCh,</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">case</span> ProtocolGRPC:</span><br><span class="line">        <span class="comment">// Create the gRPC server</span></span><br><span class="line">        server = &amp;GRPCServer&#123;</span><br><span class="line">            Plugins: pluginSet,</span><br><span class="line">            Server:  opts.GRPCServer,</span><br><span class="line">            TLS:     tlsConfig,</span><br><span class="line">            Stdout:  stdout_r,</span><br><span class="line">            Stderr:  stderr_r,</span><br><span class="line">            DoneCh:  doneCh,</span><br><span class="line">            logger:  logger,</span><br><span class="line">        &#125;</span><br><span class="line">    <span class="keyword">default</span>:</span><br><span class="line">        <span class="built_in">panic</span>(<span class="string">"unknown server protocol: "</span> + protoType)</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// Initialize the servers</span></span><br><span class="line">    <span class="keyword">if</span> err := server.Init(); err != <span class="literal">nil</span> &#123;</span><br><span class="line">        logger.Error(<span class="string">"protocol init"</span>, <span class="string">"error"</span>, err)</span><br><span class="line">        <span class="keyword">return</span></span><br><span class="line">    &#125;</span><br><span class="line">    ...</span><br><span class="line"></span><br><span class="line">    <span class="comment">// Accept connections and wait for completion</span></span><br><span class="line">    <span class="keyword">go</span> server.Serve(listener)</span><br><span class="line"></span><br><span class="line">    ctx := context.Background()</span><br><span class="line">    <span class="keyword">if</span> opts.Test != <span class="literal">nil</span> &amp;&amp; opts.Test.Context != <span class="literal">nil</span> &#123;</span><br><span class="line">        ctx = opts.Test.Context</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">select</span> &#123;</span><br><span class="line">    <span class="keyword">case</span> &lt;-ctx.Done():</span><br><span class="line">        listener.Close()</span><br><span class="line">        <span class="keyword">if</span> s, ok := server.(*GRPCServer); ok &#123;</span><br><span class="line">            s.Stop()</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="comment">// Wait for the server itself to shut down</span></span><br><span class="line">        &lt;-doneCh</span><br><span class="line">    <span class="keyword">case</span> &lt;-doneCh:</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>代码很多,这里只展示了核心代码，其实正做的一件事就是初始化并启动RPC服务，做好接受请求的准备。</p><p>更多的代码在插件使用这块，首先我们New了一个Client，这个Client是维护插件的，而且是一个插件一个Client，所以如果你要实现多插件共存，可以去实现一个插件和Client的映射关系即可。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> Client <span class="keyword">struct</span> &#123;</span><br><span class="line">    config            *ClientConfig</span><br><span class="line">    exited            <span class="keyword">bool</span></span><br><span class="line">    l                 sync.Mutex</span><br><span class="line">    address           net.Addr</span><br><span class="line">    process           *os.Process</span><br><span class="line">    client            ClientProtocol</span><br><span class="line">    protocol          Protocol</span><br><span class="line">    logger            hclog.Logger</span><br><span class="line">    doneCtx           context.Context</span><br><span class="line">    ctxCancel         context.CancelFunc</span><br><span class="line">    negotiatedVersion <span class="keyword">int</span></span><br><span class="line"></span><br><span class="line">    <span class="comment">// clientWaitGroup is used to manage the lifecycle of the plugin management</span></span><br><span class="line">    <span class="comment">// goroutines.</span></span><br><span class="line">    clientWaitGroup sync.WaitGroup</span><br><span class="line"></span><br><span class="line">    <span class="comment">// stderrWaitGroup is used to prevent the command's Wait() function from</span></span><br><span class="line">    <span class="comment">// being called before we've finished reading from the stderr pipe.</span></span><br><span class="line">    stderrWaitGroup sync.WaitGroup</span><br><span class="line"></span><br><span class="line">    <span class="comment">// processKilled is used for testing only, to flag when the process was</span></span><br><span class="line">    <span class="comment">// forcefully killed.</span></span><br><span class="line">    processKilled <span class="keyword">bool</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>在main里面当我们New完Client之后，依次调用了Client和Dispense2个方法，这个2个方法非常重要：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// Client returns the protocol client for this connection.</span></span><br><span class="line"><span class="comment">// Subsequent calls to this will return the same client.</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(c *Client)</span> <span class="title">Client</span><span class="params">()</span> <span class="params">(ClientProtocol, error)</span></span> &#123;</span><br><span class="line">    _, err := c.Start()</span><br><span class="line">    <span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="literal">nil</span>, err</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    c.l.Lock()</span><br><span class="line">    <span class="keyword">defer</span> c.l.Unlock()</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> c.client != <span class="literal">nil</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> c.client, <span class="literal">nil</span></span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">switch</span> c.protocol &#123;</span><br><span class="line">    <span class="keyword">case</span> ProtocolNetRPC:</span><br><span class="line">        c.client, err = newRPCClient(c)</span><br><span class="line"></span><br><span class="line">    <span class="keyword">case</span> ProtocolGRPC:</span><br><span class="line">        c.client, err = newGRPCClient(c.doneCtx, c)</span><br><span class="line"></span><br><span class="line">    <span class="keyword">default</span>:</span><br><span class="line">        <span class="keyword">return</span> <span class="literal">nil</span>, fmt.Errorf(<span class="string">"unknown server protocol: %s"</span>, c.protocol)</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">        c.client = <span class="literal">nil</span></span><br><span class="line">        <span class="keyword">return</span> <span class="literal">nil</span>, err</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> c.client, <span class="literal">nil</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>其中c.Start这个方法干了很多事情，简单说就是根据配置里面的cmd，也就是咱们编译插件之后得到二进制可执行文件，启动插件的rpc服务。</p><p>然后再根据协议的不同，启动RPC服务或者GRPC服务，得到一个真正可用Client，相当于就是通道已经打通了，接下来就是发起请求。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(c *RPCClient)</span> <span class="title">Dispense</span><span class="params">(name <span class="keyword">string</span>)</span> <span class="params">(<span class="keyword">interface</span>&#123;&#125;, error)</span></span> &#123;</span><br><span class="line">    p, ok := c.plugins[name]</span><br><span class="line">    <span class="keyword">if</span> !ok &#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="literal">nil</span>, fmt.Errorf(<span class="string">"unknown plugin type: %s"</span>, name)</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">var</span> id <span class="keyword">uint32</span></span><br><span class="line">    <span class="keyword">if</span> err := c.control.Call(</span><br><span class="line">        <span class="string">"Dispenser.Dispense"</span>, name, &amp;id); err != <span class="literal">nil</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="literal">nil</span>, err</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    conn, err := c.broker.Dial(id)</span><br><span class="line">    <span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="literal">nil</span>, err</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> p.Client(c.broker, rpc.NewClient(conn))</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>Dispense方法就是根据插件名拿到对应的插件对象，然后又包装了一层拿到一个Client对象，还记得最初定义插件时候那个Client吗？</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(GreeterPlugin)</span> <span class="title">Client</span><span class="params">(b *plugin.MuxBroker, c *rpc.Client)</span> <span class="params">(<span class="keyword">interface</span>&#123;&#125;, error)</span></span> &#123;</span><br><span class="line">    <span class="keyword">return</span> &amp;GreeterRPC&#123;client: c&#125;, <span class="literal">nil</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>最后，断言并调用插件的方法，这时候才是真正发起了RPC请求，并获取返回结果：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">greeter := raw.(example.Greeter)</span><br><span class="line">greeter.Greet()</span><br></pre></td></tr></table></figure><p>有一点，我感觉特别奇怪，从这个插件的实现来看，Dispense这步更像是细分了插件里面的插件，因为在我理解，一个二进制文件就是一个插件，一个插件只有一个实现。</p><p>但是很明显，这个库并不这么认为，它认为一个插件文件里面可以实现多个插件，所以它增加了一个Plugins来存储插件的映射关系，也就是说你可以在一个插件里面实现多个接口。</p><p>这种实现也相当于是一种约束，实际上在Grafana这个项目里面，它通过这种方式声明了可以支持的插件类型，作为插件，根据需求实现部分接口即可。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// getPluginSet returns list of plugins supported</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">getPluginSet</span><span class="params">()</span> <span class="title">goplugin</span>.<span class="title">PluginSet</span></span> &#123;</span><br><span class="line">    <span class="keyword">return</span> goplugin.PluginSet&#123;</span><br><span class="line">        <span class="string">"diagnostics"</span>: &amp;grpcplugin.DiagnosticsGRPCPlugin&#123;&#125;,</span><br><span class="line">        <span class="string">"resource"</span>:    &amp;grpcplugin.ResourceGRPCPlugin&#123;&#125;,</span><br><span class="line">        <span class="string">"data"</span>:        &amp;grpcplugin.DataGRPCPlugin&#123;&#125;,</span><br><span class="line">        <span class="string">"stream"</span>:      &amp;grpcplugin.StreamGRPCPlugin&#123;&#125;,</span><br><span class="line">        <span class="string">"renderer"</span>:    &amp;pluginextensionv2.RendererGRPCPlugin&#123;&#125;,</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="5-畅想"><a href="#5-畅想" class="headerlink" title="5.畅想"></a>5.畅想</h2><p>写了这么多，对于这个库，个人觉得可用性非常高，经过生产实际检验的，但是只有核心功能，缺少封装，对于使用插件的系统来说，至少要实现以下功能：</p><ol><li>多插件的管理。</li><li>插件自动、手动更新。</li><li>插件健康检查，因为这个插件本质就是一个rpc服务，可能会挂掉，挂掉之后该怎么办呢？</li></ol><p>如果真的要想好好用起这个库，确实还得花不少功夫，有一个好得地方是我们可以参考Hashicorp家的其它开源项目代码来完善，其实我也在想能不能稍微把这个库封装一下提供一个简单易用的接口。</p>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;最近项目里面有需要实现插件机制，所谓插件就是指可以在不用发布新的版本情况下实现对程序功能的扩展，这种插件机制应用特别广泛，常见的比如咱们浏览器的扩展、Nginx扩展、PHP扩展等等。&lt;/p&gt;
&lt;p&gt;在Go语言里面，官方自带了一个插件扩展机制，这个我之前有文章介绍过，详情可以点击 &lt;a href=&quot;https://wangbenjun.site/2021/coding/golang/go-plugin.html&quot;&gt;Go Plugin实现插件化编程&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;但是Go官方自带这个插件机制很不成熟，更像是一个半成品，存在很多问题，比如插件无法卸载、各自版本依赖必须完全一致，其中对版本依赖必须一致这个要求非常致命，因为在一个大型的项目里面，很难限制插件开发者使用的依赖库版本，很难做到统一。&lt;/p&gt;
&lt;p&gt;今天咱们就介绍一个基于RPC实现的插件机制，而且这种方式是经过大规模实践验证的，可用性非常高，值得尝试一下。&lt;/p&gt;
    
    </summary>
    
    
      <category term="Golang" scheme="https://wangbenjun.site/categories/Golang/"/>
    
    
      <category term="Golang" scheme="https://wangbenjun.site/tags/Golang/"/>
    
  </entry>
  
  <entry>
    <title>Mac黑苹果超简单的安装入门教程</title>
    <link href="https://wangbenjun.site/2021/linux/hackintosh.html"/>
    <id>https://wangbenjun.site/2021/linux/hackintosh.html</id>
    <published>2021-12-04T11:55:00.000Z</published>
    <updated>2024-12-29T03:02:14.814Z</updated>
    
    <content type="html"><![CDATA[<p>其实到今天，黑苹果的技术已经非常成熟了，甚至形成了一条产业链，有专门做黑苹果安装的某宝店和一些电脑店。其实之前我也装过几次黑苹果，就是没坚持用下去就格了，最近又装了个黑苹果，感觉挺好用的，今天咱们就总结一下目前最简单的方式。</p><h2 id="1-何为黑苹果？"><a href="#1-何为黑苹果？" class="headerlink" title="1.何为黑苹果？"></a>1.何为黑苹果？</h2><p>黑苹果并不是黑色的苹果，说到黑有些人觉得是不是有问题，比如黑客通常都被认为是干坏事的。其实黑苹果这个说法还是真和黑客有关系，因为黑客的英文叫<strong>Hacker</strong>，而黑苹果英文叫<strong>Hackintosh</strong>，Mac的全称是<strong>Macintosh</strong>。</p><img src="/images/2021/WX20211204-221138@2x.png" /><p>苹果的OS从来都是只自用，搭配Mac电脑出售，不对外单独出售，如果你想用Mac系统就得买苹果电脑，但是呢？苹果的电脑卖又那么贵，确实贵。。。所以一群黑客破解了MacOS，可以装在普通电脑上面，所以叫黑苹果，简单说就是盗版了的MacOS，与之相对的白苹果就是指正经安装在Mac电脑上。</p><a id="more"></a><h2 id="2-硬件提示"><a href="#2-硬件提示" class="headerlink" title="2.硬件提示"></a>2.硬件提示</h2><p>如果说你是奔着黑苹果来的，建议你网上看一下一些攻略，尽量采用兼容的台式机硬件配置，或者买那些已经完美驱动的笔记本。</p><p>如果你是纯属娱乐，想折腾着玩玩，建议以下情况不要折腾：</p><ol><li>N卡用户。因为从很早之前开始，苹果就已经和AMD合作了，所以N卡就断驱动了，好像10系之后就无解了，除非你屏蔽独显。反之，推荐A卡，可以免驱。</li><li>笔记本用户谨慎。笔记本由于有触摸板、摄像头、无线网卡等配件，型号相当复杂，有些驱动是无解的，折腾之前最好网上搜一下有没有成功案例。</li></ol><p>对于广大台式机用户，以目前的情况下，安装成功率在99%以上，是最容易成功的了，推荐尝试。</p><h2 id="3-引导方式"><a href="#3-引导方式" class="headerlink" title="3.引导方式"></a>3.引导方式</h2><p>目前黑苹果有2种主流引导方式：四叶草Clover、Opencore简称OC。其实黑苹果最麻烦的就是引导这块的东西，经常装系统的人都应该知道，如果引导出问题了系统就启动不了了，特别在多系统的情况下，引导会更加复杂。</p><p>这2套引导方式相关工具链和教程略有差异，建议在搜索相关教程的时候注意一下，本文将采用OC引导的方式来进行，听说OC是未来。</p><h2 id="4-推荐教程"><a href="#4-推荐教程" class="headerlink" title="4.推荐教程"></a>4.推荐教程</h2><p>说到OC引导，有一个网站上面有非常详细的安装教程，内容非常翔实，一步步按照上面介绍的来，99%可以成功。</p><p><a href="https://dortania.github.io/OpenCore-Install-Guide/prerequisites.html" target="_blank" rel="noopener">https://dortania.github.io/OpenCore-Install-Guide/prerequisites.html</a></p><p>但是唯一不好的是网站是英文的，目前还没人给翻译一下，对于一些英文不好童鞋来说是个问题，而且整个内容巨长，懂英文也不好理解。</p><img src="/images/2021/WX20211204-205036@2x.png" /><p>本文将参考该网站部分内容，尽可能简单快速教大家，对于一些不清楚的地方，可以参考此网站。</p><h2 id="5-安装过程"><a href="#5-安装过程" class="headerlink" title="5.安装过程"></a>5.安装过程</h2><p>整个安装过程分为几大部分，一步步来就可以了</p><h3 id="1-制作启动盘"><a href="#1-制作启动盘" class="headerlink" title="1.制作启动盘"></a>1.制作启动盘</h3><p>首先，准备一个4G的U盘，此处建议参考: <a href="https://dortania.github.io/OpenCore-Install-Guide/installer-guide/" target="_blank" rel="noopener">https://dortania.github.io/OpenCore-Install-Guide/installer-guide/</a></p><p>支持Windows、Linux、Mac多系统，内容非常详细，简单说就是几大部分：</p><p>1.下载MacOS镜像。<br>下载opencore的软件包，在<strong>macrecovery</strong>目录里面作者提供了一个python脚本，需要我们本地安装一个python运行环境，然后执行这个脚本下载镜像。</p><p>2.在U盘里面建文件夹。<br>在U盘里面建一个名为<strong>com.apple.recovery.boot</strong>的文件夹，把下载的镜像文件放到里面。</p><p>详细步骤这里就不翻译了，建议看上面链接，作者针对不同系统提供了多种操作方式和工具，写的真的非常详细，英文不好的同学建议开启翻译。</p><h3 id="2-配置EFI文件夹"><a href="#2-配置EFI文件夹" class="headerlink" title="2.配置EFI文件夹"></a>2.配置EFI文件夹</h3><p>这一步是最核心、最麻烦的地方，黑苹果能不能成功主要看这个，先看一个标准的EFI配置文件，里面最重要的是ACPI文件里面的电源相关、以及Kexts文件夹里面的补丁。</p><img src="/images/2021/WX20211204-211340@2x.png" /><p>不同的电脑主板和CPU都不一样，所以这一块没有一个统一的答案，如果按照上面教程的步骤的话非常繁琐。。。但是我们可以照抄别人的答案，去Github上面搜一下</p><p>这里推荐一个仓库: <a href="https://github.com/daliansky/Hackintosh" target="_blank" rel="noopener">https://github.com/daliansky/Hackintosh</a></p><p>里面有很多型号，对应不同的主板、笔记本，如果找不到自己的需要可以带上你的电脑型号在Github上面搜一下，实在不行，百度一下看看。。。万一真的找不到，找个相近的配置试一试也可以。</p><p>假设你找到了，下载EFI文件夹放到U盘里面就可以了，下一步就是安装了。</p><h3 id="3-BIOS配置"><a href="#3-BIOS配置" class="headerlink" title="3.BIOS配置"></a>3.BIOS配置</h3><p>这一步也很重要，BIOS该关的还是得关，但是不同品牌的主板BIOS设置选项不一样，位置也不一样，所以这里也不细说了，网上文章很多，建议大家搜一下。</p><p>注意搜的时候带上自己的主板品牌，比如”黑苹果BIOS设置 技嘉”，网上的教程还是很多的，对着设置就行了，这一步不算很难。</p><h3 id="4-安装"><a href="#4-安装" class="headerlink" title="4.安装"></a>4.安装</h3><p>插上U盘，开机从U盘启动，正常的话应该进入Opencore的界面，选择第一个加载一会应该会进入安装流程，选择硬盘工具格式化硬盘、分区，然后安装，其实这一步也没什么说的，正常情况下会重启1-2次，如果顺利的话，最后应该就会成功进入系统。</p><p>有些人以为进桌面就成功了，其实还没有，还差一步，这时候实际上你是从U盘引导启动的，拔掉U盘就不行了。</p><h3 id="5-设置EFI引导"><a href="#5-设置EFI引导" class="headerlink" title="5.设置EFI引导"></a>5.设置EFI引导</h3><p>这时候需要安装一个工具，名字叫<strong>Opencore Configurator</strong>，然后菜单栏选择挂载EFI，然后你应该可以在苹果的文件管理器<strong>访达</strong>里面看到一个EFI的硬盘，打开可以看到一个EFI文件夹，把它删了，打开U盘，把EFI文件夹复制过来。</p><img src="/images/2021/WX20211204-214229@2x.png" /><p>至此，所有安装过程结束，拔掉U盘重启看看是否能成功开机，如果失败了，可以把U盘再插上，从U盘引导开机，所以U盘先留着备用，防止后面折腾的时候出问题系统启动不了。</p><p>通常来说能进系统桌面基本上算是完成了90%，但是可能还有一些问题，比如无法睡眠、Wi-Fi不好使，这时候就得靠自己一个个去解决了，折腾么。。。就是图个乐。</p><h2 id="6-感想"><a href="#6-感想" class="headerlink" title="6.感想"></a>6.感想</h2><p>实际上Mac系统的交互方式是和Windows、Linux不一样的，很多人第一次用会很不习惯，举个例子，最常用的复制粘贴快捷键，在Windows里面是ctrl+c、ctrl+v，在Mac里面是command+c、command+v。并且在Mac里面右键菜单的复制不是Windows的复制，是副本的意思，真正的复制是拷贝。。。Mac里面的F5不能刷新网页！！！</p><p>最骚的是，有些软件并不遵循这个设定，比如Chrome浏览器的右键选项复制就是真的复制，没有拷贝，有点操蛋，苹果就是这么倔强。</p><img src="/images/2021/WX20211204-215536@2x.png" /><p>在Mac里面command相当于win键、option相当于alt键，如果你有一个苹果键盘可能好一点，如果在普通的键盘，这个默认的键位相当不习惯，很容易按错。</p><p>对此，虽然可以通过第三方软件去改，或者设置修饰键，但是个人觉得既然用了Mac系统，就去适应它的设定比较好一点，如果带着Windows先入为主的概念来看Mac，确实有很多不适应的地方，但是折腾么，图个乐。。。</p><p>如果实在适应不了，格式化重装回<strong>Ubuntu</strong>或者<strong>Windows</strong>。。。Over</p>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;其实到今天，黑苹果的技术已经非常成熟了，甚至形成了一条产业链，有专门做黑苹果安装的某宝店和一些电脑店。其实之前我也装过几次黑苹果，就是没坚持用下去就格了，最近又装了个黑苹果，感觉挺好用的，今天咱们就总结一下目前最简单的方式。&lt;/p&gt;
&lt;h2 id=&quot;1-何为黑苹果？&quot;&gt;&lt;a href=&quot;#1-何为黑苹果？&quot; class=&quot;headerlink&quot; title=&quot;1.何为黑苹果？&quot;&gt;&lt;/a&gt;1.何为黑苹果？&lt;/h2&gt;&lt;p&gt;黑苹果并不是黑色的苹果，说到黑有些人觉得是不是有问题，比如黑客通常都被认为是干坏事的。其实黑苹果这个说法还是真和黑客有关系，因为黑客的英文叫&lt;strong&gt;Hacker&lt;/strong&gt;，而黑苹果英文叫&lt;strong&gt;Hackintosh&lt;/strong&gt;，Mac的全称是&lt;strong&gt;Macintosh&lt;/strong&gt;。&lt;/p&gt;
&lt;img src=&quot;/images/2021/WX20211204-221138@2x.png&quot; /&gt;

&lt;p&gt;苹果的OS从来都是只自用，搭配Mac电脑出售，不对外单独出售，如果你想用Mac系统就得买苹果电脑，但是呢？苹果的电脑卖又那么贵，确实贵。。。所以一群黑客破解了MacOS，可以装在普通电脑上面，所以叫黑苹果，简单说就是盗版了的MacOS，与之相对的白苹果就是指正经安装在Mac电脑上。&lt;/p&gt;
    
    </summary>
    
    
      <category term="Linux" scheme="https://wangbenjun.site/categories/Linux/"/>
    
    
      <category term="Mac" scheme="https://wangbenjun.site/tags/Mac/"/>
    
  </entry>
  
  <entry>
    <title>Golang各类型转换</title>
    <link href="https://wangbenjun.site/2021/coding/golang/type-conv.html"/>
    <id>https://wangbenjun.site/2021/coding/golang/type-conv.html</id>
    <published>2021-11-21T02:20:00.000Z</published>
    <updated>2024-12-29T03:02:14.810Z</updated>
    
    <content type="html"><![CDATA[<p>在使用Golang做业务的时候经常遇到各个类型之间的转换，今天就稍微总结一下常见类型之间的转换方式，主要是int、string、float64、[]byte这几个类型。</p><h2 id="1-int和string互转"><a href="#1-int和string互转" class="headerlink" title="1.int和string互转"></a>1.int和string互转</h2><p>这个用的还是比较多的，特别是在Web开发的时候，前端传值一般都是string居多，转换主要使用一个库<code>strconv</code>来操作：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// StringToInt string =&gt; int</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">StringToInt</span><span class="params">(str <span class="keyword">string</span>)</span> <span class="title">int</span></span>  &#123;</span><br><span class="line">    i, err := strconv.Atoi(str)</span><br><span class="line">    <span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="number">0</span></span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> i</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// IntToString int =&gt; string</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">IntToString</span><span class="params">(i <span class="keyword">int</span>)</span> <span class="title">string</span></span> &#123;</span><br><span class="line">    <span class="keyword">return</span> strconv.Itoa(i)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><a id="more"></a><p>很多人在转string的时候喜欢用<code>fmt.Sprintf(&quot;%d&quot;, i)</code>，结果一样。</p><p>至于int和int64之间的互转，直接<code>int64(i)</code>这种写法就可以了，但是可能会存在一个精度丢失，但是Go的int会根据不同的系统架构自动变化，在64位机器上就是int64，所以一般情况下这么转没问题。</p><h2 id="2-float和string互转"><a href="#2-float和string互转" class="headerlink" title="2.float和string互转"></a>2.float和string互转</h2><p>string转float还是用strconv里面的函数，但是float转string的时候的函数用法也着实复杂，所以很多人喜欢用<code>fmt.Sprintf(&quot;%f&quot;, f)</code>，结果一样。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// StringToFloat string =&gt; int </span></span><br><span class="line"><span class="comment">// 注意这个bitSize要么是32，要么是64，如果瞎写的话默认给你算64</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">StringToFloat</span><span class="params">(str <span class="keyword">string</span>)</span> <span class="title">float64</span></span>  &#123;</span><br><span class="line">    f, err := strconv.ParseFloat(str, <span class="number">64</span>)</span><br><span class="line">    <span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="number">0</span></span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> f</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// FloatToString float64 =&gt; string</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">FloatToString</span><span class="params">(f <span class="keyword">float64</span>)</span> <span class="title">string</span></span>  &#123;</span><br><span class="line">    <span class="keyword">return</span> strconv.FormatFloat(f, <span class="string">'g'</span>, <span class="number">-1</span>, <span class="number">64</span>)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="3-其它类型"><a href="#3-其它类型" class="headerlink" title="3.其它类型"></a>3.其它类型</h2><p>1.[]byte和string互转。string本质上就是字节数组[]byte，所以它们俩直接强转就行了，没有什么好说的。</p><p>2.int和float64互转。这2个类型之间也可以强制转换，但是也存在精度问题，比如100.9转换成int就变成100了，浮点数都会丢失。</p><p>3.int和[]byte互转。理论上这种需求很少，如果非要在做的话，涉及到[]byte的类型转换都可以先转成string，然后再强制转[]byte。</p>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;在使用Golang做业务的时候经常遇到各个类型之间的转换，今天就稍微总结一下常见类型之间的转换方式，主要是int、string、float64、[]byte这几个类型。&lt;/p&gt;
&lt;h2 id=&quot;1-int和string互转&quot;&gt;&lt;a href=&quot;#1-int和string互转&quot; class=&quot;headerlink&quot; title=&quot;1.int和string互转&quot;&gt;&lt;/a&gt;1.int和string互转&lt;/h2&gt;&lt;p&gt;这个用的还是比较多的，特别是在Web开发的时候，前端传值一般都是string居多，转换主要使用一个库&lt;code&gt;strconv&lt;/code&gt;来操作：&lt;/p&gt;
&lt;figure class=&quot;highlight go&quot;&gt;&lt;table&gt;&lt;tr&gt;&lt;td class=&quot;gutter&quot;&gt;&lt;pre&gt;&lt;span class=&quot;line&quot;&gt;1&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;2&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;3&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;4&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;5&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;6&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;7&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;8&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;9&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;10&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;11&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;12&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;13&lt;/span&gt;&lt;br&gt;&lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;code&quot;&gt;&lt;pre&gt;&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;comment&quot;&gt;// StringToInt string =&amp;gt; int&lt;/span&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;function&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;title&quot;&gt;StringToInt&lt;/span&gt;&lt;span class=&quot;params&quot;&gt;(str &lt;span class=&quot;keyword&quot;&gt;string&lt;/span&gt;)&lt;/span&gt; &lt;span class=&quot;title&quot;&gt;int&lt;/span&gt;&lt;/span&gt;  &amp;#123;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;    i, err := strconv.Atoi(str)&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;    &lt;span class=&quot;keyword&quot;&gt;if&lt;/span&gt; err != &lt;span class=&quot;literal&quot;&gt;nil&lt;/span&gt; &amp;#123;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;        &lt;span class=&quot;keyword&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;number&quot;&gt;0&lt;/span&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;    &amp;#125;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;    &lt;span class=&quot;keyword&quot;&gt;return&lt;/span&gt; i&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&amp;#125;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;comment&quot;&gt;// IntToString int =&amp;gt; string&lt;/span&gt;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&lt;span class=&quot;function&quot;&gt;&lt;span class=&quot;keyword&quot;&gt;func&lt;/span&gt; &lt;span class=&quot;title&quot;&gt;IntToString&lt;/span&gt;&lt;span class=&quot;params&quot;&gt;(i &lt;span class=&quot;keyword&quot;&gt;int&lt;/span&gt;)&lt;/span&gt; &lt;span class=&quot;title&quot;&gt;string&lt;/span&gt;&lt;/span&gt; &amp;#123;&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;    &lt;span class=&quot;keyword&quot;&gt;return&lt;/span&gt; strconv.Itoa(i)&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;&amp;#125;&lt;/span&gt;&lt;br&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/figure&gt;
    
    </summary>
    
    
      <category term="Golang" scheme="https://wangbenjun.site/categories/Golang/"/>
    
    
      <category term="Golang" scheme="https://wangbenjun.site/tags/Golang/"/>
    
  </entry>
  
  <entry>
    <title>为什么PHP都在转Go?</title>
    <link href="https://wangbenjun.site/2021/coding/from-php-to-go.html"/>
    <id>https://wangbenjun.site/2021/coding/from-php-to-go.html</id>
    <published>2021-11-14T04:39:00.000Z</published>
    <updated>2024-12-29T03:02:14.808Z</updated>
    
    <content type="html"><![CDATA[<p>作为一名以Web开发为主的程序员，上一次写PHP已经是2年前的时候了，不出意外今后应该都不会再去写PHP代码了，原因很简单：Go太好用了。</p><p>这个话题看上去像是语言之争，听起来没有意义，因为大家都说真正牛逼的人什么语言都能写，语言只是工具，最重要的是编程思想。话说的没错，但是从实际开发来说，不同语言的生态环境还是有点区别的，比如很多库包装和调用的形式不一样，来回切换成本很高。</p><p>举个例子，常用的http库，如果你写PHP你立马会想到Guzzle这个强大的库，如果是Go呢，标准库自带的功能就已经足够了，如果是Python，应该是用requests这个库，但是Java呢，我还真不清楚。。。</p><p>从实际工作的角度来说，一个公司的技术栈往往都是以某些语言为主，比如字节都是Go为主、百度偏向PHP、阿里京东基本上都是Java。原因有很多，但是大多都是规范、维护、内部学习等，毕竟你招聘的时候岗位上面都会说明主要语言。</p><a id="more"></a><h3 id="1-PHP那些年"><a href="#1-PHP那些年" class="headerlink" title="1.PHP那些年"></a>1.PHP那些年</h3><p>曾经有一段时间，那是PHP最辉煌的时代，是一个前端尚未独立，前后端不分的时代，那时候网页的HTML内容都是后端处理返回，这一过程简称“套模板”。</p><p>那时候对于前端来说，只需要给后端一个静态的HTML，后端会使用模板语言去渲染数据，生成一个动态内容的页面。那个年代不仅有PHP、还有JSP（Java的模板语言）、ASP，不过在这场模板语言之战中，PHP是最大的赢家，成为Web页面开发的王者。</p><img src="/images/2021/2021-11-14_14-47.png"/><p>那时候很多公司对于PHP的定位就是前端，PHPer主要的工作就是从后端服务（Java）获取数据，简单处理一下，然后渲染HTML模板返回就行了，这导致至今还有很多人依然以为PHP是写前端的。。。</p><p>然而这个辉煌时代没有维持多久，前端变天了，随着react、vue、angular等前端MVVC框架的诞生，前后端分离已经是大势所趋。现在不再需要后端去渲染模板了，后端只需要写接口，把数据以JSON的形式给到前端就行了。</p><p>很多人喜欢使用PHP原因是因为其作为一门弱类型语言，语法非常灵活，所以在套模板这方面确实快，简单的处理数据也方便，在当年，配合Apache服务器，简直是利器。</p><h3 id="2-来自Go的狙击"><a href="#2-来自Go的狙击" class="headerlink" title="2.来自Go的狙击"></a>2.来自Go的狙击</h3><p>在前后端分离的时代，虽然不用PHP套模板了，PHP还可以去写接口，配合一些PHP Web框架，写起接口来也是非常快的，三五行代码开启一个Web服务，相当于其它语言比如Java来说太简单方便了。</p><p>但是这一优势在Go面前算不上太大优势，Go开启的一个Web服务更简单，连nginx都帮你省了，而且在协程的加持下，性能非常高。</p><p>下面我从几个方面来说一下Go和PHP的对比：</p><h4 id="1-语言设计"><a href="#1-语言设计" class="headerlink" title="1.语言设计"></a>1.语言设计</h4><p>虽然把动态弱类型和静态强类型语言放在一起对比本身就不太公平，但是Go出身名门，由知名大佬操刀设计，这么多年来，在语言设计上面非常谨慎，所以Go的特性非常少，学习使用成本非常低，上手快。</p><p>PHP历史包袱有点重，且不论PHP几个大版本之间的兼容性问题，光语言设计本身的坑也不少，相信写过几年PHP的人都深有感触，说白了，背后没有大公司支撑操刀设计。</p><h4 id="2-PHP本质是C"><a href="#2-PHP本质是C" class="headerlink" title="2.PHP本质是C"></a>2.PHP本质是C</h4><p>如果你了解PHP你会发现很多PHP大佬本质上是C程序员，因为PHP本质上是C写的一个Web库，PHP的源码就是C，你写的每一行PHP代码都会被解释器翻译成C代码去执行。</p><p>所以如果你想精通PHP，那么你必须精通C语言，很多PHP程序员就是图着简单易用来的，你让去研究C语言，大部分人都做不到。这2者的跨度太大了，通常搞Web开发的都不会写C，精通C的人一般不会搞Web开发。本人不才，写了几年PHP也只是停留在语言本身层面，没有深入了解过其底层设计，看了就忘。</p><p>PHP的运行依赖大量C扩展，比如常见的mysql、curl、json、mbstring扩展等等，如果你的业务需要高性能计算，就得自己去写扩展，这意味着你还是离不开C语言。</p><h4 id="3-资源占用"><a href="#3-资源占用" class="headerlink" title="3.资源占用"></a>3.资源占用</h4><p>这2个语言之间除了性能的差异之外，对资源的占用更加明显，PHP非常耗资源，当你的并发上来之后你会发现CPU和内存可能会成为瓶颈。</p><p>虽然说Web层往往可以横向扩展，内存不够用无非就是多跑几台服务器的事情，但是当你的服务器数量达到成千上万规模，使用Go可以节省数倍的资源开销。</p><p>这时候有人会说，PHP有swoole，常驻内存，高性能，也有协程可以实现并发编程，资源占用低。个人感觉swoole完全偏离了PHP的初衷，使用swoole意味着你要抛弃PHP生态里面大量成熟的库，更像是断臂求生。还有一点，swoole掌握在商业公司手里，相信大家前一段时间也听说过一些争论，圈子有点乱，用Go的话放心Google不会收费的。换句话说，都用上了swoole了，学习成本也不低，何不尝试一下新语言Go呢？</p><h3 id="3-大势所趋"><a href="#3-大势所趋" class="headerlink" title="3.大势所趋"></a>3.大势所趋</h3><p>实际上，从目前国内市场来看，PHP转Go已经是大势所趋，很多培训班现在连PHP的课程都不开了，都在教Go。</p><p><img src="/images/2021/2021-11-14_14-58.png"></p><p>另外，很多正在用PHP的公司也正在尝试使用Go去做一些东西，一些老项目可能不会动，但是新项目用Go的很多很多，虽然说短期内PHP依然有市场，但是趋势不可逆。</p><p>随着Go的生态不断完善，第三方库越来越多，在Web开发方面，Go可能成为Java最有力的竞争者，而PHP则可能慢慢没落。</p><p>很早就有人说，Go是现代C语言，但现在看起来C依然还是那个C，在高性能领域的地位无可撼动。Go自带GC和runtime，在性能和易用性之间取一个平衡，这一点非常成功。</p><p>你说呢？</p>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;作为一名以Web开发为主的程序员，上一次写PHP已经是2年前的时候了，不出意外今后应该都不会再去写PHP代码了，原因很简单：Go太好用了。&lt;/p&gt;
&lt;p&gt;这个话题看上去像是语言之争，听起来没有意义，因为大家都说真正牛逼的人什么语言都能写，语言只是工具，最重要的是编程思想。话说的没错，但是从实际开发来说，不同语言的生态环境还是有点区别的，比如很多库包装和调用的形式不一样，来回切换成本很高。&lt;/p&gt;
&lt;p&gt;举个例子，常用的http库，如果你写PHP你立马会想到Guzzle这个强大的库，如果是Go呢，标准库自带的功能就已经足够了，如果是Python，应该是用requests这个库，但是Java呢，我还真不清楚。。。&lt;/p&gt;
&lt;p&gt;从实际工作的角度来说，一个公司的技术栈往往都是以某些语言为主，比如字节都是Go为主、百度偏向PHP、阿里京东基本上都是Java。原因有很多，但是大多都是规范、维护、内部学习等，毕竟你招聘的时候岗位上面都会说明主要语言。&lt;/p&gt;
    
    </summary>
    
    
      <category term="PHP" scheme="https://wangbenjun.site/categories/PHP/"/>
    
    
      <category term="PHP" scheme="https://wangbenjun.site/tags/PHP/"/>
    
  </entry>
  
  <entry>
    <title>使用Go Plugin实现插件化编程</title>
    <link href="https://wangbenjun.site/2021/coding/golang/go-plugin.html"/>
    <id>https://wangbenjun.site/2021/coding/golang/go-plugin.html</id>
    <published>2021-10-29T15:43:00.000Z</published>
    <updated>2024-12-29T03:02:14.809Z</updated>
    
    <content type="html"><![CDATA[<p>说到插件这个东西，很多人都不陌生，一般来说，插件化有几个好处，一个是增加程序扩展性，丰富功能。另外，还可以实现热更新，有些大型应用，动辄几个GB的安装程序，如果一个小小的更新就需要重新下载整个程序，这时候我们就可以把经常更新的模块插件化，这样更新的时候只需要下载一个小更新文件。比如说平时咱们Chrome浏览器都会装一些插件，可以扩展浏览器实现更多的功能，还能灵活的安装卸载。</p><p>Golang在1.8版本之后提供了一个 Plugin 的机制，可以动态的加载so文件，实现插件化，虽然并不是非常成熟，但是在特定的情况下还是非常好用。</p><blockquote><p>Currently plugins are only supported on Linux, FreeBSD, and macOS.</p></blockquote><a id="more"></a><h2 id="1-快速开始"><a href="#1-快速开始" class="headerlink" title="1.快速开始"></a>1.快速开始</h2><p>插件代码和普通代码没什么区别，只是在编译的时候不一样，但是要求是必须只有一个main包</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> main</span><br><span class="line"></span><br><span class="line"><span class="keyword">var</span> Name = <span class="string">"Plugin Name"</span></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">GetName</span><span class="params">()</span> <span class="title">string</span></span> &#123;</span><br><span class="line">    <span class="keyword">return</span> Name</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>使用<code>go build -buildmode=plugin</code>编译，会得到一个so文件，怎么使用这个文件呢？</p><p>很简单，分三步：</p><p>1.先打开so文件，如果一个插件已经被打开了，那么会返回已存在的plugin</p><p>2.使用Lookup查找需要调用的变量或者函数，名字必须大写开头</p><p>3.断言后调用</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">    <span class="comment">//打开加载插件，参数是插件的存储位置，可以是相对路径</span></span><br><span class="line">    open, err := plugin.Open(<span class="string">"/home/jwang/Documents/plg.so"</span>)</span><br><span class="line">    <span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">        <span class="built_in">panic</span>(err)</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">//查找标识符</span></span><br><span class="line">    lookup, err := open.Lookup(<span class="string">"GetName"</span>)</span><br><span class="line">    <span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">        <span class="built_in">panic</span>(err)</span><br><span class="line">    &#125;</span><br><span class="line">    res := lookup.(<span class="function"><span class="keyword">func</span><span class="params">()</span> <span class="title">string</span>)<span class="params">()</span></span></span><br><span class="line">    fmt.Printf(<span class="string">"%v\n"</span>, res)</span><br><span class="line"></span><br><span class="line">    name, err := open.Lookup(<span class="string">"Name"</span>)</span><br><span class="line">    <span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">        <span class="built_in">panic</span>(err)</span><br><span class="line">    &#125;</span><br><span class="line">    fmt.Printf(<span class="string">"%v\n"</span>, *name.(*<span class="keyword">string</span>))</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>从上面的代码可以看到，插件的使用方式非常朴实无华，简单易懂。</p><p>一般来说，为了实现插件化，可以事先定义好一些接口，然后由插件去实现这些接口，这样才能保证一致性，但是接口的定义不能写在插件包或者调用包里面。这时候就需要定义一个专门的公共包，把接口的定义写在里面，这样插件包和调用包都可以引用。</p><h2 id="2-注意事项"><a href="#2-注意事项" class="headerlink" title="2.注意事项"></a>2.注意事项</h2><p>之所以说这个插件方案不成熟，主要是由于主程序和插件程序之间存在很强的依赖性，比如：</p><p>1.编译的GO版本必须完全一致</p><p>2.双方依赖的公共第三方库版本必须完全一致</p><p>3.GOPATH也得保持一致，这一点可以在编译时候使用trimpath参数解决</p><p>4.插件加载之后无法卸载</p><p>这些问题短时间内好像官方也没有解决的意思，或者说无法解决。总之，Go plugin目前的应用很少，毕竟作为网络编程语言，在容器化大行其道的环境下，更新程序是一件很轻松的事情，除非有特殊需要。</p><h2 id="3-隐藏的坑"><a href="#3-隐藏的坑" class="headerlink" title="3.隐藏的坑"></a>3.隐藏的坑</h2><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// Open opens a Go plugin.</span></span><br><span class="line"><span class="comment">// If a path has already been opened, then the existing *Plugin is returned.</span></span><br><span class="line"><span class="comment">// It is safe for concurrent use by multiple goroutines.</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">Open</span><span class="params">(path <span class="keyword">string</span>)</span> <span class="params">(*Plugin, error)</span></span> &#123;</span><br><span class="line">    <span class="keyword">return</span> open(path)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>注意上面这段注释：如果一个路径已经被打开，那么将会返回一个已存在的插件。</p><p>这是啥意思呢？</p><p>假设你的服务是一个常驻进程，会定时的检测插件是否更新，如果有更新的话就拉取新的插件到指定目录，然后加载插件。</p><p>以上的想法很美好，但是你会发现其实插件压根没更新，你用的还是老的插件，虽然那个so文件已经是最新的了，但是Golang并不会加载新的插件。</p><p>因为它只认路径，换句话说就是文件名，所以如果的插件需要更新，务必在文件名上面带上版本号，换句话说，在一个进程内，插件只能新增，不能更新，除非你的服务重启。</p><p>这个确实非常不方便，究其原因，可能是官方压根不上心吧！</p>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;说到插件这个东西，很多人都不陌生，一般来说，插件化有几个好处，一个是增加程序扩展性，丰富功能。另外，还可以实现热更新，有些大型应用，动辄几个GB的安装程序，如果一个小小的更新就需要重新下载整个程序，这时候我们就可以把经常更新的模块插件化，这样更新的时候只需要下载一个小更新文件。比如说平时咱们Chrome浏览器都会装一些插件，可以扩展浏览器实现更多的功能，还能灵活的安装卸载。&lt;/p&gt;
&lt;p&gt;Golang在1.8版本之后提供了一个 Plugin 的机制，可以动态的加载so文件，实现插件化，虽然并不是非常成熟，但是在特定的情况下还是非常好用。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Currently plugins are only supported on Linux, FreeBSD, and macOS.&lt;/p&gt;
&lt;/blockquote&gt;
    
    </summary>
    
    
      <category term="Golang" scheme="https://wangbenjun.site/categories/Golang/"/>
    
    
      <category term="Golang" scheme="https://wangbenjun.site/tags/Golang/"/>
    
  </entry>
  
  <entry>
    <title>漫谈IT项目开发流程</title>
    <link href="https://wangbenjun.site/2021/coding/project-manager.html"/>
    <id>https://wangbenjun.site/2021/coding/project-manager.html</id>
    <published>2021-08-24T12:30:00.000Z</published>
    <updated>2024-12-29T03:02:14.812Z</updated>
    
    <content type="html"><![CDATA[<p>本人工作多年，干倒不少公司，待过十几个人的团队，也进过上百人的团队，也算是项目开发经验丰富。今天我就来说说互联网公司项目开发的常见流程，主要来自于本人经验总结，结合了我这么多年来不同公司的实践所得。对于平时个人开发或者参与多人项目开发，大家可以参考一下，不一定适合所有公司，采取其中部分流程也是可行。</p><p>个人觉得从整体来说，项目开发流程大体分为3个大的部分，第一个部分就是需求，这个需求来源可能是BOSS、可能是用户反馈、可能是产品人员拍脑袋。第二部分是编程开发，程序员干活的地方。第三部分是测试验收，主要是测试人员干活的地方。</p><a id="more"></a><h2 id="1-需求"><a href="#1-需求" class="headerlink" title="1.需求"></a>1.需求</h2><p>这里说的需求往大了说可能是一个新项目，比如做一个商城APP，往小了说也可能一个项目里面的一个新模块新功能，我觉得都可以算需求。</p><p>大部分公司都有产品经理这个角色，虽然名字带着个经理两个字，但是实际上也是普通员工，这个角色说简单点就是画个原型图，对着友商的产品抄一抄，可能偶尔对开发程序员来句: 为什么xxx家都能实现你不能实现？</p><p>由于经常给程序员提各种需求、提bug，少不了争论和撕逼。但是这个角色往高了说，像微信的产品经理李小龙这样的可不是画原型了，他们则是对自己得产品有着深刻理解，是一个产品的灵魂设计者。</p><img src="/images/2021/2021-08-24_21-15.png" /> <p>言归正传，玩归玩，闹归闹，产品这个职位不一定得有，但是需求一定要明确：你得告诉开发人员要做什么东西，即使没有原型图，你拿着友商的竞品展示一下对着抄也行啊。</p><p>需求应该有多明确？我见过写的最好的需求文档，是精确到每一个输入输出数据、包括长度大小，有一些高保真的原型图甚至都把交互设计出来了，还能点，所以越细越好，不然在实际开发中还得写一步问一步，只要产品不嫌烦，我没意见。</p><p>一般来说，一个需求在产品内部之间已经做过评审等工作，需求给到开发这边已经相对比较完善了，但是依然需要对齐。</p><p>所以这一部分在真实的项目开发中，一般有下面几件事要去做：</p><p>1.参加产品会议。参会人员包括前后端开发、测试等支撑人员，会上由产品人员对需求做一个简单明了的介绍，并配备项目流程图、原型图等资料，明确项目背景、需要做的东西、以及项目边界。</p><p>2.介绍完之后，可以问一下参会人员有没有什么疑问，不明白的地方，由产品人员解答相关问题</p><p>3.作为开发，开完会心里面应该有个概念了，有什么问题，该提就提，早点和产品说，现在改还来得及，产品写的不一定对，错的地方你可以纠正。对于有些不好实现的功能也可以和产品商量一下可以不可以换种方式或者放到下一期需求，等等。</p><p>说白了，这一步就是把相关人员拉到一起对一下需求，答疑解惑，这一步一定得有。这一步不仅仅是把需求给到开发，更是对需求的一个理解贯彻，哪些能做哪些不能做都需要开会讨论明确的，不是说一个文档扔给开发，产品就不管了。</p><p>接下来，一般产品在和开发等人员对完需求之后，会做一些修改，该明确的明确，双方都没有问题的时候，正式提需求，邮件抄送相关人员，提个禅道或者类似的项目流程管理的东西。</p><p>关于项目排期这块，大部分时候是由开发来定，如果需求不紧急的话。但是有些公司比较追求效率，很多时候是由产品定，或者老板直接拍板定个日期。个人觉得只要不是太离谱的话都没问题，如果时间实在不够，完成不了，一定要联合开发人员一起反馈，调整排期，千万不要等到最后几天才说时间不够，完成不了，这样显得太业余。</p><blockquote><p>关于需求这块，我觉得最重要的是要明确需求，我见过很多产品自己都对想做的东西想的不太明白，考虑的不细致，做到一半发现设计有bug或者遗漏的地方。不求你想的十全十分，但是至少逻辑上要自恰，流程要走通。</p></blockquote><h2 id="2-开发"><a href="#2-开发" class="headerlink" title="2.开发"></a>2.开发</h2><p>拿到需求之后，接下来就是编程开发，一般来说，一个需求会分配给好几个开发一起去做，这时候就得选一个人站出来负责整体把控，把大家拉一个群，方便交流。然后分解需求，分工，然后写各自的功能就行了，有问题群里及时沟通。对于敏捷开发等一些开发模式，公司如果有精力也有相关人员去管理，也可以去实践一下（对人要求太高）。</p><p>本人之前主要是从事Web开发为主，说简单点就是写API接口，自然少不了数据表设计、前后端接口联调等工作，所以有时候还包括以下工作要去做：</p><h3 id="1-数据表设计"><a href="#1-数据表设计" class="headerlink" title="1.数据表设计"></a>1.数据表设计</h3><p>这点很重要，一个需求拿到手，需要那些数据文档里面应该都体现了，需要几张表应该都能估算出来，这时候应该让一个人，或者大家一起，把这次需求需要用到的数据表设计一下。然后找时间拉个会，一起评审一下字段结构设计是否合理，该改的就改。</p><blockquote><p>千万不要每个人自己搞自己的，应该整体考虑设计新增的每一张表，不然时间一长，项目里面的各种表肯定很混乱</p></blockquote><h3 id="2-接口设计"><a href="#2-接口设计" class="headerlink" title="2.接口设计"></a>2.接口设计</h3><p>在前后端分离的时代，在涉及前端页面的项目里面，自然少不了接口文档。在我的职业生涯里面，遇到过2种方式，一种是先写接口，再完善文档，然后告诉前端哪个地方调哪个接口，这种方式就需要前端等后端先开发接口才能写页面。</p><p>还有一种方式，就是先写接口文档，再写接口，这意味着开发需要先发挥想象力，把需要的接口字段定好，和前端对齐之后，前端就可以使用mock工具模拟接口数据进行开发，完全不依赖后端，等到后端接口开发完毕之后再进行联调。</p><p>对于第二种方式，有些人可能觉得我接口都没写，我咋知道有那些字段，实际上，如果需求很明确，数据表也设计完成，这不是什么问题。</p><p>对于简单需求，接口数量少，采用第一种方式也没啥问题，如果大的需求，我建议还是第二种比较好，更加科学合理，前后端可以同步开发，而且在沟通上也更明确，不用扯皮。</p><p>完成上面2个工作之后，对于程序员来说，剩下的就是按照产品需求文档，实现具体的业务逻辑了，有条不紊。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">    echo <span class="string">"Hello World!"</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="3-测试"><a href="#3-测试" class="headerlink" title="3.测试"></a>3.测试</h2><p>测试可轻可重，对于有些公司来说，在项目开发中比重与编码相当，但也有公司觉得测试只是附属工作，不必浪费太多时间，开发人员写好单元测试就可以了。</p><p>不过做过实际项目开发的人都应该知道对于那些复杂的需求，指望完全依靠单元测试来保障项目质量简直是做梦…</p><p>严格来说，测试人员在第一个阶段就需要介入了，要跟着开发一起参会，需要去理解产品需求，然后编写对应的测试用例，测试用例需要尽可能的覆盖所有情况。我之前有家公司还会要求评审测试用例，一个简单的需求都可能有几十个用例。</p><img src="/images/2021/2021-08-24_21-21.png" /> <p>对于API接口，测试还需要对每个接口单独做测试，使用一些测试工具进行测试，对于部分接口可能还会做一些性能测试。</p><p>前后端联调完之后，还需要测试整体功能是否符合预期，必要时使用抓包工具定位问题，给开发提bug，等等！</p><p>总之，测试是一个非常系统性和专业性的工作，并不是一个简单的活，这里只是简单说几句，一个合格负责任的测试并不轻松，对产品需求的理解也需要到位，还需要和开发一起加班，开发改完bug之后，测试需要验证，做回归测试，编写测试报告等等。</p><p>但是我也见过有些公司的测试只做最后的黑盒测试，也就是像普通用户一样点一点，用一用，跑一下正常的流程，尽可能的模拟正常用户去使用，发现存在的问题。</p><p>甚至对于某些不太重要的项目，比如管理后台，开发自测就可以了。。。</p><hr><p>在整个项目开发过程中，产品这个项目应该是贯穿整个项目的，不是说提一个需求，要一个排期，然后撒手不管坐等deadline就行了。</p><p>一个合格的产品应该时刻关心项目开发进度，必要时给予相关人员帮忙，协调各部门之间的工作，在最后阶段也可以参与到黑盒测试中，做一个验收工作。在开发过程中如果需要调整需求，必须及时通知相关人员。</p><hr><p>最后，简单总结下整体流程：</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">1、需求对齐，拉上前后端开发、测试等人员</span><br><span class="line">2、前后端对齐，对于后端，需要对齐数据表、接口字段等前期准备工作</span><br><span class="line">3、程序编码</span><br><span class="line">4、测试，包括接口测试、功能测试</span><br><span class="line">5、验收</span><br></pre></td></tr></table></figure><p>希望对一些新人有用！</p>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;本人工作多年，干倒不少公司，待过十几个人的团队，也进过上百人的团队，也算是项目开发经验丰富。今天我就来说说互联网公司项目开发的常见流程，主要来自于本人经验总结，结合了我这么多年来不同公司的实践所得。对于平时个人开发或者参与多人项目开发，大家可以参考一下，不一定适合所有公司，采取其中部分流程也是可行。&lt;/p&gt;
&lt;p&gt;个人觉得从整体来说，项目开发流程大体分为3个大的部分，第一个部分就是需求，这个需求来源可能是BOSS、可能是用户反馈、可能是产品人员拍脑袋。第二部分是编程开发，程序员干活的地方。第三部分是测试验收，主要是测试人员干活的地方。&lt;/p&gt;
    
    </summary>
    
    
      <category term="Coding" scheme="https://wangbenjun.site/categories/Coding/"/>
    
    
      <category term="编程" scheme="https://wangbenjun.site/tags/%E7%BC%96%E7%A8%8B/"/>
    
  </entry>
  
  <entry>
    <title>Golang并发调用接口-Context</title>
    <link href="https://wangbenjun.site/2021/coding/golang/context.html"/>
    <id>https://wangbenjun.site/2021/coding/golang/context.html</id>
    <published>2021-08-07T12:19:00.000Z</published>
    <updated>2024-12-29T03:02:14.809Z</updated>
    
    <content type="html"><![CDATA[<p>Context这个词在编程中经常见到，一般被翻译成上下文，这个翻译既抽象又形象。在日常生活中，普通人说的上下文一般指的是文章的上下文，指的是我们在看文章的时候要结合上下文去分析某句话的意思。</p><p>举个例子，有一句话叫： 这个东西挺好吃，小明吃了一斤。光看这句话，你并不知道小明到底吃的是什么？毕竟这个可能是任何东西，你必须翻看文章前面的语句去看这个到底代表的是啥意思，这里上下文表示的就是一个前后的依赖关系。</p><p>在CPU里面，context也有类似的意思，我们常说CPU上下文切换，因为单核CPU一次只能执行一个运算，我们之所以可以同时听歌、写代码，那是因为有一种CPU时间片轮转机制在不同进程之间切换，由于其速度非常快，导致你觉得好像是在同时运行。在这个切换进程的过程中，CPU就需要保存上下文，这里的上下文其实就是寄存器、程序计数器等数据。</p><p>其实，虽然上下文的叫法理论上讲没太大毛病，但是其实翻译成语境、或者环境更加贴切。言归正传，下面咱就说说Go标准库里面的Context是啥、以及能干啥？</p><a id="more"></a><h2 id="1-手动终止协程"><a href="#1-手动终止协程" class="headerlink" title="1.手动终止协程"></a>1.手动终止协程</h2><p>在Golang里面，Context往往是和协程紧密联系在一起的，因为Context是并发安全的，可以在不同协程之间同步状态。</p><p>举个例子： 主进程里面启动多个协程去处理数据，当我们关闭程序的时候，有一些协程还在处理中，这时候你该咋办？如果直接不管可能导致某些数据丢失，这时候你就可以通过Context去通知协程做一些终止前的准备工作。</p><p>比如下面这个例子里面，我们就启动了3个协程，5s之后通过context发送取消信号，子协程在收到信息之后依次退出。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">    ctx, cancelFunc := context.WithCancel(context.Background())</span><br><span class="line"></span><br><span class="line">    <span class="keyword">for</span> _, i := <span class="keyword">range</span> []<span class="keyword">int</span>&#123;<span class="number">1</span>, <span class="number">2</span>, <span class="number">3</span>&#125; &#123;</span><br><span class="line">        <span class="keyword">go</span> doSomething(ctx, i)</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 5s之后通知所有协程关闭</span></span><br><span class="line">    time.Sleep(time.Second * <span class="number">5</span>)</span><br><span class="line">    cancelFunc()</span><br><span class="line"></span><br><span class="line">    <span class="comment">// 纯粹是留点时间给协程打印退出信息</span></span><br><span class="line">    time.Sleep(time.Second * <span class="number">5</span>)</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">doSomething</span><span class="params">(ctx context.Context, i <span class="keyword">int</span>)</span></span> &#123;</span><br><span class="line">    <span class="keyword">for</span> &#123;</span><br><span class="line">        <span class="keyword">select</span> &#123;</span><br><span class="line">        <span class="keyword">case</span> &lt;-ctx.Done(): <span class="comment">//收到退出信号</span></span><br><span class="line">            fmt.Printf(<span class="string">"Prepare to shutdown: %d\n"</span>, i)</span><br><span class="line">            <span class="keyword">return</span></span><br><span class="line">        <span class="keyword">default</span>:</span><br><span class="line">            fmt.Printf(<span class="string">"Do Something: %d\n"</span>, i)</span><br><span class="line">            time.Sleep(time.Second)</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>此外，Context库还有一个<code>WithTimeout</code>，其实他们俩用法差不多，唯一的区别就是<code>WitchCancel</code>是需要你手动调用取消函数，而且Timeout这个顾名思义，是定时自动取消。</p><h2 id="2-定时自动终止"><a href="#2-定时自动终止" class="headerlink" title="2.定时自动终止"></a>2.定时自动终止</h2><p>以上面的例子为基础，我们只需要把main里面稍微改动一下，使用<code>WithTimeout</code>即可。这个cancelFunc是可以忽略的，但是最好用defer调用一下，否则可能导致内存泄露。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">    ctx, cancelFunc := context.WithTimeout(context.Background(), time.Second * <span class="number">5</span>)</span><br><span class="line">    <span class="keyword">defer</span> cancelFunc()</span><br><span class="line">    <span class="keyword">for</span> _, i := <span class="keyword">range</span> []<span class="keyword">int</span>&#123;<span class="number">1</span>, <span class="number">2</span>, <span class="number">3</span>&#125; &#123;</span><br><span class="line">        <span class="keyword">go</span> doSomething(ctx, i)</span><br><span class="line">    &#125;</span><br><span class="line">    time.Sleep(time.Second * <span class="number">10</span>)</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>除此之外，还有一个<code>WithDeadLine</code>，意思就是设置一个截止时间，到了这个时间就自动取消，不过在我们这个例子里面，基本上和Timeout等价，稍微改动即可：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">ctx, cancelFunc := context.WithDeadline(context.Background(), time.Now().Add(time.Second * <span class="number">5</span>))</span><br></pre></td></tr></table></figure><p>实际还得看应用场景去选择使用那个函数。</p><p>下面，咱们来看一个比较贴合实际的应用场景： 假设我们需要并发的调用3个接口，而且还需要获取返回结果，并且设置一个超时时间，比如3s。</p><p>并发调3个接口并不是太大问题，我们只需要启动3个协程即可，获取响应结果的话，可以采用chan。在超时这块，实际上如果只是调接口的话，实际上Go的标准库里面httpClient可以设置一个超时时间。</p><p>所以有一个简单的写法：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">    <span class="keyword">var</span> (</span><br><span class="line">        <span class="comment">// 设置请求超时时间</span></span><br><span class="line">        client = http.Client&#123;Timeout: time.Second * <span class="number">3</span>&#125;</span><br><span class="line">        ch     = <span class="built_in">make</span>(<span class="keyword">chan</span> []<span class="keyword">byte</span>)</span><br><span class="line">    )</span><br><span class="line">    <span class="keyword">go</span> request(client, <span class="string">"https://www.baidu.com"</span>, ch)</span><br><span class="line">    <span class="keyword">go</span> request(client, <span class="string">"https://www.taobao.com"</span>, ch)</span><br><span class="line">    <span class="keyword">go</span> request(client, <span class="string">"https://www.google.com"</span>, ch)</span><br><span class="line"></span><br><span class="line">    i := <span class="number">0</span></span><br><span class="line">    <span class="keyword">for</span> &#123;</span><br><span class="line">        resp := &lt;-ch</span><br><span class="line">        fmt.Printf(<span class="string">"%d\n"</span>, <span class="built_in">len</span>(resp))</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 接收3个之后关闭chan</span></span><br><span class="line">        i++</span><br><span class="line">        <span class="keyword">if</span> i == <span class="number">3</span> &#123;</span><br><span class="line">            <span class="built_in">close</span>(ch)</span><br><span class="line">            <span class="keyword">break</span></span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">// more code....</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">request</span><span class="params">(client http.Client, apiUrl <span class="keyword">string</span>, res <span class="keyword">chan</span> []<span class="keyword">byte</span>)</span></span> &#123;</span><br><span class="line">    req, err := http.NewRequest(<span class="string">"GET"</span>, apiUrl, <span class="literal">nil</span>)</span><br><span class="line">    <span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">        res &lt;- <span class="literal">nil</span></span><br><span class="line">        <span class="keyword">return</span></span><br><span class="line">    &#125;</span><br><span class="line">    resp, err := client.Do(req)</span><br><span class="line">    <span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">        res &lt;- <span class="literal">nil</span></span><br><span class="line">        <span class="keyword">return</span></span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">defer</span> resp.Body.Close()</span><br><span class="line">    all, err := ioutil.ReadAll(resp.Body)</span><br><span class="line">    <span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">        res &lt;- <span class="literal">nil</span></span><br><span class="line">        <span class="keyword">return</span></span><br><span class="line">    &#125;</span><br><span class="line">    res &lt;- all</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>这种依赖httpClient的写法大部分情况下没问题，但是假设子协程出问题了，没有往chan里面送东西，这个程序就会一直阻塞在那里。</p><p>对于这种情况，有2种解决方案：</p><h3 id="一、接收端超时"><a href="#一、接收端超时" class="headerlink" title="一、接收端超时"></a>一、接收端超时</h3><p>接收端指的是上面代码里面接收结果的chan，我们可以设置一个超时时间，比如下面这个例子里面，我设置了3s，超过3s我就不等了，该干啥干啥去。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">    <span class="keyword">var</span> ch = <span class="built_in">make</span>(<span class="keyword">chan</span> []<span class="keyword">byte</span>)</span><br><span class="line">    <span class="keyword">go</span> request(<span class="string">"https://www.baidu.com"</span>, ch)</span><br><span class="line">    <span class="keyword">go</span> request(<span class="string">"https://www.taobao.com"</span>, ch)</span><br><span class="line">    <span class="keyword">go</span> request(<span class="string">"https://www.google.com"</span>, ch)</span><br><span class="line"></span><br><span class="line">    i := <span class="number">0</span></span><br><span class="line">    timer := time.NewTimer(time.Second * <span class="number">3</span>)</span><br><span class="line">L:</span><br><span class="line">    <span class="keyword">for</span> &#123;</span><br><span class="line">        <span class="keyword">select</span> &#123;</span><br><span class="line">        <span class="keyword">case</span> &lt;-timer.C:</span><br><span class="line">            <span class="keyword">break</span> L</span><br><span class="line">        <span class="keyword">case</span> resp := &lt;-ch:</span><br><span class="line">            fmt.Printf(<span class="string">"%d\n"</span>, <span class="built_in">len</span>(resp))</span><br><span class="line">            <span class="comment">// 接收3个之后关闭chan</span></span><br><span class="line">            i++</span><br><span class="line">            <span class="keyword">if</span> i == <span class="number">3</span> &#123;</span><br><span class="line">                <span class="built_in">close</span>(ch)</span><br><span class="line">                <span class="keyword">break</span> L</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">// more code...</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="二、执行端超时"><a href="#二、执行端超时" class="headerlink" title="二、执行端超时"></a>二、执行端超时</h3><p>这种方式意思就是我们要保证协程肯定能返回结果，即使超时也要返回一个nil，那这里就还得用到Context来做这件事。<br>在子协程里面，我们通过go启动一个协程，当收到取消信号的时候就返回一个nil，表示已经超时了。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> &#123;</span><br><span class="line">    ctx, cancelFunc := context.WithTimeout(context.Background(), time.Second*<span class="number">3</span>)</span><br><span class="line">    <span class="keyword">defer</span> cancelFunc()</span><br><span class="line"></span><br><span class="line">    <span class="keyword">var</span> ch = <span class="built_in">make</span>(<span class="keyword">chan</span> []<span class="keyword">byte</span>)</span><br><span class="line">    <span class="keyword">go</span> request(ctx, <span class="string">"https://www.baidu.com"</span>, ch)</span><br><span class="line">    <span class="keyword">go</span> request(ctx, <span class="string">"https://www.taobao.com"</span>, ch)</span><br><span class="line">    <span class="keyword">go</span> request(ctx, <span class="string">"https://www.google.com"</span>, ch)</span><br><span class="line"></span><br><span class="line">    i := <span class="number">0</span></span><br><span class="line">    <span class="keyword">for</span> &#123;</span><br><span class="line">        resp := &lt;-ch</span><br><span class="line">        fmt.Printf(<span class="string">"%d\n"</span>, <span class="built_in">len</span>(resp))</span><br><span class="line">        <span class="comment">// 接收3个之后关闭chan</span></span><br><span class="line">        i++</span><br><span class="line">        <span class="keyword">if</span> i == <span class="number">3</span> &#123;</span><br><span class="line">            <span class="built_in">close</span>(ch)</span><br><span class="line">            <span class="keyword">break</span></span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">// more code...</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">request</span><span class="params">(ctx context.Context, apiUrl <span class="keyword">string</span>, res <span class="keyword">chan</span> []<span class="keyword">byte</span>)</span></span> &#123;</span><br><span class="line">    <span class="keyword">go</span> <span class="function"><span class="keyword">func</span><span class="params">()</span></span> &#123;</span><br><span class="line">        <span class="keyword">select</span> &#123;</span><br><span class="line">        <span class="keyword">case</span> &lt;-ctx.Done():</span><br><span class="line">            fmt.Println(<span class="string">"timeout..."</span>)</span><br><span class="line">            res &lt;- <span class="literal">nil</span></span><br><span class="line">        &#125;</span><br><span class="line">    &#125;()</span><br><span class="line">    resp, err := http.Get(apiUrl)</span><br><span class="line">    <span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">        res &lt;- <span class="literal">nil</span></span><br><span class="line">        <span class="keyword">return</span></span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">defer</span> resp.Body.Close()</span><br><span class="line">    all, err := ioutil.ReadAll(resp.Body)</span><br><span class="line">    <span class="keyword">if</span> err != <span class="literal">nil</span> &#123;</span><br><span class="line">        res &lt;- <span class="literal">nil</span></span><br><span class="line">        <span class="keyword">return</span></span><br><span class="line">    &#125;</span><br><span class="line">    <span class="comment">// 模拟超时</span></span><br><span class="line">    time.Sleep(time.Second * <span class="number">10</span>)</span><br><span class="line">    res &lt;- all</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>执行结果如下：</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">timeout...</span><br><span class="line">0</span><br><span class="line">timeout...</span><br><span class="line">0</span><br><span class="line">timeout...</span><br><span class="line">0</span><br></pre></td></tr></table></figure><h2 id="3-传递值"><a href="#3-传递值" class="headerlink" title="3.传递值"></a>3.传递值</h2><p><code>WithValue</code>这个用的不多，效果和函数之间传参差不多，但是context的最大特点就是可以继承，当你的协程链条特别长的时候，这个就比较有意义了，比如说在不同子协程之间传递一些通用的配置变量。</p><p>纸上得来终觉浅，绝知此事要躬行，网上关于Context实现的原理的文章挺多，这里我就不献丑了，自己研究也不深，更多的研究其实际应用场景，加深自己的理解。希望上面的一些例子对你实际开发有用。</p>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;Context这个词在编程中经常见到，一般被翻译成上下文，这个翻译既抽象又形象。在日常生活中，普通人说的上下文一般指的是文章的上下文，指的是我们在看文章的时候要结合上下文去分析某句话的意思。&lt;/p&gt;
&lt;p&gt;举个例子，有一句话叫： 这个东西挺好吃，小明吃了一斤。光看这句话，你并不知道小明到底吃的是什么？毕竟这个可能是任何东西，你必须翻看文章前面的语句去看这个到底代表的是啥意思，这里上下文表示的就是一个前后的依赖关系。&lt;/p&gt;
&lt;p&gt;在CPU里面，context也有类似的意思，我们常说CPU上下文切换，因为单核CPU一次只能执行一个运算，我们之所以可以同时听歌、写代码，那是因为有一种CPU时间片轮转机制在不同进程之间切换，由于其速度非常快，导致你觉得好像是在同时运行。在这个切换进程的过程中，CPU就需要保存上下文，这里的上下文其实就是寄存器、程序计数器等数据。&lt;/p&gt;
&lt;p&gt;其实，虽然上下文的叫法理论上讲没太大毛病，但是其实翻译成语境、或者环境更加贴切。言归正传，下面咱就说说Go标准库里面的Context是啥、以及能干啥？&lt;/p&gt;
    
    </summary>
    
    
      <category term="Golang" scheme="https://wangbenjun.site/categories/Golang/"/>
    
    
      <category term="Golang" scheme="https://wangbenjun.site/tags/Golang/"/>
    
  </entry>
  
  <entry>
    <title>在阿里干了1年多的外包是什么体验？</title>
    <link href="https://wangbenjun.site/2021/life/one-year-and-half.html"/>
    <id>https://wangbenjun.site/2021/life/one-year-and-half.html</id>
    <published>2021-07-25T14:01:00.000Z</published>
    <updated>2025-01-24T13:37:52.402Z</updated>
    
    <content type="html"><![CDATA[<p>之前有写好几篇文章是关于阿里外包的，比如<a href="https://wangbenjun.site/2020/life/2019-4.html">《谈一谈阿里外包工作》</a> 等等，感兴趣的可以翻一下我之前写的。</p><p>不知不觉，在阿里干外包已经干了1年半了，不长也不短，渐渐的感觉自己已经非常适应这样的生活，不太愿意去尝试新的工作，一直也没静下心来思考这些问题，今天刚好决定静下心来写点东西，分享一下这1年多的经历。</p><a id="more"></a><h2 id="1-日复一日"><a href="#1-日复一日" class="headerlink" title="1.日复一日"></a>1.日复一日</h2><p>这1年多说起来也惭愧，并没有做什么值得一说的东西，基本上都是一些重复性工作，比如数据白屏化、数据处理、后端CURD接口等等，另外还写了很多基于antd库的前端页面。</p><p>同部门和我坐在一起的外包同事大概有7-8个，但是归属不同的主管，干的工作内容也有所差异，不过平时也和他们聊天交流，大多都是在做一些相对杂碎的活，简单说就是哪里需要干哪里，并没有一个完整的项目去给你做。</p><p>我所在的部门是属于阿里云的内部平台，他们正式工做的工作，大概就是收集各个服务节点上报的数据、处理这些数据，然后基于数据构建策略模型，得到一定的结果，再去验证结果是否符合预期。</p><p>对于外包来说，能做的东西不多，也就是接手一些不重要、边缘化的项目，此外就是给正式工打杂，做一些辅助性的工作，听上去挺无聊，实际上确实是这样，技术难度并不高。</p><p>说到学东西，其实别说外包，就是正式员工也不一定能够接触到项目最核心的代码，每个人都有自己负责的一块，有一个方向，大大小小的项目数不胜数，但是最核心的那些不是一般人接触不到。</p><h2 id="2-朝九晚五"><a href="#2-朝九晚五" class="headerlink" title="2.朝九晚五"></a>2.朝九晚五</h2><p>正因为上面所说的，部门对外包的定位就是干一些辅助性、偏前端的工作，所以工作自然也很轻松，再加上弹性打卡，每天其实可以早上10点钟到公司，晚上6点钟准备收拾走人。</p><p>实际工作中，阿里对外包的管理非常松散，夸张的说，只要你能把钉钉打卡搞定，你人在不在办公场地没人管，因为外包是一个集中的办公场地，和正式工不在一起，自然也没人去监督你，这样的工作生活估计是很多IT人羡慕不已的，但确实大多数外包的真实写照。</p><p>大多数时候他们的需求都比较简单、时间也很充裕，每天正常的工作时间完全可以搞定，根本用不到加班，更别说周末了，周末不会有人找你的。。。</p><p>对我来说，这就是外包唯一值得留恋的地方，唯一的好处。</p><h2 id="3-普通工资"><a href="#3-普通工资" class="headerlink" title="3.普通工资"></a>3.普通工资</h2><p>既然工作轻松，那工资自然也不多，大部分外包工资水平都属于很普通的水平，肯定是没法和正式工比，据我了解，低的是上限，下限还行。</p><p>一般下限是15k左右，也就是1年左右工作经验，刚毕业、或者刚出培训班那种，其实这个水平在北京来说并不是很差。 </p><p>再往上也就20k左右，不会太高了，所以说一个4-5年工作经验的外包可能只比1年经验的人多拿5k块钱，经验在这里确实不值钱，毕竟门槛很低。</p><p>说到工资，不得不说一下外包的涨薪制度，一般外包公司都有调薪制度，半年或者1年调一次，但是不会主动给调，得你自己申请。</p><p>外包涨薪一般有2种机制：一种是外包公司直接给你涨，就是外包公司内部调薪。因为阿里给外包公司的工资是一个固定的数，举个例子，比如阿里给外包公司25k，外包公司给你15k，中间商外包公司赚了10k，你提涨工资的话，外包公司就把自己赚的那部分拿一部分给你加薪，相当于它自己少赚点。</p><p>另一个途径就是调级，实际上在入职的时候阿里会给外包定一个级别，比如高级、资深等等，每个级别的工资是在一定范围内的，如果你提调级通过之后，阿里会给你涨工资，这就相当于外包公司不用少赚钱。</p><p>我之前提调薪的时候，外包公司告诉我帮我先提调级，如果不通过再走公司内部调薪，阿里这边的主管和我说调级没问题，但是最后不知道什么情况，外包公司直接给我提了，并没有走调级流程，具体是啥个情况我也不知道。</p><p>其实这个调薪幅度并不高，以我的标准来看，大概也就10%左右，可能还不到。</p><p>再说一点，我们部门的同学有来自不同外包公司的，据我了解不同外包公司的待遇大不相同，比如有的有13薪，有的只有12薪，有的涨薪很难。</p><h2 id="4-一个萝卜一个坑"><a href="#4-一个萝卜一个坑" class="headerlink" title="4.一个萝卜一个坑"></a>4.一个萝卜一个坑</h2><p>我这1年里面，周围走了大概4-5个人，走了之后立马又重新招人，之前和阿里主管吃饭的时候聊过，他们说公司给每个部门的名额是固定的，不招白不招，即使现在没事也得招，不可能有活了才去临时招人。</p><p>所以基本上就是一个萝卜一个坑，有些人可能天天闲着没事，那也正常，照样给你发工资，讲实话，这点得感谢阿里，解决了很多人的就业问题。</p><p>外包的门槛有多低呢，就我个人而言，之前面试进来的时候问的问题真的很简单，2个电话面试就结束了，这导致后面来了很多新同事水平真的一言难尽，我不知道是不是普遍现象，很多工作了1年多的同学连Git都没弄明白，对分支的概念模糊，代码冲突了也不会合并。有些人在编程概念和思想上面也匮乏，导致沟通上面有很大问题。</p><p>可能我对那些新人的要求太高，毕竟有些都是比我小好多岁的年轻人，对于我这样的老鸟来说，这些问题自然都不是问题，理解万岁吧！</p><p>但是我其实想对那些年轻人说，不要这么早进外包，人会废掉！即使自己学历或者其它方面比较差，进不了大公司，进一些创业公司也不错，在这里真的什么也学不到。</p><p>吐槽几句，在我所在部门不乏一些p7级别的，和他们有一些接触交流，人都还不错，但是我看他们写的代码也是一言难尽，看不出来什么水平，可能是我期望太高，也可能是因为并不是什么核心项目的代码，所以写的很随意。</p><h2 id="5-转正的饼"><a href="#5-转正的饼" class="headerlink" title="5.转正的饼"></a>5.转正的饼</h2><p>之前和其它外包同学聊过这件事，我发现很多阿里的主管都给外包画过这样的饼，意思就是说表现好可以转正式员工。</p><p>其实我来阿里干外包之前也有过这个想法，但是干了几个月之后我就明白了，这几乎是不可能的事情，可能在阿里的几千名外包中有那么几个成功过，但是可以这么说，99%都是不可能转正。</p><p>我觉得如果想转正，有以下几种可能：</p><p>第一，自身学历等条件不差，比如说你就是211、985院校毕业的。你整个大专、普通本科在阿里真的没人瞧的上，据我了解，我所在部门那些p7大佬学历都不差，招的实习生也都是名校毕业。</p><p>第二，部门重用你，给你安排了重要性比较高的活，而且你干的不错，体现出了自己的价值。这也不是说不可能，但是在我所在的部门几乎不可能。</p><p>第三，你和你的主管关系好，平时来往多，他愿意提携你一把。这点完全是我臆想的，实际上以我个人经验，我只和我的主管吃过1次饭，平时只是工作沟通，私下完全没有任何交流，我本人也不是那种会打理关系的人。</p><h2 id="6-思考"><a href="#6-思考" class="headerlink" title="6.思考"></a>6.思考</h2><p>虽然工作上没怎么花时间努力，但是作为一个热爱编程的人，编码的脚步不会停下，在此期间，也在努力学习，尝试写一些小玩意、整一些开源项目，比如说拿Go写Gui桌面应用。</p><p>这么多年的工作经历也让我认识到一个事实，大部分工作岗位并不需要高超的技术，大部分人只是螺丝钉，拿着一些现成的库、开源的工具去做一些偏应用的开发工作。只有极少数人在干那些底层、前沿的编程。</p><p>我承认我也是大多数人，对算法、OS等编程底层知识研究不深，但却熟知各种库的调用、熟练使用Google等搜索工具解决问题，简单说就是一个熟练工。</p><p>这一定是坏事吗？不一定，人人都知道那些底层基础知识很重要，但是社会需求并不多，大多数公司只想招能干活的、能解决问题的，谁关心你会不会写二叉树反转算法，不过我确实佩服那些算法写的溜的人。我也尝试去练习算法，每次leetcode刷着刷着就坚持不下去了，更可怕的是刷过的题过几天就忘了。。。</p><p>其实，在我和一些新人打交道的时候，我会发现很多人都存在一个问题：缺乏好奇心和探索精神。比如遇到一个问题，他们不知道怎么解决，这倒也很正常，但是他们连搜索都不知道搜索什么关键字，这倒是最可怕的。有些人用着一个软件都不知道有哪些功能，能做什么事，他们生怕点错了会导致什么坏的结果，所以干脆不去尝试。</p><p>最后，就这样吧！希望我明年不要在干外包了。。。</p>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;之前有写好几篇文章是关于阿里外包的，比如&lt;a href=&quot;https://wangbenjun.site/2020/life/2019-4.html&quot;&gt;《谈一谈阿里外包工作》&lt;/a&gt; 等等，感兴趣的可以翻一下我之前写的。&lt;/p&gt;
&lt;p&gt;不知不觉，在阿里干外包已经干了1年半了，不长也不短，渐渐的感觉自己已经非常适应这样的生活，不太愿意去尝试新的工作，一直也没静下心来思考这些问题，今天刚好决定静下心来写点东西，分享一下这1年多的经历。&lt;/p&gt;
    
    </summary>
    
    
      <category term="Life" scheme="https://wangbenjun.site/categories/Life/"/>
    
    
      <category term="Life" scheme="https://wangbenjun.site/tags/Life/"/>
    
  </entry>
  
  <entry>
    <title>Ubuntu屏幕撕裂问题</title>
    <link href="https://wangbenjun.site/2021/linux/ubuntu-screen-tearing.html"/>
    <id>https://wangbenjun.site/2021/linux/ubuntu-screen-tearing.html</id>
    <published>2021-07-06T13:03:00.000Z</published>
    <updated>2024-12-29T03:02:14.815Z</updated>
    
    <content type="html"><![CDATA[<p>Linux下的显示画面撕裂（Tearing）问题由来已久，如果你观察比较仔细，肯定会发现使用Ubuntu播放视频的时候画面明显有撕裂的现象，这倒不是什么大问题，但是却一直以来存在。</p><p>因为我是使用Ubuntu当主力机，每天不仅拿来编程，也拿来娱乐，看视频电影，浏览网页，对这个现象比较注意，其实这种问题有时候也不单单是Linux存在的问题，很多玩游戏的朋友应该知道很多游戏都有一个垂直同步的选项，这也是为了解决画面撕裂问题。</p><p>这种画面撕裂问题本质上是显卡输出和显示器刷新频率不同步导致的问题，这里记录一下我的解决方式，一个是自己备用，另外分享给需要的朋友。</p><a id="more"></a><p>在Ubuntu系统下，分2种情况下：</p><p>如果你是独显，比如Nvidia显卡，可以在英伟达控制面板里面勾选垂直同步选项，实际上这个效果并不明显，其实我也不知道为什么。</p><p>后来，我找到了一个非常有效的方式，打开 <strong>&#x2F;etc&#x2F;X11&#x2F;xorg.conf</strong> 文件，这个文件在老版本的Ubuntu里面是自动生成的，新的版可能并没有，但是我们可以手动生成，通过下面的方式：</p><p>按下快捷键 Ctrl+Alt+F1 进入终端模式</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">sudo service lightdm stop &#x2F;&#x2F;关闭图形界面</span><br><span class="line">sudo Xorg -configure &#x2F;&#x2F;生成配置，默认位置是 ~&#x2F;xorg.conf.new</span><br><span class="line">sudo service lightdm start</span><br><span class="line">mv ~&#x2F;xorg.conf.new &#x2F;etc&#x2F;X11&#x2F;xorg.conf &#x2F;&#x2F;把生成的配置复制到对应位置</span><br></pre></td></tr></table></figure><p>整个配置文件内容比较多，具体啥意思我也没去细究，但是其中有一块我们需要改，内容大概如下：</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br></pre></td><td class="code"><pre><span class="line">Section &quot;Device&quot;</span><br><span class="line">        #Option     &quot;ColorKey&quot;           # &lt;i&gt;</span><br><span class="line">        #Option     &quot;VideoKey&quot;           # &lt;i&gt;</span><br><span class="line">        #Option     &quot;Tiling&quot;             # [&lt;bool&gt;]</span><br><span class="line">        #Option     &quot;LinearFramebuffer&quot;  # [&lt;bool&gt;]</span><br><span class="line">        #Option     &quot;HWRotation&quot;         # [&lt;bool&gt;]</span><br><span class="line">        Option     &quot;VSync&quot;              &quot;True&quot;</span><br><span class="line">        #Option     &quot;PageFlip&quot;           # [&lt;bool&gt;]</span><br><span class="line">        #Option     &quot;SwapbuffersWait&quot;    # [&lt;bool&gt;]</span><br><span class="line">        #Option     &quot;TripleBuffer&quot;       # [&lt;bool&gt;]</span><br><span class="line">        #Option     &quot;XvPreferOverlay&quot;    # [&lt;bool&gt;]</span><br><span class="line">        #Option     &quot;HotPlug&quot;            # [&lt;bool&gt;]</span><br><span class="line">        #Option     &quot;ReprobeOutputs&quot;     # [&lt;bool&gt;]</span><br><span class="line">        #Option     &quot;XvMC&quot;               # [&lt;bool&gt;]</span><br><span class="line">        #Option     &quot;ZaphodHeads&quot;        # &lt;str&gt;</span><br><span class="line">        #Option     &quot;VirtualHeads&quot;       # &lt;i&gt;</span><br><span class="line">        Option     &quot;TearFree&quot;           &quot;True&quot;</span><br><span class="line">        #Option     &quot;PerCrtcPixmaps&quot;     # [&lt;bool&gt;]</span><br><span class="line">        #Option     &quot;FallbackDebug&quot;      # [&lt;bool&gt;]</span><br><span class="line">        #Option     &quot;DebugFlushBatches&quot;  # [&lt;bool&gt;]</span><br><span class="line">        #Option     &quot;DebugFlushCaches&quot;   # [&lt;bool&gt;]</span><br><span class="line">        #Option     &quot;DebugWait&quot;          # [&lt;bool&gt;]</span><br><span class="line">        #Option     &quot;BufferCache&quot;        # [&lt;bool&gt;]</span><br><span class="line">    Identifier  &quot;Card0&quot;</span><br><span class="line">    Driver      &quot;intel&quot;</span><br><span class="line">    BusID       &quot;PCI:0:2:0&quot;</span><br><span class="line">EndSection</span><br></pre></td></tr></table></figure><p>这里只截取部分片段，其中关键就是Option字段的配置，可以看到这里其实有很多选项，默认情况下都是注释掉的，但有2个选项我打开了，分别是VSync、TearFree，设置为True即可。</p><p>然后重启就会发现画面撕裂问题解决了，至少在我的CPU 8700k的集显下，带动4k显示器没问题，非常流畅，但是为什么这个配置默认被注释掉也是很奇怪，必然有原因的，我Google了一下这个问题，发现有文章提到打开会使用更多的内存、增加响应延迟，没毛病和我想的一样，在Windows下很多游戏都有“垂直同步”这个设置选项，打开的话会锁60帧，显卡负载会低一点，但是显示响应会慢一点，虽然可能是毫秒级别，但对于很多fps游戏很致命。</p><p>总之，在我电脑上我感觉没啥问题，如果你经常看视频，对画面撕裂问题接受不了，不妨设置一下试试。</p>]]></content>
    
    <summary type="html">
    
      &lt;p&gt;Linux下的显示画面撕裂（Tearing）问题由来已久，如果你观察比较仔细，肯定会发现使用Ubuntu播放视频的时候画面明显有撕裂的现象，这倒不是什么大问题，但是却一直以来存在。&lt;/p&gt;
&lt;p&gt;因为我是使用Ubuntu当主力机，每天不仅拿来编程，也拿来娱乐，看视频电影，浏览网页，对这个现象比较注意，其实这种问题有时候也不单单是Linux存在的问题，很多玩游戏的朋友应该知道很多游戏都有一个垂直同步的选项，这也是为了解决画面撕裂问题。&lt;/p&gt;
&lt;p&gt;这种画面撕裂问题本质上是显卡输出和显示器刷新频率不同步导致的问题，这里记录一下我的解决方式，一个是自己备用，另外分享给需要的朋友。&lt;/p&gt;
    
    </summary>
    
    
      <category term="Linux" scheme="https://wangbenjun.site/categories/Linux/"/>
    
    
      <category term="Ubuntu" scheme="https://wangbenjun.site/tags/Ubuntu/"/>
    
  </entry>
  
  <entry>
    <title>Golang算法之买卖股票的最佳时机II</title>
    <link href="https://wangbenjun.site/2021/leetcode/8.best-time-to-buy-and-sell-t.html"/>
    <id>https://wangbenjun.site/2021/leetcode/8.best-time-to-buy-and-sell-t.html</id>
    <published>2021-07-04T13:36:00.000Z</published>
    <updated>2024-12-29T03:02:14.812Z</updated>
    
    <content type="html"><![CDATA[<blockquote><p>买卖股票的最佳时机II，简单难度，leetcode地址：<a href="https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-ii" target="_blank" rel="noopener">https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-ii</a></p></blockquote><h2 id="1-描述"><a href="#1-描述" class="headerlink" title="1.描述"></a>1.描述</h2><p>给定一个数组 prices ，它的第i个元素 prices[i] 表示一支给定股票第 i 天的价格。</p><p>设计一个算法来计算你所能获取的最大利润。你可以尽可能地完成更多的交易（多次买卖一支股票）。</p><p>示例 1：</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">输入: prices &#x3D; [7,1,5,3,6,4]</span><br><span class="line">输出: 7</span><br><span class="line">解释: 在第 2 天（股票价格 &#x3D; 1）的时候买入，在第 3 天（股票价格 &#x3D; 5）的时候卖出, 这笔交易所能获得利润 &#x3D; 5-1 &#x3D; 4 。</span><br><span class="line">随后，在第 4 天（股票价格 &#x3D; 3）的时候买入，在第 5 天（股票价格 &#x3D; 6）的时候卖出, 这笔交易所能获得利润 &#x3D; 6-3 &#x3D; 3 。</span><br></pre></td></tr></table></figure><a id="more"></a><p>示例 2:</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">输入: prices &#x3D; [1,2,3,4,5]</span><br><span class="line">输出: 4</span><br><span class="line">解释: 在第 1 天（股票价格 &#x3D; 1）的时候买入，在第 5 天 （股票价格 &#x3D; 5）的时候卖出, 这笔交易所能获得利润 &#x3D; 5-1 &#x3D; 4 。</span><br><span class="line">注意你不能在第 1 天和第 2 天接连购买股票，之后再将它们卖出。因为这样属于同时参与了多笔交易，你必须在再次购买前出售掉之前的股票。</span><br></pre></td></tr></table></figure><h2 id="2-题解"><a href="#2-题解" class="headerlink" title="2.题解"></a>2.题解</h2><p>这种题基本上都可以采用暴力循环遍历法解决，这里就不多说了。除此之外，还可以采用动态规划的方法，比较复杂，但是这里介绍的是一个贪心算法。</p><p>这种解法的思想就是可能多次交易，只要今天价格比昨天高就卖出股票，然后把所有获利加在一起就是最大利润。</p><p>这里以[7,1,5,3,6,4]为例，推算步骤如下：</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">1 &gt; 7 ? 不成立，利润 0</span><br><span class="line">5 &gt; 1 ? 已成立，利润 4</span><br><span class="line">3 &gt; 5 ? 不成立，利润 0</span><br><span class="line">6 &gt; 3 ? 已成立，利润 3</span><br><span class="line">4 &gt; 6 ? 不成立，利润 0</span><br></pre></td></tr></table></figure><p>最后把所有例如相加，即是最大利润，但是其操作的过程并不是最优解，而且实际上过程中也没法这么操作，以为这些操作过程中并没有把亏损算进去。</p><p>这种算法虽然结果是正确的，但是过程却很奇妙，代码逻辑也不复杂。</p><p>参考代码：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">maxProfit</span><span class="params">(prices []<span class="keyword">int</span>)</span> <span class="title">int</span></span> &#123;</span><br><span class="line">    maxProfit := <span class="number">0</span></span><br><span class="line">    <span class="keyword">for</span> i := <span class="number">1</span>; i &lt; <span class="built_in">len</span>(prices); i++ &#123;</span><br><span class="line">        <span class="keyword">if</span> prices[i] &gt; prices[i<span class="number">-1</span>] &#123;</span><br><span class="line">            maxProfit += prices[i] - prices[i<span class="number">-1</span>]</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> maxProfit</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>]]></content>
    
    <summary type="html">
    
      &lt;blockquote&gt;
&lt;p&gt;买卖股票的最佳时机II，简单难度，leetcode地址：&lt;a href=&quot;https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-ii&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock-ii&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;1-描述&quot;&gt;&lt;a href=&quot;#1-描述&quot; class=&quot;headerlink&quot; title=&quot;1.描述&quot;&gt;&lt;/a&gt;1.描述&lt;/h2&gt;&lt;p&gt;给定一个数组 prices ，它的第i个元素 prices[i] 表示一支给定股票第 i 天的价格。&lt;/p&gt;
&lt;p&gt;设计一个算法来计算你所能获取的最大利润。你可以尽可能地完成更多的交易（多次买卖一支股票）。&lt;/p&gt;
&lt;p&gt;示例 1：&lt;/p&gt;
&lt;figure class=&quot;highlight plain&quot;&gt;&lt;table&gt;&lt;tr&gt;&lt;td class=&quot;gutter&quot;&gt;&lt;pre&gt;&lt;span class=&quot;line&quot;&gt;1&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;2&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;3&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;4&lt;/span&gt;&lt;br&gt;&lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;code&quot;&gt;&lt;pre&gt;&lt;span class=&quot;line&quot;&gt;输入: prices &amp;#x3D; [7,1,5,3,6,4]&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;输出: 7&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;解释: 在第 2 天（股票价格 &amp;#x3D; 1）的时候买入，在第 3 天（股票价格 &amp;#x3D; 5）的时候卖出, 这笔交易所能获得利润 &amp;#x3D; 5-1 &amp;#x3D; 4 。&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;随后，在第 4 天（股票价格 &amp;#x3D; 3）的时候买入，在第 5 天（股票价格 &amp;#x3D; 6）的时候卖出, 这笔交易所能获得利润 &amp;#x3D; 6-3 &amp;#x3D; 3 。&lt;/span&gt;&lt;br&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/figure&gt;
    
    </summary>
    
    
      <category term="LeetCode" scheme="https://wangbenjun.site/categories/LeetCode/"/>
    
    
      <category term="算法" scheme="https://wangbenjun.site/tags/%E7%AE%97%E6%B3%95/"/>
    
  </entry>
  
  <entry>
    <title>Golang算法之买卖股票的最佳时机</title>
    <link href="https://wangbenjun.site/2021/leetcode/7.best-time-to-buy-and-sell.html"/>
    <id>https://wangbenjun.site/2021/leetcode/7.best-time-to-buy-and-sell.html</id>
    <published>2021-07-01T14:03:00.000Z</published>
    <updated>2024-12-29T03:02:14.812Z</updated>
    
    <content type="html"><![CDATA[<blockquote><p>买卖股票的最佳时机，简单难度，leetcode地址：<a href="https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock" target="_blank" rel="noopener">https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock</a></p></blockquote><h2 id="1-描述"><a href="#1-描述" class="headerlink" title="1.描述"></a>1.描述</h2><p>给定一个数组 prices ，它的第i个元素 prices[i] 表示一支给定股票第 i 天的价格。</p><p>你只能选择 某一天 买入这只股票，并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。</p><p>返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润，返回 0 。</p><p>示例 1：</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">输入：[7,1,5,3,6,4]</span><br><span class="line">输出：5</span><br><span class="line">解释：在第 2 天（股票价格 &#x3D; 1）的时候买入，在第 5 天（股票价格 &#x3D; 6）的时候卖出，最大利润 &#x3D; 6-1 &#x3D; 5 。</span><br><span class="line">注意利润不能是 7-1 &#x3D; 6, 因为卖出价格需要大于买入价格；同时，你不能在买入前卖出股票。</span><br></pre></td></tr></table></figure><a id="more"></a><p>示例 2:</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">输入：prices &#x3D; [7,6,4,3,1]</span><br><span class="line">输出：0</span><br><span class="line">解释：在这种情况下, 没有交易完成, 所以最大利润为 0。</span><br></pre></td></tr></table></figure><h2 id="2-题解"><a href="#2-题解" class="headerlink" title="2.题解"></a>2.题解</h2><h3 id="方法一-暴力循环"><a href="#方法一-暴力循环" class="headerlink" title="方法一: 暴力循环"></a>方法一: 暴力循环</h3><p>leetcode上面很多题都可以使用暴力循环这种方式解决，毕竟没有什么是循环遍历解决不了的，只是效率问题。</p><p>这道题也不例外，我们只需2层循环遍历，依次计算不同买卖天之间的利润，求最大值就行，代码逻辑朴实无华！</p><p>参考代码:</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">maxProfit</span><span class="params">(prices []<span class="keyword">int</span>)</span> <span class="title">int</span></span> &#123;</span><br><span class="line">    max := <span class="number">0</span></span><br><span class="line">    <span class="keyword">for</span> i := <span class="number">0</span>; i &lt; <span class="built_in">len</span>(prices); i++ &#123;</span><br><span class="line">        <span class="keyword">for</span> j := i + <span class="number">1</span>; j &lt; <span class="built_in">len</span>(prices); j++ &#123;</span><br><span class="line">            <span class="keyword">if</span> p := prices[j] - prices[i]; p &gt; max &#123;</span><br><span class="line">                max = p</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> max</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>然而，这段代码在leetcode里面提交运行超时，因为测试用例里面有一个超长的数组，服了！</p><h3 id="方法二-动态规划"><a href="#方法二-动态规划" class="headerlink" title="方法二: 动态规划"></a>方法二: 动态规划</h3><p>要看明白这个解法，得换个思路，假设我们知道哪天价格最低，我们就可以在那天买，然后在价格最高那天卖出。</p><p>假如计划在第 i 天卖出股票，那么最大利润的差值一定是在[0, i-1] 之间选最低点买入；所以遍历数组，依次求每个卖出时机的的最大差值，再从中取最大值。</p><p>这个思路结合下面这段代码看，比较有效果：</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">maxProfit</span><span class="params">(prices []<span class="keyword">int</span>)</span> <span class="title">int</span></span> &#123;</span><br><span class="line">    minPrice := prices[<span class="number">0</span>]</span><br><span class="line">    maxProfit := <span class="number">0</span></span><br><span class="line">    <span class="keyword">for</span> i := <span class="number">0</span>; i &lt; <span class="built_in">len</span>(prices); i++ &#123;</span><br><span class="line">        <span class="keyword">if</span> prices[i] &lt; minPrice &#123;</span><br><span class="line">            minPrice = prices[i]</span><br><span class="line">        &#125;<span class="keyword">else</span> <span class="keyword">if</span> prices[i] - minPrice &gt; maxProfit &#123;</span><br><span class="line">            maxProfit = prices[i] - minPrice</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> maxProfit</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>以[7,1,5,3,6,4]为例，我们手动推导一次：</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">minPrice &#x3D; 7， maxProfit &#x3D; 0</span><br><span class="line">i &#x3D; 0, 7 &lt; 7? 不成立，7-7 &gt; 0?  不成立, minPrice &#x3D; 7， maxProfit &#x3D; 0</span><br><span class="line">i &#x3D; 1, 1 &lt; 7? 已成立，                 minPrice &#x3D; 1， maxProfit &#x3D; 0</span><br><span class="line">i &#x3D; 2, 5 &lt; 1? 不成立，5-1 &gt; 0?  已成立, minPrice &#x3D; 1， maxProfit &#x3D; 4</span><br><span class="line">i &#x3D; 3, 3 &lt; 1? 不成立，3-1 &gt; 4?  不成立, minPrice &#x3D; 1， maxProfit &#x3D; 4</span><br><span class="line">i &#x3D; 4, 6 &lt; 1? 不成立，6-1 &gt; 4?  已成立, minPrice &#x3D; 1， maxProfit &#x3D; 5</span><br><span class="line">i &#x3D; 5, 4 &lt; 1? 不成立，4-1 &gt; 4?  不成立, minPrice &#x3D; 1， maxProfit &#x3D; 5</span><br></pre></td></tr></table></figure><p>首先，我们定义了最低价格和最大利润2个变量，其中最低价格默认为第一个元素。 然后，依次遍历数组，把每个元素的值和最低价格做比较，有3种结果：</p><p>一、如果元素值小于最低价格，那么就更新最低价格</p><p>二、如果元素值等于最低价格，那么就不做任何操作</p><p>三、如果元素值大于最低价格，说明此时有利可图，我们就算一下利润是多少，如果利润比之前利润大，那么就更新利润</p><p>在整个计算过程中，我们动态的更新了最低价格和最大利润，最终得到的结果就是最大利润。</p>]]></content>
    
    <summary type="html">
    
      &lt;blockquote&gt;
&lt;p&gt;买卖股票的最佳时机，简单难度，leetcode地址：&lt;a href=&quot;https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock&quot; target=&quot;_blank&quot; rel=&quot;noopener&quot;&gt;https://leetcode-cn.com/problems/best-time-to-buy-and-sell-stock&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;1-描述&quot;&gt;&lt;a href=&quot;#1-描述&quot; class=&quot;headerlink&quot; title=&quot;1.描述&quot;&gt;&lt;/a&gt;1.描述&lt;/h2&gt;&lt;p&gt;给定一个数组 prices ，它的第i个元素 prices[i] 表示一支给定股票第 i 天的价格。&lt;/p&gt;
&lt;p&gt;你只能选择 某一天 买入这只股票，并选择在 未来的某一个不同的日子 卖出该股票。设计一个算法来计算你所能获取的最大利润。&lt;/p&gt;
&lt;p&gt;返回你可以从这笔交易中获取的最大利润。如果你不能获取任何利润，返回 0 。&lt;/p&gt;
&lt;p&gt;示例 1：&lt;/p&gt;
&lt;figure class=&quot;highlight plain&quot;&gt;&lt;table&gt;&lt;tr&gt;&lt;td class=&quot;gutter&quot;&gt;&lt;pre&gt;&lt;span class=&quot;line&quot;&gt;1&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;2&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;3&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;4&lt;/span&gt;&lt;br&gt;&lt;/pre&gt;&lt;/td&gt;&lt;td class=&quot;code&quot;&gt;&lt;pre&gt;&lt;span class=&quot;line&quot;&gt;输入：[7,1,5,3,6,4]&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;输出：5&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;解释：在第 2 天（股票价格 &amp;#x3D; 1）的时候买入，在第 5 天（股票价格 &amp;#x3D; 6）的时候卖出，最大利润 &amp;#x3D; 6-1 &amp;#x3D; 5 。&lt;/span&gt;&lt;br&gt;&lt;span class=&quot;line&quot;&gt;注意利润不能是 7-1 &amp;#x3D; 6, 因为卖出价格需要大于买入价格；同时，你不能在买入前卖出股票。&lt;/span&gt;&lt;br&gt;&lt;/pre&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/figure&gt;
    
    </summary>
    
    
      <category term="LeetCode" scheme="https://wangbenjun.site/categories/LeetCode/"/>
    
    
      <category term="算法" scheme="https://wangbenjun.site/tags/%E7%AE%97%E6%B3%95/"/>
    
  </entry>
  
</feed>
