Create an output folder
# mkdir -p /output/
Create rootfs.tar
# cd /# tar cf /output/rootfs.tar --exclude=/proc --exclude=/dev --exclude=/boot --exclude=/run --exclude=/sys --exclude=/output /
Compress tar file with xz
# xz -v -T 0 /output/rootfs.tar
Build docker image from scratch with rootfs.tar.xz
FROM scratchADD ./rootfs.tar.xz /
]]>Bash functions are a way to group a set of commands and logic into a reusable block in a Bash script. They allow you to encapsulate common tasks or complex sequences of commands into a single callable entity. Here’s an example of how to define and use a Bash function:
function hello() { ...}
or
hello() { ...}
Note: bash function will only return the return code of the last command that run in function.
For example: the hello
function will not catch the return code of command1
hello() { command1 command2}
If you care about the execution results of all instructions throughout the entire function process, you need to use the “&&” symbol to chain all the commands together. Such as:
hello() { command1 && \ command2}
set -e
is a common command used in Bash scripts. It sets an error handling flag that causes the script to exit immediately if any command within the script fails.
#!/bin/bashset -e# Some commands here...command1command2#...# If any command fails, the script will exit here
realpath
is a command in bash that is used to get the absolute path of a file or directory.
dirname
is a built-in command in Bash scripts that returns the directory name part of a given path or the directory path of a given file path.
BASH_SOURCE
is an internal variable in the Bash shell. It holds the path of the script or function that is currently being executed.
You can get absolute path of the bash script file you are running.
#!/usr/bin/env bashset -ebash_script="$(realpath "${BASH_SOURCE[0]}")"
And set the dirctory where bash script in as working directory.
#!/usr/bin/env bashset -eworkdir="$(realpath "$(dirname "$(realpath "${BASH_SOURCE[0]}")")")"cd "${workdir}"
In Bash, you can use the read
command to prompt for confirm input from the user. Here’s an example of how to write a confirm input in Bash:
read -p "Are you sure [y/n]? " -r nameif [[ ! $name =~ ^[Yy]$ ]]then echo "==> Aborted" exit 0fi
The naive way to check if a variable is empty.
if [ "${variable}" == "" ]; then ...fi
The tidy code could be like:
a="${variable:?}"
]]>Run the following code in Dashboard -> Manage Jenkins -> Tools and Actions -> Script Console
import com.cloudbees.plugins.credentials.Credentialsimport com.cloudbees.plugins.credentials.CredentialsProviderCredentialsProvider.lookupCredentials(Credentials.class, Jenkins.instance, null, null).each { c -> def className = c.properties.class.toString() def p = c.properties println("ID: ${c.id}") println("Scope: ${c.scope}") println("Description: ${c.description}") println("Type: ${className.substring(className.lastIndexOf('.') + 1)}") switch (c.properties.class.toString()) { case 'class com.cloudbees.plugins.credentials.impl.UsernamePasswordCredentialsImpl': println("Username: ${p.username}") println("Password: ${p.password}") break case 'class com.cloudbees.jenkins.plugins.sshcredentials.impl.BasicSSHUserPrivateKey': println("Username: ${p.username}") println("Private Key: |") println("${p.privateKey}") break case 'class org.jenkinsci.plugins.plaincredentials.impl.FileCredentialsImpl': println("Filename: ${p.fileName}") println("File Content: |") println("${new String(c.getSecretBytes().getPlainData())}") break case 'class org.jenkinsci.plugins.plaincredentials.impl.StringCredentialsImpl': println("Secret: ${p.secret}") break default: println("WARN: unknown credential type") println("credential properties: ${p.toString()}") } println("------------------------------------------------------------------------------")}
Or use the more tidy code
com.cloudbees.plugins.credentials.SystemCredentialsProvider.getInstance().getCredentials().forEach{ it.properties.each { prop, val -> println(prop + " = " + val) } println("----------------------------------------------------")}
]]>Here are some things you need to prepare in advance before using them formally.
“ide.await.scope.completion” to be a registry option that can be toggled to alter the behavior of JetBrains IDEs during shutdown. Sometimes when you close the IDE, it gets stuck at the ‘Closing Project’ dialog.
Here’s a breakdown of the steps outlined in the workaround:
$ curl -L https://mirrors.ustc.edu.cn/docker-ce/linux/centos/docker-ce.repo -o /etc/yum.repos.d/docker-ce.repo$ sed -i 's+https://download.docker.com+https://mirrors.ustc.edu.cn/docker-ce+' /etc/yum.repos.d/docker-ce.repo$ sed -i 's+$releasever+8+' /etc/yum.repos.d/docker-ce.repo
$ yum install -y docker-ce
$ systemctl enable docker$ systemctl start docker
$ systemctl status docker
The result should show like below. Active status should be running
.
[root@localhost ~]# systemctl status docker● docker.service - Docker Application Container Engine Loaded: loaded (/usr/lib/systemd/system/docker.service; enabled; vendor preset: disabled) Active: active (running) since Sat 2024-01-27 16:52:24 CST; 10s ago Docs: https://docs.docker.com Main PID: 12132 (dockerd) Tasks: 7 Memory: 32.7M CGroup: /system.slice/docker.service └─12132 /usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock
]]>However, I encountered a minor issue while using them: I prefer to edit certain files using the system-associated editor rather than the built-in Markdown editor in JetBrains IDE. For instance, Markdown files.
When you want to use a system-associated editor instead of the built-in editor in JetBrains IDE for certain files, you can follow these steps:
After completing these steps, the system will use the associated editor to open the specified file types instead of the built-in editor in JetBrains IDE.
]]>Aside from its aesthetics, Deepin also provides a range of built-in applications, including a file manager, music player, video player, and more, all designed to integrate seamlessly with the desktop environment. It’s known for its simplicity and elegance, making it a popular choice for users who prefer a polished and modern Linux experience.
Deepin Linux also includes its own software center, which provides users with access to a wide range of applications for installation and updates. It’s available in multiple languages and is used by people around the world who appreciate its user-friendly approach and attractive design.
After system startup, it will consume 1.2GB of memory by default.
root@deepin:~# free -mh total used free shared buff/cache availableMem: 7.7Gi 1.2Gi 4.7Gi 22Mi 1.9Gi 6.3GiSwap: 0B 0B 0B
systemctl disable lightdmsystemctl disable dde-filemanager-daemonsystemctl disable deepin-accounts-daemonsystemctl disable deepin-devicemanager-server
After disabling some unnecessary services using the above methods, the system now only consumes 347MB of memory.
root@server:~# free -mh total used free shared buff/cache availableMem: 7.7Gi 347Mi 7.2Gi 2.0Mi 183Mi 7.1GiSwap: 0B 0B 0B
]]>In the Linux desktop environment, Terminator is a highly versatile terminal tool that allows for multiple horizontal and vertical splits of the current page. However, there is an issue where the divider between splits is transparent, which can sometimes make it difficult to distinguish between the split blocks.
If you want to use a solid color as the background for the split line, you can use the following method:
Edit Terminator source code file /usr/share/terminator/terminatorlib/terminator.py
. Please note that modifying this file requires root
permissions.
Search background_type
, this should be in about line 495。
if profiles[profile]['background_type'] == 'transparent': bgalpha = profiles[profile]['background_darkness']
Replace these two lines with:
if profiles[profile]['background_type'] == 'image': backgound_image = profiles[profile]['background_image'] if profiles[profile]['background_type'] == 'transparent' or profiles[profile]['background_type'] == 'image': bgalpha = profiles[profile]['background_darkness']
Add a ~/.config/gtk-3.0/gtk.css
file in the user’s home directory with the following content:
.terminator-terminal-window separator { background: #303030;}
]]>“如果调试是清除软件缺陷的过程,那么编程一定是把它们放进去的行为”。—— Edsger Djikstra
编写自动化软件测试就像和自己玩电话游戏–当你误解了信息是什么时,你是唯一可以责备的人。如果你为自己的代码写测试,这已经够难的了,但是考虑到你为别人写的代码写测试,而这些代码本来就没有被测试。现在,这就像试图理解一张在蓝色牛仔裤口袋里洗过的纸上的信息是什么……三次!
这是在被测试的代码已经写完之后才写的测试。现在考虑一下先写测试的做法–这就像和自己玩高手过招,先写一些看似合理的规范或测试,以确保你要写的解决方案会 “做正确的事”。但是,如果你学过计算机科学,那么这听起来非常像解决停止问题–只是更糟糕,因为现在你不仅需要向自己证明,还需要向编译器/解释器证明你希望它做的事情是正确的。
那么,为什么在过去的20年里,测试一直是现代软件工程实践中不可或缺的一部分–无论是测试优先还是测试最后,我们专业的软件工程师仍然需要考虑如何测试和验证软件以满足需求。
又到了故事时间。
上次在第一部分中,我写到我是如何艰难地学会了如何设计系统以适应现代的扩展性、可靠性、可用性、可维护性和安全性要求。设计一个解决方案只能到此为止,因为到了最后,这个解决方案需要被实施–有时,它必须由一个团队或多个团队来完成。
你可以想象,跨团队的协调工作将是问题的主要来源,但我们是否可以做些什么来减少这种负担?自动测试来了–特别是那种指定行为而不是测试实现的测试。
当我在Friendster工作的时候,我可以知道我所从事的服务的客户端到底期望什么。然而,这并不是完全明确的–我们有一个我们所遵循的协议(这是在协议缓冲区流行之前)和一些被这些客户端调用的URI。语义并没有完全阐明,但我们有一个忠实的听众–我可以阅读客户端的代码,并从当前的实现中找出期望。
这一点很重要–我们不是创建一个全新的协议或创建一个新的合同,而是从已知的需求开始,我们可以把这些需求写成自动测试。我首先要做的几件事之一是把这些测试变成我可以编程的规范,并逐步把实现带到满足要求的地步。这项工作产生了两个产品:
这两个开源解决方案都是内部定义的技术要求的结果。我们从一个现有的系统开始,将其分解成各个组成部分,然后逐步实施这些解决方案,直到我们可以将非业务关键部分作为开源软件共享。
你可能会问,为什么我需要从测试开始?因为测试允许我以一种渐进和可预测的方式来填充解决方案以满足需求。有了测试,我和审查我所写的代码的人就能理解需求是什么,并通过运行测试自动验证它们。这使我们能够逐步获得我们所需要的信心,我们得到了一个满足我们需求的解决方案。
有了测试,我就可以专注于什么是必要的和足够的功能,同时让我有信心重构和改进解决方案,并快速验证我是否破坏了编码需求的测试。通过让测试涵盖需求,我已经能够捕捉到许多错误,并快速地交付功能,在此过程中无畏地进行重构。
那是在2007-2008年左右,很多这些概念(如测试驱动开发和行为驱动开发)刚刚开始流行,但通常是在企业软件行业。在这里,我把其中一些好的想法应用到了微服务和水平可扩展的系统中!
快进了几年,我们现在到了2023年,测试在某些圈子里已经变成了一个肮脏的词(TDD和BDD往往烧毁了很多人,主要是由于对原则的误解),并且已经成为一种事后的想法,我们要求我们的副驾驶为我们写的代码做单元测试。这有点可惜,因为采用正确测试的高性能软件工程团队在适应不断变化的需求和改进解决方案的实施方面的自由度是非常有价值的,那些没有及早投资的团队往往会在很晚的时候才意识到,测试本来可以拯救他们的重大故障,由于错误爬到生产中而导致的不眠之夜,或者由于解决方案质量差和速度低而失去业务。
在这篇文章中,我将写更多关于测试在现代软件工程中的作用,以及如何正确地进行测试使你和你的团队在行业中取得成功。
在我们进一步讨论之前,最好先了解测试的不同级别或类别。如果你以前没有写过测试,知道有一个相当健全的测试术语分类法可能会有好处,这样你至少可以跟上围绕它们发生的讨论。
在某些情况下,你可能会遇到需要有人工或人类驱动的测试来覆盖一些不可预测的或组合巨大的可能性空间(考虑计算机游戏,人工智能模型,控制系统等)。这些在软件工程行业中仍然有很好的地位,但在这篇文章中,我将专注于自动化测试案例。
现在我们有了一些定义,让我们深入了解一些现代软件工程的测试方法以及它是如何改变我们解决问题的方式的。
测试驱动开发或TDD是一种实现软件的方法论,首先将测试(或规范)写成可执行代码,看到测试失败(先是红色),实现一个解决方案以满足需求,看到测试成功(绿色),重构解决方案的可读性和灵活性,同时保持测试成功运行(保持绿色),并进行迭代。下面是对这个方法论中每个步骤的更多解释:
请注意,即使你已经有了一个没有测试的代码库,你也可以开始遵循TDD。你可以自上而下(从UATs到单元测试)或自下而上(从单元测试到UATs),并在此过程中开始重构你的接口,让你觉得更有信心代表逻辑组件或领域模型。
从一开始就遵循TDD有很多好处:
这也很好,但我们也需要承认TDD的成本和一些缺点。
TDD并不是我们在这个世界上遇到的所有软件质量问题的万能药。然而,它是一种实践,可以帮助保持重要的焦点,以便我们可以有信心地设计满足需求的系统。
如果你已经遵循TDD,那很好。但如果你不这样做,你有可以在以下情况下自动运行的测试仍然很重要:
自动化测试正在成为提供具有竞争力和更高质量的软件系统的关键工具,特别是在我们今天看到的现代软件工程实践中。鉴于我们正在构建和部署的系统的复杂性和关键性,很难看到我们如何能够在没有自动化测试的情况下向前管理。
假设你已经实现了自动化测试,你有UATs、系统测试、集成测试和单元测试,你可以运行。你也有一个回归测试套件和非功能需求表达为自动化测试。你如何将你的测试实践带到现代软件工程时代?
特别是对于那些在云中作为分布式系统开发和部署的软件,在像Kubernetes这样的环境中协调,控制平面分别管理工作负载和资源的放置和管理,公共云供应商为网络存在和地理多样性提供管理资源,应用程序的架构变得越来越复杂。测试这些应用程序变得非常困难和昂贵。
以下是管理这种复杂性需要考虑的几件事,并确保你能跟上现代大规模全球可用服务的需求:
随着系统变得越来越复杂,因为它们是分布式的,处理的规模也越来越大,自动化测试对于确保我们正在开发的各种交互式系统的质量和正确性只会更加重要。
编写和维护有效的自动化测试,代表软件系统的关键要求,正成为今天软件工程专业人员的追求和重要技能。拥有测试专家的日子已经一去不复返了,就像现在每个人都是开发人员和运营工程师一样。现代软件工程要求每个软件工程从业者都知道并理解自动化测试的价值,它如何影响我们交付给客户的软件系统的稳健性、质量和有效性。
在一天结束时,软件工程是关于建立正确的东西来解决正确的问题。知道解决这个问题的要求是什么,是能够有效解决这个问题的关键。
]]>那时,软件开发主要是指在你的机器上本地运行的软件,或者,如果你有机会的话,在时间共享的计算机上运行的软件,其处理能力明显高于你,可以做商业相关的事情。我记得,在大学里听说过一个计算机科学家使用的程序,它需要一个多核处理器来生成数千个学生的课程表;生成和打印课程表需要数周时间。直到今天,我仍然不确定程序运行时间还是打印到纸上时间哪个更长。
今天,大多数正在开发的软件要么在云上运行,要么在需要访问云的设备上运行,要么为同样在云上运行的其他软件提供动力。在密闭空间内工作的软件系统(如嵌入式软件系统),如果不能在其他地方获得更强大的计算平台,那是非常罕见的。会计系统现在压缩了大量的数据,这些数据被托管在公司内部或外部的数据仓库的服务器群中。销售系统现在由第三方管理客户关系,并由更多的第三方或内部开发人员开发插件。
但是,今天这些软件系统是如何建立的,以服务数百到数百万的用户,同时还能保持我们对今天使用的软件所期望的性能和响应性?
作为一个有20年之久的软件工程师,我见过许多系统从堆栈的每个层面被开发出来。从DOS时代的中断处理程序到JavaScript驱动的动画,甚至是无代码的报告生成。几周前,我甚至让ChatGPT-4根据我给它的一些描述来生成一些我想要的Python代码!但这是另一个故事!但这是另一个故事了。
在这篇文章中,我写的是系统设计,它如何成为现代软件工程实践的一个关键部分,以及它将是人类软件工程师在中短期内仍能提供价值的关键领域之一。
很久以前,我是一家公司的软件工程师,这家公司在处理他们自己带来的成功的负荷方面存在问题。我把这家公司称为Friendster。当我加入这家公司时,我所负责的项目已经很晚了,而且有许多与内存管理有关的错误。他们的核心服务(是的,在2007年我们这样称呼它之前,它是一个微服务)是用C++编写的,但有内存泄漏,处理请求的时间太长,而且被设计为在自己的内存中缓存和提供数据。它需要是无状态的,但最后却变成了有状态的。
在项目进行了几周后,我恳求高级工程领导层放弃这个服务的迭代,而是从头开始写一些符合要求的东西;这将是对现有实施的一个直接替换。我们有一个最后期限,因为该服务只能再处理几个月的增长,然后它就不能再以重新水化的方式处理缓存的大小。
重新启动服务所花的时间比它能保持的时间还要长,直到内存泄漏使它瘫痪。这是一个 “赌上我的职业生涯 “的时刻,但我几乎没有这个时间。我们必须让它运转起来。
系统设计开始了。我们做的第一件事是列出系统必须满足的要求,依赖服务(PHP前端代码)和这个核心服务之间的合同是什么,以及一个关于我们如何满足三个关键的非技术要求的计划:性能、效率和弹性。
系统设计涉及到了解系统必须执行其功能的约束条件,所需的功能是什么,以及相对于所有其他属性而言,系统的哪些属性是重要的。一旦你有了这些定义,你就可以开始设计一个符合要求的系统,并系统地规划出解决方案的交付。
当我们谈及系统设计时,通常有几个组成部分:
首先要了解一些事情,比如:系统是自成一体的(即:不会访问外部资源)还是分布式的?它是否会有一个用户界面,或者是非交互式的(例如,它是否会生成一份打印出来的报告,或者它在运行过程中是否需要来自人为或其他系统的输入?)它是否需要处理大量的流量?它是在任何时候只有十个人使用,还是在任何时候有千万个用户使用它?
一旦你对其中一些问题有了答案,通过系统设计原则做出决定就会更容易。
在这个现代社会中,设计软件系统的几个关键原则直到系统需要扩展时才完全出现——从一个单用户系统到一个应该能够同时处理成千上万甚至数百万用户的系统。以下是我们将在本文中介绍的一些内容:
当一个系统可以在资源成比例增长的情况下被部署来处理负载的增长时,它就是可扩展的。一个系统的扩展系数被定义为服务于系统负载增长所需的资源量的增长。我们在软件系统中会遇到两种典型的扩展情况:垂直扩展和水平扩展。
垂直扩展是指为软件系统提供更多的空间或单机资源以处理需求的增长。考虑一下网络附加存储设备的情况。你通过设备提供的存储越多,它能存储的数据就越多。如果你需要它处理更多的并发连接和I/O操作(IOPs),你通常需要增加计算能力和网络接口来处理增加的负载。
横向扩展是指用软件的副本复制一个系统或多台机器,以处理需求的增长。考虑一下隐藏在负载均衡器后面的静态网络内容服务器的情况。添加更多的服务器可以让更多的客户连接并从网络服务器上下载内容,当负载减弱后,网络服务器的数量可以缩减到适合当前需求的规模。
有些系统可以处理混合或对角线的扩展。例如,一些分布式数据库架构允许分割计算和存储节点,这样,计算重的工作负载可以使用具有更多计算资源的节点。相反,IOPs的重度工作负载可以在存储+计算节点上运行。例如,流处理应用程序可能会分离出需要更多内存和计算的工作负载(例如,事件源或分析工作负载),并适当地扩展这些工作负载,并独立于IOPs的重型工作负载(例如,压缩和归档)。
当一个系统能够容忍部分故障和恢复而不严重降低服务质量时,它就是可靠的。一个系统的可靠性的一部分包括其在延迟、吞吐量和遵守商定的操作范围方面的可预测性。
确保系统可靠性的通常方法包括以下内容:
构建可靠系统需要记住的关键一点是,以一种定义明确的方式处理潜在的故障,使依赖系统能够做出反应。这意味着如果有输入可能导致系统对所有人都可用,那么它就不是一个可靠的系统。同样地,如果系统依赖于另一个可能不可靠的系统,那么它应该用策略来处理不可靠的问题,以确保可靠性。
当以相应的努力来改变一个系统,并以最小的用户干扰来部署时,这个系统是可维护的。这就要求在实施系统的时候,假定需求会发生变化,并且系统有足够的灵活性来处理可预见的方向变化。这也意味着要确保代码的可读性,以便下一组维护者(可能是同一个团队,但在未来用新的眼光来看待它)能够维护软件,并使其进化以满足未来的需求。
没有人愿意被困在维护那些僵化的、难以改变的、没有良好组织的、文件化程度低的、设计不良的、未经测试的、胡乱拼凑的软件。
确保代码质量高是卓越工程的一部分,体现了专业精神和优秀的工艺。这不仅是一件好事,而且众所周知,它可以让高功能和高性能的工程团队提供可以改变和扩展的软件,以持续提供价值。
如果你的服务不可用,它可能不存在。
系统设计应该解决一个系统应该如何保持可用性,以保持与客户和系统用户的相关性。这意味着:
在我职业生涯的早期,我了解到,一个不稳定和不可用的系统有时会成为失去客户信任的最大原因。一旦你失去了客户的信任,就很难重新获得信任。
系统设计应该把安全作为一个关键环节来解决,特别是在互联网连接系统的时代,安全威胁和漏洞会对我们的客户和系统的使用者造成实际伤害。构建安全软件的目标并不是要达到完美,而是要了解漏洞和攻击所涉及的风险。拥有一个适当的安全威胁模型和一个系统的方法来理解风险所在,以及哪些类型的威胁值得优先考虑和设计缓解措施,是安全设计和工程实践的开始。
今天,随着我们的软件系统成为现代社会更多部分的关键任务服务的一部分,安全不再是可有可无的了。在我们设计的系统中,从一开始就认真对待安全问题,使我们更接近于能够更好地依赖我们所建立和部署的软件,以满足我们用户的需求。赢得客户的信任已经很不容易了,只需要一个漏洞就可以失去很好的一部分信任。
鉴于以上几个方面,现代分布式系统的一些模式已经出现,以不同的方式解决了这些方面的一些问题。让我们来探讨一下我们今天看到的关于系统设计的五个方面的一些比较流行的设计模式。
随着分布式系统的兴起,其重点是通过冗余建立可靠性和规模,通过横向扩展建立效率和性能,以及通过将系统的部分解耦为独立运行的服务来建立弹性,”微服务 “一词通过实现以下几点而得到普及:
通过我们的方面来看,微服务有吸引人的特性,如果适用于用例的话,这使它成为一个好的模式:
微服务是分解大型应用的一个好方法,在这里可以确定需要自己的扩展和可靠性域的逻辑分区。不过,当从头开始时,从一开始就设计微服务是不太理想的,因为有可能将服务分解成太小的碎片。微服务之间的通信成本–通常为HTTP或gRPC请求–是很重要的,只有在必要时才应该产生。确定功能是否适合于一个服务的一个好方法是遵循领域驱动设计或功能分解这样的做法。
如同在基于微服务的解决方案中,使用无服务器实现进一步将服务请求的关键功能部分委托给底层基础设施。如果在微服务中,服务是由一个持久化进程提供的,那么无服务器解决方案通常只实现一个入口点,以处理对一个端点的请求(通常是通过HTTP或gRPC的URI)。在无服务器部署中,没有配置实际的服务器,而是由部署环境根据需要启动资源来处理进来的请求。有时,这些资源会停留一段时间,以摊销启动它们的成本,但这只是一个实施细节。
让我们通过系统设计的各个方面来看看无服务器解决方案是如何叠加的:
无服务器解决方案,或功能即服务,是一种非常有吸引力的方式,通过关注业务逻辑和价值,让底层基础设施处理服务的可扩展性、可靠性和可用性,来进行原型设计甚至部署生产级解决方案。这是一个典型的起点,可以让一个具有最小运营负担的解决方案启动和运行,对于大多数原型来说,这是一个证明我们假设的好方法。这也是一个典型的经验,一旦这些解决方案达到了扩展的极限,与运行这些相关的成本就变得足够高。这些都变成了根据所需规模调整的更优化的微服务实现。
然而,有些问题领域不需要在线交易处理,而微服务和无服务器的实现并不完全符合要求。考虑到可以在后台或在有资源的情况下处理事务的情况。另一种情况是后台处理活动,其结果不一定是互动的。
事件驱动的系统遵循的模式是有一个事件源和事件汇,事件(消息)分别来自和被发送。处理是由订阅者和发布者分别对这些源和汇进行的。事件驱动系统的一个例子是一个聊天机器人,它可以参与许多对话(事件源和汇),并在它们进来时处理消息。
分布式事件驱动系统可以有多个并发的消息处理程序在相同的源上等待,可能会发布太多的汇,作为其他消息处理程序的源。这种通过汇和源将处理器连锁起来的模式被称为事件管道。通常情况下,汇和源有一个单一的实现,提供一个消息队列接口,并根据通过系统的消息需求来进行扩展。许多分布式队列管理系统也可以有效地从对角线扩展中获益,比如Apache Kafka、RabbitMQ等。
让我们通过我们的五个方面来研究分布式事件驱动系统:
现代软件工程需要设计可扩展、可靠、可维护、可用和安全的系统。设计分布式系统需要非常严格的要求,因为现代系统的现实复杂性随着社会对更好的软件服务的需求而增长。我们回顾了分布式系统的三种现代设计模式,并研究了设计良好的系统的五个方面。
作为软件工程师,我们负责设计系统,解决现代分布式系统的关键问题。
在该系列的下一篇文章中,我将会写到测试及其在现代软件工程中的作用。
【转自】 https://betterprogramming.pub/modern-software-engineering-a-series-part-1-system-design-d689fabae772
]]>IEEE 802.1X是一种网络访问控制(NAC)协议,它定义了一种在网络交换机或接入点上进行认证的标准方式。它是一个端口级别的访问控制协议,通过端口的身份验证来控制网络上的主机是否有权访问资源。IEEE 802.1X提供了一种允许网络管理员验证用户的身份、检查设备是否满足最低安全标准并允许或拒绝访问网络的方法。
使用IEEE 802.1X,网络管理员可以防止未经授权的设备、病毒或其他恶意软件进入网络并对网络进行攻击。此外,它也提供了一种机制来保护网络免受暴力破解、ARP欺骗和MAC地址欺骗等攻击形式。因此,IEEE 802.1X可以增加网络的安全性以确保网络的保密性和数据完整性。
Linux下网络配置工具不同的发行版之间略有不同,而且有些发行版默认的网络工具存在问题,会导致认证失败的问题。
本文提供一种基于Docker的认证方法。
首先当然需要安装 Docker 和 docker-compose
Dockerfile
FROM debian:11RUN set -e; \ apt-get update; \ apt-get upgrade -y; \ apt-get install -y net-tools wpasupplicant
docker-compose.yaml
version: '3'services: uniteos: build: context: ./ dockerfile: Dockerfile command: wpa_supplicant -D wired -i enp3s0 -c /wpa.conf -dd -t network_mode: host volumes: - ./wpa.conf:/wpa.conf:ro restart: always
wpa.conf
ctrl_interface=/var/run/wpa_supplicantctrl_interface_group=rootap_scan=0network={ key_mgmt=IEEE8021X eap=PEAP phase2="autheap=GTC" identity="<username>" password="<password>" priority=2}
docker-compose up -d
]]>init()
函数进行初始化,然而一个项目会有多个可执行程序,比如: 本文将介绍如何使用 godepgraph 工具解决 Golang 项目开发过程中优化代码结构。
go install github.com/kisielk/godepgraph@latest
godepgraph github.com/ismdeep/station/cmd/server
输出如下:
digraph godep {splines=orthonodesep=0.4ranksep=0.8node [shape="box",style="rounded,filled"]edge [arrowsize="0.5"]"bufio" [label="bufio" color="palegreen" URL="https://godoc.org/bufio" target="_blank"];"bytes" [label="bytes" color="palegreen" URL="https://godoc.org/bytes" target="_blank"];"compress/bzip2" [label="compress/bzip2" color="palegreen" URL="https://godoc.org/compress/bzip2" target="_blank"];"compress/flate" [label="compress/flate" color="palegreen" URL="https://godoc.org/compress/flate" target="_blank"];"compress/gzip" [label="compress/gzip" color="palegreen" URL="https://godoc.org/compress/gzip" target="_blank"];"compress/zlib" [label="compress/zlib" color="palegreen" URL="https://godoc.org/compress/zlib" target="_blank"];"container/list" [label="container/list" color="palegreen" URL="https://godoc.org/container/list" target="_blank"];"unicode" [label="unicode" color="palegreen" URL="https://godoc.org/unicode" target="_blank"];"unicode/utf16" [label="unicode/utf16" color="palegreen" URL="https://godoc.org/unicode/utf16" target="_blank"];"unicode/utf8" [label="unicode/utf8" color="palegreen" URL="https://godoc.org/unicode/utf8" target="_blank"];"unsafe" [label="unsafe" color="palegreen" URL="https://godoc.org/unsafe" target="_blank"];}
但是这样的输出显示是有太多是我们不想去关心的。可以通过以下参数进行筛选。
通过 -onlyprefixes
可以筛选仅以某个包名为前缀的包。
godepgraph -onlyprefixes github.com/ismdeep/station github.com/ismdeep/station/cmd/server
首先安装 graphviz 包
sudo apt updatesudo apt install -y graphviz
产生依赖关系树并输出svg图片
godepgraph -onlyprefixes github.com/ismdeep/station github.com/ismdeep/station/cmd/server | dot -Tsvg -o server.svg
从上图来看,包名前缀也是比较多余,可以通过 sed 命令清理掉。
godepgraph -onlyprefixes github.com/ismdeep/station github.com/ismdeep/station/cmd/server | sed 's/github\.com\/ismdeep\/station\///g' | dot -Tsvg -o server.svg
]]>tmux ls
tmux new -s <session-name>
tmux attach -t <session-name>
or
tmux a -t <session-name>
]]>SPA ( single-page application )仅在Web 页面初始化时加载相应的 HTML、JavaScript和CSS.一旦页面加载完成,SPA 不会因为用户的操作而进行页面的重新加载或跳转;取而代之的是利用路由机制实现 HTML 内容的变换,UI与用户的交互避免页面的重新加载。
优点:
基于上面一点,SPA 相对对服务器压力小;前后端职责分离,架构清晰,前端进行交互逻辑,后端负责数据处理;
缺点:
v-if 是真正的条件渲染,因为它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建;也是情性的:如果在初始渲染时条件为假,则什么也不做——直到条件第一次变为真时,才会开始渲染条件块。
v-show 就简单得多 ——不管初始条件是什么,元素总是会被渲染,并且只是简单地基于 CSS 的“display” 属性进行切换。
所以,v-if 适用于在运行时很少改变条件,不需要频繁切换条件的场景;v-show 则适用于需要非常频繁切换条件的场景。
class可以通过对象语法和数组语法进行动态绑定:
对象语法:
<div v-bind:class="{ 'active': isActive, 'text-danger': hasError }"></div>data: { isActive: true, hasError: false}
数组语法:
<div v-bind:class="[isActive ? activeClass : '', errorClass]"></div>data: { activeClass: 'active', errorClass: 'text-danger'}
style也可以通过对象语法和数组语法进行动态绑定:
对象语法:
<div v-bind:style="{ color: activeColor, fontSize: fontSize + 'px' }"></div>data: { activeColor: 'red', fontSize: 30}
数组语法:
<div v-bind:style="[styleColor, styleSize]"></div>data: { styleColor: { color: 'red' styleSize:{ fontSize:"'23px' }}
所有的 prop 都使得其父子 prop 之间形成了一个单向下行鄉定:父级prop 的更新会向下流动到子组件中,但是反过来则不行。这样会防止从子组件意外改变父级组件的状态,从而导致你的应用的数据流向难以理解。
额外的,每次父级组件发生更新时,子组件中所有的prop 都将会刷新为最新的值。这意味着你不应该在一个子组件内部改变 prop。如果你这样做了,vue 会在浏览器的控制台中发出警告。子组件想修改时,只能通过 $emit 派发一个自定义事件,父组件接收到后,由父组件修改。
有两种常见的试图改变一个prop 的情形:
这个prop 用来传递一个初始值;这个子组件接下来希望将其作为一个本地的 prop数据来使用。在这种情况下,最好定义一个本地的data 属性并将这个 prop 用作其初始值:
props: ['initialCounter'],data: function() { return { counter: this.initialCounter }}
这个prop 以一种原始的值传入且需要进行转换。在这种情况下,最好使用这个prop 的值来定义一个计算属性
props: ['size']computed: { normalizedSize: function() { return this.size.trim().toLowerCase() }}
computed:是计算属性,依赖其它属性值,并且 computed 的值有缓存,只有它依赖的属性值发生改变,下一次获取 computed 的值时才会重新计算 computed 的值;
watch:更多的是「观察」的作用,类似于某些数据的监听回调,每当监听的数据变化时都会执行回调进行后续操作;
运用场景:
由于JavaScript 的限制,vue 不能检测到以下数组的变动:
vm.items[indexOfltem] = newValue
vm.items.length = newLength
为了解决第一个问题,Vue 提供了以下操作方法:
// Vue.setVue.set(vm.items, indexOfItem, newValue)// vm.$set, Vue.set的一个别名vm.$set(vm.items, indexOfItem, newValue)// Array.prototype.splicevm.items.splice(indexOfItem, 1, newValue)
为了解决第二个问题,Vue提供了一下操作方法:
// Array.prototype.splicevm.items.splice(newLength)
(1) 生命周期是什么?
Vue实例有一个完整的生命周期,也就是从开始创建、初始化数据、编译模版、挂载Dom->渲染、更新->渲染、卸载等一系列过程,我们称这是 vue 的生命周期。
(2) 各个生命周期的作用
生命周期 | 描述 |
---|---|
beforeCreate | 组件实例被创建之初,组件的属性生效之前 |
created | 组件实例已经完全创建,属性也绑定,但真实 dom 还没有生成,$el 还不可用 |
beforeMount | 在挂载开始之前被调用:相关的render 函数首次被调用 |
mounted | el被新创建的 vm.$el替换,并挂载到实例上去之后调用该钩子 |
beforeUpdate | 组件数据更新之前调用,发生在虚拟 DOM 打补丁之前 |
update | 组件数据更新之后 |
activated | keep-alive 专属,组件被激活时调用 |
deactivated | keep-alive 专属,组件被销毁时调用 |
beforeDestroy | 组件销毁前调用 |
destroyed | 组件销毁后调用 |
Vue的父组件和子组件生命周期钩子函数执行顺序可以归类为以下4部分:
可以在钩子西数 created、beforeMount、mounted 中进行调用,因为在这三个钩子函数中,data 己经创建,可以将服务端端返回的数据进行赋值。但是本人推荐在created 钩子西数中调用异步请求,因为在 created 钩子西数中调用异步请求有以下优点:
在钩子函数 mounted 被调用前,vue 已经将编译好的模板挂载到页面上,所以在mounted 中可以访问操作 DOM。 vue 具体的生命周期示意图可以参见如下,理解了整个生命周期各个阶段的操作,关于生命周期相关的面试题就难不倒你了。
比如有父组件 Parent 和子组件 Child,如果父组件监听到子组件挂载 mounted 就做一些逻辑处理,可以通过以下写法实现:
/ Parent.vue<Child @mounted= "doSomething" />// Child.vuemounted(){ this.$emit("mounted");}
以上需要手动通过 $emit 触发父组件的事件,更简单的方式可以在父组件引用子组件时通过 @hook 来监听即可,如下所示:
// Parent.vue<Child @hook:mounted= "doSomething"></Child>doSomething(){ console.log('父组件监听到 mounted 钩子函数...')}// Child.vuemounted(){ console.log('子组件触发mounted 钩子函数...')}// 以上输出顺序为:// 子组件触发mounted钩子函数.// 父组件监听到mounted钩子函数.当然 @hook 方法不仅仅是可以监听 mounted,其它的生命周期事件,例如:created, updated 等都可以监听。
keep-alive 是 vue 内置的一个组件,可以使被包含的组件保留状态,避免重新渲染,其有以下特性:
为什么组件中的 data 必须是一个函数,然后return 一个对象,而new vue 实例里,data 可以直接是一个对象?
// datadata() { return { message:"子组件", childName:this.name }}// new Vuenew Vue({ el: '#app', router, template: '<App/>, components: {App}})
因为组件是用来复用的,且JS里对象是引用关系,如果组件中 data 是一个对象,那么这样作用域没有隔离,子组件中的data 属性值会相互影响,如果组件中 data 选项是一个西数,那么每个实例可以维护一份被返回对象的独立的拷贝,组件实例之间的data 属性值不会互相影响;而new vue 的实例,是不会被复用的,因此不存在引用对象的问题。
我们在 vue 项目中主要使用 v-model 指令在表单 input、textarea、select 等元素上创建双向数据绑定,我们知道 v-model 本质上不过是语法糖,v-model 在内部为不同的输入元素使用不同的属性并抛出不同的事件:
以input 表单元素为例:
<input v-model='something'>
相当于
<input v-bind:value ="something" v-on:input="something = Sevent.target.value">
如果在自定义组件中,v-model 默认会利用名为 value 的prop 和名为 input 的事件,
如下所示:
父组件:
<ModelChild -model= "message"></ModelChild>
子组件:
<div>{{value}}</div>props:{ value: String},methods: { test(){ this. $emit ('input', "小红") },},
Vue组件间通信是面试常考的知识点之一,这题有点类似于开放题,你回答出越多方法当然越加分,表明你对 vue 掌握的越熟练。vue 组件问通信只要指以下 3 类通信:父子组件通信、隔代组件通信、兄弟组件通信,下面我们分别介绍每种通信方式且会说明此种方法可适用于哪类组件间通信。
(1) props / $emit 适用 父子组件通信
这种方法是 vue 组件的基础,相信大部分同学耳闻能详,所以此处就不举例展开介绍。
(2) ref 与 $parent / $children 适用 父子组件通信
(3) EventBus($emit / $on)适用于 父子、隔代、兄弟组件通信
这种方法通过一个空的 vue 实例作为中央事件总线(事件中心),用它来触发事件和监听事件,从而实现任何组件间的通信,包括父子、隔代、兄弟组件。
(4) $attrs/$listeners 适用于 隔代组件通信
(5) provide / inject 适用于隔代组件通信
祖先组件中通过 provider 来提供变量,然后在子孙组件中通过 inject 来注入变量。provide / inject API 主要解决了跨级组件间的通信问题,不过它的使用场景,主要是子组件获取上级组件的状态,跨级组件间建立了一种主动提供与依赖注入的关系。
(6) Vuex 适用于 父子、隔代、兄弟组件通信
Vuex 是一个专为 vuejs 应用程序开发的状态管理模式。每一个vuex 应用的核心就是store(仓库)。“store” 基本上就是一个容器,它包含着你的应用中大部分的状态( state )。
Vuex是一个专为Vue.js应用程序开发的状态管理模式。每一个Vuex应用的核心就是store(仓库)。“store”基本上就是一个容器,它包含着你的应用中大部分的状态( state )。
(1) vuex 的状态存储是响应式的。当 vue 组件从 store 中读取状态的时候,若 store中的状态发生变化,那么相应的组件也会相应地得到高效更新。
(2) 改变 store 中的状态的唯一途径就是显式地提交 (commit) mutation。这样使得我们可以方便地跟踪每一个状态的变化。
主要包括以下几个模块:
Vue.js是构建客户端应用程序的框架。默认情况下,可以在浏览器中输出Vue组件进行生成DOM 和操作DOM。然而,也可以将同一个组件渲染为服务端的 HTML字符串,将它们直接发送到浏览器,最后将这些静态标记”激活”为客户端上完全可交互的应用程序。
即:SSR大致的意思就是vue在客户端将标签渲染成的整个html 片段的工作在服务端完成,服务端形成的html 片段直接返回给客户端这个过程就叫做服务端渲染。
服务端渲染 SSR 的优缺点如下:
(1) 服务端渲染的优点:
(2) 服务端渲染的缺点:
vue-router 有3种路由模式:hash、history、abstract,对应的源码如下所示:
switch(mode) { case 'history': this.history = new HTML5History(this, options.base) break case 'hash': this.history = new HashHistory(this, options.base, this.fallback) break case 'abstract': this. history = new AbstractHistory(this, options.base) break default: if (process.env.NODE ENV!== 'production') { assert(false, "invalid mode: ${mode}") }}
其中,3种路由模式的说明如下:
(1) hash 模式的实现原理
早期的前端路由的实现就是基于 location.hash 来实现的。其实现原理很简单,location.hash 的值就是URL 中#后面的内容。比如下面这个网站,它的location.hash 的值为”#search’:
hash 路由模式的实现主要是基于下面几个特性:
(2) history 模式的实现原理
HTML5 提供了 History API 来实现 URL 的变化。其中做最主要的 API 有以下两个:history.pushState()
和 history.repalceState()
。这两个 API可以在不进行刷新的情况下,操作浏览器的历史纪录。唯一不同的是,前者是新增一个历史记录,后者是直接替换当前的历史记录,如下所示:
window.history.pushState(null, null, path);window.history.replaceState(null, null, path);
history 路由模式的实现主要基于存在下面几个特性:
history.pushState()
或 history.replaceState()
不会触发 popstate 事件,这时我们需要手动触发页面跳转(渲染)。ModelView-ViewModel (MvvM)是一个软件架构设计模式,由微软 WPF 和Silverlight 的架构师 Ken Cooper 和Ted Peters 开发,是一种简化用户界面的事件驱动编程方式。由 John Gossman(同样也是 WPF 和 Silverlight 的架构师)于2005年在他的博客上发表MVVM 源自于经典的 Model-View-Controller (MVC)模式,MvVM 的出现促进了前端开发与后端业务逻辑的分离,极大地提高了前端开发效率,MVVM 的核心是ViewModel 层,它就像是一个中转站 (value converter),负责转换Model 中的数据对象来让数据变得更容易管理和使用,该层向上与视图层进行双向数据绑定,向下与Model 层通过接口请求进行数据交互,起呈上启下作用。
(1) View 层
View 是视图层,也就是用户界面。前端主要由 HTML 和CSS 来构建。
(2) Model 层
Model 是指数据模型,泛指后端进行的各种业务逻辑处理和数据操控,对于前端来说就是后端提供的 api 接口。
Vue 数据双向鄉定主要是指:数据变化更新视图,视图变化更新数据。
即:
vue 主要通过以下 4个步骤来实现数据双向绑定的
如果被问到 vue 怎么实现数据双向绑定,大家肯定都会回答通过 Object.defineProperty() 对数据进行劫持,但是 Object.defineProperty() 只能对属性进行数据劫持,不能对整个对象进行劫持,同理无法对数组进行劫持,但是我们在使用 vue框架中都知道,vue 能检测到对象和数组(部分方法的操作)的变化,那它是怎么实现的呢?我们查看相关代码如下:
/*** Observe a list of Array items.*/observeArray(items: Array<any>) { for (let i = 0, | = items.length; i < I; i++) { observe(items[i]) // observe 功能为监测数据的变化 }}/*** 对属性进行递归遍历*/let childob = !shallow && observe(val) // observe 功能为监测数据的变化
通过以上 vue 源码部分 查看,我们就能知道 vue 框架是通过遍历数组 和递归遍历对象,从而达到利用 Object.defineProperty( 也能对对象和数组(部分方法的操作)进行监听。
Proxy 的优势如下:
Object.defineProperty 的优势如下:
受现代 JavaScript 的限制,vue 无法检测到对象属性的添加或删除。由于 vue 会在初始化实例时对属性执行 getter/setter 转化,所以属性必须在 data 对象上存在才能让vue 将它转换为响应式的。但是vue 提供了 Vue.set (object, propertyName, value) / vm.$set (object, propertyName, value) 来实现为对象添加响应式属性,那框架本身是如何实现的呢?
我们查看对应的 vue 源码:vue/src/core/instance/index.js
export function set (target: Array<any> | Object, key: any, val: any): any { // target 为数组 if (Array.isArray(target) && isValidArrayIndex(key)) { // 修改数组的长度,避免索引>数组长度导致splcie()执行有误 target.length = Math.max(target.length, key) //利用数组的splice变异方法触发响应式 target.splice(key, 1, val) return val } // key 已经存在,直接修改属性值 if (key in target && !(key in Object.prototype)) { target[key] = val return val } const ob = (target: any)._ob_ // target 本身就不是响应式数据,直接赋值 if (!ob) { target[key] = val return val } // 对属性进行响应式处理 defineReactive(ob.value, key, val) ob.dep.notify() return val}
我们阅读以上源码可知,vm.$set 的实现原理是:
优点:
缺点:
虛拟 DOM 的实现原理主要包括以下3部分:
key 是为 Vue 中 Vnode 的唯一标记,通过这个key,我们的diff 操作可以更准确、更快速。vue 的 diff 过程可以概括为:oldCh 和newCh 各有两个头尾的变量 oldStartindex、oldEndindex 和 newStartindex、newEndlindex,它们会新节点和旧节点会进行两两对比,即一共有4种比较方式:newStartindex 和 oldStartindex 、newEndIndex 和 oldEndindex、newStartIndex 和 oldEndIndex , newEndIndex 和oldStartindex, 如果以上 4种比较都没匹配,如果设置了key,就会用key 再进行比较,在比较的过程中,遍历会往中间靠,一旦 StartIndex>EndIndex 表明 oldCh 和 newCh 至少有一个已经遍历完了,就会结束比较。
所以 vue 中 key 的作用是:key是为vue 中 vnode 的唯一标记,通过这个key,我们的 diff 操作可以更准确、更快速。
更准确:因为带 key 就不是就地复用了,在sameNode 函数 a.key === b.key 对比中可以避免就地复用的情况。所以会更加准确。
更快速:利用key 的唯一性生成map 对象来获取对应节点,比遍历方式更快,源码如下:
function createKeyToOldIdx (children, beginIdx, endIdx){ let i, key const map = 0 for (i = beginIdx; i <= endIdx; ++i) { key = children[i].key if (isDef(key)) map[key] = i } return map}
(1) 代码层面的优化
(2) Webpack 层面的优化
(3) 基础的Web技术的优化
Vue 3.0 的目标是让 Vue 核心变得更小、更快、更强大,因此 Vue 3.0 增加以下这些新特性:
(1) 监测机制的改变
3.0 将带来基于代理 Proxy 的 observer 实现,提供全语言覆盖的反应性跟踪。这消除了 vue 2 当中基于 Object.defineProperty 的实现所存在的很多限制:
新的 observer 还提供了以下特性:
(2) 模板
模板方面没有大的变更,只改了作用域插槽,2.× 的机制导致作用域插槽变了,父组件会重新渲染,而3.0把作用域插槽改成了函数的方式,这样只会影响子组件的重新渲染,提升了渲染的性能。同时,对于render 西数的方面,vue3.0 也会进行一系列更改来方便习惯直接使用 api 来生成 vdom.
(3) 对象式的组件声明方式
Vue2.x 中的组件是通过声明的方式传入一系列 option,和Type Script 的结合需要通过一 些装饰器的方式来做,量然能实现功能,但是比较麻烦。3.0 修改了组件的声明方式,改成了类式的写法,这样使得和 Type Script 的结合变得很容易。
此外,Vue 的源码也改用了 Type Script 来写。其实当代码的功能复杂之后,必须有。个静态类型系统来做一些辅助管理。现在 vue3.0 也全面改用 TypeScript 来重写了,更是使得对外暴露的 api 更容易结合 TypeScript。静态类型系统对于复杂代码的维护确实很有必要。
(4) 其它方面的更改
Vue 3.0 的改变是全面的,上面只涉及到主要的 3个方面,还有一些其他的更改:
《技术的本质》这本书讲述的并不是一项一项具体的技术,或者是某些将要出炉的新技术。作者最想告诉我们的是技术是什么,以及技术是如何进化的。
《技术的本质》这本书作者是著名的技术思想家、经济学家布莱恩·阿瑟教授,他涉猎了非常多的领域,包括电子工程、数学、经济学等等。但是,半个世纪以来,他一直在研究和追踪唯一一个重要的课题,那就是路径依赖性。也就是研究人的行为如何依赖于过去的全部行为。由此,他把这个研究应用到了很多的领域,其中就包括技术的进化和发展。可以说,阿瑟是少有的可以称为既是狐狸型通才又是刺猬型专才的人。阿瑟教授曾经担任过斯坦福大学人口研究与经济学系的主任。也正是在那一年,他成为了斯坦福大学历史上最年轻的终身教授。随后,他在经济学泰斗、诺贝尔奖获得者阿罗教授的邀请之下,加入了圣塔菲研究所。他这一待,就是20多年,成为了这座跨科学研究圣殿当中当之无愧的元老级人物。阿瑟教授的研究,对于深入了解创新贡献很大。在1990年的时候,他就获得了专门颁给那些研究创新问题的顶尖学者的熊彼特奖。阿瑟教授对于技术本质的探寻,为技术创新的发展带来了很多重要的洞见,比如 Java 语言的开发就用到了他的思想。在高科技投资领域,阿瑟教授的很多观点都是决策者必须要掌握的风向标。在《技术的本质》这本书里,阿塞教授就告诉我们,技术并不是死的,而是活生生的,它有自己的进化方向,也有自己的行事逻辑。他在写作这本书的时候,讲了很多引人入胜的例子,从人类最初是怎么打造粗陋的石器,到怎么完成了复杂的石油冶炼,怎么从简易的莱特兄弟的飞行器到火箭和航天飞机的发明,还有基因编辑、水力发电、养殖蜜蜂等等。
通过这些林林总总技术进化和创新的例子色也提出了不少新的概念,比如说自创生、垫脚石、技术活跃、技术体等等。同时,他也提出了很多新的见解,比如说科学和经济是技术的副产品,又比如新技术先是对精神的建构,之后才是对物质的建构。接下来,我将通过三个部分来讲述《技术的本质》这本书中最重要的内容,分别是:
首先,技术是什么,换句话说,技术从本质上来讲具有什么特性?技术从本质上来讲是自创生的,也就是说记述自己,创造了自己。如果我们去仔细观察任何一项新技术,就会发现所有新技术都不是无中生有被发明出来的,新技术都是从现存技术当中组合出来的。一组新的要素技术可以无限构成新的组合。比如黏合剂,既可以把它涂在玻璃纸的背面,成为透明胶带;也可以把它用在波音飞机的消音版上来抵抗声波的震动。你还可以把它用在高尔夫球杆上,在强烈的撞击之下,碳素纤维和碳依然能够牢牢的粘在一起。甚至,还可以把它涂在带电的玻璃表面,装到显示器上,这就成了智能手机的触摸屏。再比如全球定位系统,可以把它安装在飞机、轮船、汽车上进行导航,还可以用它来辅助土地勘探、规划土地耕种等等。某一项技术,就像是化学反应当中一个高度活跃的成分,虽然只是参与了各种技术组合当中的一项功能,但是却主导着这些不同的组合,它可以在无限的组合当中和其他的技术匹配在一起,为不同的目的进一步组合成各种各样的可能性。
书中,作者就给出了有关技术的三层定义,第一层定义也是最基础的一个定义,那就是技术是实现人的目的的一种手段,从这个角度来看,技术是单数的。它可以是一种方法,比如语音识别算法,也可以是一个过程,比如说化学工程当中的过滤法,还可以是一项装置,比如一个柴油发动机。从这个层面来说,技术可以是简单的,也可以是复杂的,可以是物质的,也可以是非物质的。当我们在强调技术是一种软件的时候,我们看到的就是过程和方法,而当我们强调技术是一种硬件的时候,我们看到的就是具体的物理设备。技术的第二层定义,指的是它是实践和元器件的集成。从这个视角上来看,技术常常是复数的是技术的集合,或者叫做技术的工具箱。那当我们提到电子技术或者是生物技术的时候,就是指技术的这第二层定义。那技术的第三层定义指的是,它可以是运用在某种文化当中的装置和工程实践的结合。换句话说,这层定义是指技术是人类所有过去和现存技术的总和,是产生于对各种各样的自然现象的应用。从这个角度上来看,技术具有更普遍的意义。比如当凯文凯利在他的演讲当中说,技术就是和硅谷相关的一切,那这个时候他指的就是技术的这第三层定义。
本书中,作者谈论的技术更多是基于第二层和第三层定义,而他谈论的重点尤其在于技术是如何进化的。
技术是如何进化的,即新技术是怎么产生的。关于技术的进化,这样的讨论和观点其实从很早以前就开始了。1859年,达尔文出版了有关生物进化的《物种起源》,仅仅四年之后英国著名的作家塞缪尔·巴特勒,就提出了机械王国的理论,他写了一篇影响广泛的文章——《机械中的达尔文》。巴特勒,他提出的观点就是在努力的把技术塞进一个类似达尔文生物进化论的理论框架里面去。如今大家都知道,达尔文提出的生物进化两个最主要的机制就是变异和选择。如果我们来分析一些特定技术的时候,确实能够看到跟生物进化机制相类似的一些情况。比如说像火车的进化,就是通过变异和选择逐渐发展出了蒸汽机车,电力机车,磁悬浮列车等等。但是,用达尔文的进化理论来解释技术,有些地方就很难解释得通。比如说,很多新技术实现的是飞跃式的创新,也就是说在面对同样一个要实现的目标的时候,新技术会用完全不同的原理来实现它,一点儿都看不到前任技术的痕迹。比如说,就拿书籍出版来举例吧,人类先是发明了用笔和纸来抄写书籍,接着活字印刷术出现了,而发展到现代,出现了数码印刷机,每一次的进化都完全颠覆了以往技术。这样的例子还有很多,比如说像是激光喷气式飞机、雷达等等。这些技术,他们在刚刚出现的时候,跟以前解决同样问题的技术,使用的原理也是完全不一样的。也就是说很多新技术不是任何其他先前技术的变种,也不是在以往技术的基础上稳步积累形成的。这些情况,用变异和选择的机制完全解释不通。所以如果想要解释这种完全颠覆的新技术是怎么来的,那么就一定存在一套其他的新理论。作者就认为技术进化的机制就是组合进化。最初很简单的技术通过组合成一个一个的技术模块,发展出了越来越多的复杂技术形式。如果我们把进化的机制比喻成人。那么它更像是一个喜欢古代小玩意儿的人,而不是一个天才的发明家。这个人,并不是在从无到有地发明出一些东西,而是把现有的一些部件进行连接组合,推陈出新,制造出一个新的东西。古腾堡发明印刷机,就是一个经典的技术组合进化,它更像是一种拼装,而不是无中生有的全新突破。在印刷机这项独立发明出现之前,已经具备了很多的关键元素。比如说活字印刷术,是在公元前四世纪由毕升发明的。又比如螺旋压榨技术,最初它使用于葡萄酒压榨机的。还有油墨和纸张的生产技术,这些也都被发明出来了。
另外,古腾堡还曾经做过金匠,凭借着对这一行的了解,他又对活字印刷系统整体进行了一些改进。所以,古腾堡发明印刷机并不是突如其来的一种全新技术,而是把各个不同领域的现存成熟技术拼装在了一起。这些拼装在一起的技术模块,每一个都可以实现不同的功能,这些功能相互联系,共同服务于一个核心目标。然而,这种模块化的组合并不是技术进化的唯一机制。比如说激光技术,它的用途非常多,在农业生产当中,激光可以用在育种、除草、灭虫、检验作物基因等等方面儿,在日常生活当中,激光唱片、激光录像都是最常见的。在医疗当中,激光可以用作钻头、手术刀、焊枪等等,而在军事当中,激光可以用在坦克、舰艇、飞机等等武器装备上,还可以用在雷达上和激光制导炸弹上。机关的应用,是如此的广泛,但是当你追溯它的发展进程的时候,就会发现它并不是从某些特定的技术,比如说在农业当中,他不是从化学农药进化而来的,在武器当中,他也不是由弓箭制造技术发展而来的,在医疗当中,也并不是从制陶工艺而来的。所以一定有另外的超出组合进化的机制在起着作用。作者就认为,这第二个重要的机制就是捕捉现象。它指的是技术能够持续发现新的自然现象,并且带着特定的目的去驾驭这些现象。比如瑞士发明家维斯特劳注意到宠物狗身上沾满了带刺的苍耳之后,他就发明了尼龙搭扣。又比如,谷歌的创始人拉里佩奇和谢尔盖布林他们就观察到,人们在日常检索内容的时候,会利用文章之间的交叉引用,他们就根据这个现象开发出了谷歌后台的超链接搜索算法。作者在这本书当中用一句话给出了很精辟的概括,那就是技术的本质就是对现象的有目的的编程。我给你讲一个故事,你就能很好的理解这句话了。高露洁公司,曾经被一个难题困扰了几十年。你肯定听说过含氟牙膏,据说,可以帮助孩子预防龋齿,那制造这种牙膏的时候,就需要把氟化物的粉末加到牙膏管里去。可是,这道工序常常会把氟化物的粉尘弄得到处都是。高露洁的专家们就研究了几十年,但是都不能有效解决这个问题。最后,他们决定向社会广泛求助,提供高额奖金来悬赏解决方案。最后,一位粒子物理学专家提供了一种方法解决了这个难题。他捕捉到的现象非常的简单,那就是带电的粉尘可以被塑料吸附。所以他就想办法让这些氟化物粉末带电,同时,把一个塑料管接地,这样带电的粉尘就会自动进到塑料管里,不会弄的到处都是了。这个解决方法实在是太简单了,但是却完美解决了困扰高露洁工程师长达几十年之久的大难题。
自然现象是新技术赖以产生的必不可少的源泉。所有的技术,不论是多么简单,或者是多么复杂,实际上,都应用了一种或者是几种现象。比如说,实际上,它用到了两个自然现象,一个是化学物质燃烧带来的热能可以转化为动能,这个现象,带来了发动机,让卡车可以实现自动推进。另一个是圆的东西滚动起来,比方的东西摩擦力小,这个现象带来了轮子,让卡车更容易移动。如此看来,一辆卡车的实质其实就是一个平台加上一个车厢,这个平台能够自动推进儿,车厢装载着人或者是货物,平台能够让车厢更容易的移动。卡车就是把两个现象结合在一起的,但最近日本某家汽车公司,他们真的把这两个现象分开进行设计,再组合起来使用,产生了新的创意。你可以看到这种新型的概念车,它分成了平台和车厢两部分,可以根据需要随意组合,你可以为平台匹配上不同功能和尺寸的车厢,这样就可以实现更多的装载移动的功能。平台匹配上封闭的带座位的车厢就成为了一个旅游大巴,平台匹配上一个大翻斗,就变成了一辆工程用车。所以,今后你要购买汽车的时候,可以变成分开购买底部的可移动平台和各种不同功能的车厢,这样的话你就相当于可以通过不同的组合拥有了很多量不同功能的汽车。
技术的进化机制,就是组合进化。作者告诉我们,每个技术都是通过对一组固定的现象用不同的方式进行编程创造出来的。随着时间的推移,新的现象、核心的技术会不断的加入进化。所有的技术归根到底都来自自然现象,所有的技术最终都是这些现象的奏鸣曲。
技术的进化怎么推动了经济的进化,也就是说活生生的技术,它是怎么让经济启动了自身生生不息的繁衍。作者认为,众多的技术结合在一起,就创造出了我们称之为经济的东西,他把经济定义为一套安排和活动。社会,可以借助这套系统来满足所有的需求,构成经济的整套安排,包括所有的制度、方法和与它对应的技术,比如说诊所和外科手术,市场和定价系统,金融系统和监管体系等等,这些安排都是实现人类各种需求的手段。经济从技术当中浮现,新技术带来新的解决方案,但同时也会带来新的问题和新的需求。而经济就是技术的一种表达,技术进化会引发经济的进化。作者就认为,在理解新技术带动新经济的过程当中有两个概念非常的重要,一个是重新域定,域是领域的域,定是定位的定。另一个是标准工程。这两个概念都和创新密切相关,我先来说说什么是重新域定。所谓的玉,通常指的是一个范畴,比如时间的范畴就可以叫做时间域,空间的范畴就可以叫做空间域。那么同样的道理,我们可以把一组技术组成的工具箱和它相应的应用规范叫做这种技术的范畴,也就是技术域。这组技术,他们有共同的能力,捕捉了共同的自然现象,可以完成共同的目标。
比如说无线电工程就是一个域,数字技术也是一个域。这样,域定这个词的意思也就好理解了,那就是说,我们在开展一项任务之前,要确定具体使用哪些技术范畴。比如,一位建筑师要开始设计一个新的办公大楼,他可能会选择玻璃钢架结构,也可能会选择花岗岩。那么这个过程,就叫做域定,也就是要确定使用哪套技术工具箱和对应的规范。说到这里,重新域定的含义也就非常明确了,他指的是,你为了实现某个需求,放弃了原来使用的技术规范,
重新选择了一套新的规范。这个过程非常重要,会产生新的技术,带来颠覆性的改变,甚至你可以说重新域定这个概念,它会重新定义一个时代和它的边界。通常这会是一个伟大创意的诞生,也是一次改变世界的创举。比如说,人类运输史的发展,就是不断进行重新域定的过程。最开始,人类运输使用的是陆地域,学会开凿运河之后,运输就进入了由驳船、船夫、船闸等等构成的运河域。当发明飞机以后,运输就进入了航空域。陆地运输、水路运输和航空运输这三次重新域定,就是对人类运输史的创新。又比如,以前我们听音乐的时候,使用的是黑胶唱片,处理信息的方式是模拟,后来,我们听音乐用CD,处理信息的方式是数据。这也是一次重新域定。还比如,你可以从这个视角来看待零售出现,互联网之前,日常购物行为发生在线下实体商店,当电子商务时代来临的时候,日常购物行为发生在线上的网络商店,而现在的新零售时代,日常购物行为同时发生在线上和线下。这里日常购物行为是一项贯穿始终的就任务,而这项任务被我们在线上线下进行不断的重新表达,这就是零售业的重新域定,也是零售业的不断创新。说到这里,我们得出的结论就很重要了,那就是所有的创新都是在新的可能的世界当中,把旧的任务不断进行重新表达的过程。用这个思路来看待问题,会让你在技术急剧的变化当中更好地把握变与不变的本质。
接下来,作者又提到了标准工程。如果你看过任何一本工程手册就会发现,里面有许多的标准问题的解决方案,比如说耦合旋转轴的19种方法,15种不同的凸轮结构,或者是五种振荡器的使用方法等等。这一类的手册,提供了标准的解决方案来解决那些重复性的问题。他也可以是对特定的技术用途提供特定的改进方案,那这套标准问题解决方案经过反复使用和验证之后,就会成为通用模式,这个时候它就变成了一个新的技术架构,我们就把它叫做标准工程。
标准工程对创新带来的贡献是非常大的,他和重新域定带来创新的方式却又是非常不一样的。标准工程是对现有技术带来的标准问题提供标准的解决方案,然后,随着经验的累积产生一些微小的进步。这些微小的进步聚集在一起,被反复使用。那通过这个过程促使技术随着时间变化和进步。所以说标准工程也可以说是由微小进步的聚集所带来的缓慢创新。在20世纪初的时候,瑞士的工程师吗雅尔他就用标准工程这个创新方法创造了一系列的大桥。
这些桥采用的都是通用技术,根本不新鲜。比如说她用的材料就是常见的钢筋混凝土,而它采用的结构是当时被广为接受的形式,也就是由固定在桥拱上的构建来支撑桥面。马雅尔所做的创新,仅仅是在普通的材料和普通的结构形式之上,进行了一个微小的改进。他在桥面上加了一个平板,放在了桥拱顶部,然后,把平板的两端固定在桥墩上。这样一来,整个桥所承担的重量就被更均匀地分散在了整个结构上。这个解决方案同时也让桥拱和桥面的重量很轻,但是却有足够的强度。马雅尔的这个创新性的方案就让整个设计变得既有效又经济。就是这些普通的技术要素组合在一起却能够使得整体上呈现出了一种流动和和谐。建筑评论家就说,马雅尔建造的大桥是有史以来最美丽的混凝土大桥。这座大桥不仅是一个技术产品,更是一件艺术品。标准工程这个创新机制,最重要的不在于天才的创想,而是来自于知识和技能经年累月的积累。这里,又产生了一个非常重要的结论,那就是创新就是关于解决方案的微小改进。即使是最日常性的项目,也是能够通过微小改进发现其中值得创新的部分的。标准工程就是一个对知识深度理解和认识的过程,正是这种深层的知识储备带来了崭新的创想。
小结,技术的进化怎么推动了经济的进化?作者认为,技术和经济的发展并不是完全由之前的发展决定,但是也不是完全不依赖之前的发展。当技术的发展选择的路径是重新域定的时候,那他就更倾向于在新的世界当中把旧的任务不断进行重新表达,以此来完成创新,而当技术的发展选择的路径是标准工程的时候,那他就更倾向于在微小的改进当中发现值得创新的部分。
《技术的本质》这本书的主要内容分为三个部分:第一部分,技术是什么?技术从本质上来讲具有什么特性。第二部分,技术是如何进化的,即新技术是怎么产生的。第三部分,技术的进化是怎么推动经济的进化的,也就是说活生生的技术怎么让经济也启动了自身生生不息的繁衍。其实,不论是认为技术控制了人类的生活,还是认为技术服务了人类的生活,这两种观点都是对的。没有技术就没有人类,反之,没有人类技术的存在也就没有了意义。人类需要的不只是经济上的舒适;人类,更需要的是挑战,是目的,是意义,需要和自然融为一体。如果技术把人类与自然分离,就消灭了人性,如果技术加强了人类和自然的联系,那就更好地确立了人性。有句话是这么说的:佛陀与上帝居住在计算机的电路里,或者是周期转动的齿轮当中,就和他们居住在群山之巅或者是盘坐在莲花上同样舒服。可以说,技术是自然的拓展,是人性的拓展,是更深的宇宙法则的一部分。了解技术的本质,你才能掌握未来。自然带给人类的天性和无穷的奥秘,正是我们创意的源头,也是我们赖以生存的本源。
]]>作者: Robert Griesemer,Ian Lance Taylor
本文基于我们在 GopherCon 2021 上的讲话(视频地址:https://www.youtube.com/watch?v=Pa_e9EeCdy8 , B站: https://www.bilibili.com/video/BV1V34y1q7wP):
Go 1.18版本增加了对泛型的支持。泛型是我们自 Go 第一个开源版本以来做出的最大改变。在本文中,我们将介绍新的语言特性。这将不会试图涵盖所有的细节,但我们会点出所有重要的点。更详细的内容,以及许多例子,请参见提案文件。关于语言变化的详细描述,请看更新的语言规范。(请注意,实际上 Go 1.18 的实现对提案文件所允许的内容施加了一些限制,该规范应该是描述准确的。未来的版本可能会取消这部分限制)。
泛型是一种编写独立于正在使用的特定类型的代码的方式。那么,现在可以编写用于任何类型的函数或类型。
泛型为语言增加了三个新的重要内容:
目前,函数和类型已支持类型参数。一个参数类型列表看起来就像一个普通的参数列表,只是它用方括号代替了小括号。
为了方便说明这一点,让我们从一个基本的非泛型的浮点值 Min
函数开始吧。
func Min(x, y float64) float64 { if x < y { return x } return y}
我们可以通过添加一个类型参数列表来使这个函数泛型化 —— 使其适用于不同的类型。在这个例子中,我们增加了一个由一个类型参数 T
构成的参数列表,并用 T
替换 float64
的使用。
func GMin[T constraints.Ordered](x, y T) T { if x < y { return x } return y}
那么,现在可以用一个类型参数来调用这个函数了,调用方式如
x := GMin[int](2, 3)
向 GMin
传入类型参数,在本例中是 int
,称为实例化。实例化分为两个步骤。首先,编译器在整个泛型函数或类型中把所有的类型参数替换成它们各自的类型参数。第二,编译器验证每个类型参数是否满足各自的约束条件。我们很快就会知道这意味着什么,但是如果第二步失败,实例化就会失败,程序就会无效。
实例化成功后,我们将有一个非泛型函数,可以像其他函数一样被调用。例如,在代码中就是这样
fmin := GMin[float64]m := fmin(2.71, 3.14)
GMin[float64]
的实例化产生了一个有效的原始浮点 Min
函数,我们可以在函数调用中使用它。
类型参数也可用于类型。
type Tree[T interface{}] struct { left, right *Tree[T] value T}func (t *Tree[T]) Lookup(x T) *Tree[T] { ... }var stringTree Tree[string]
在这里,泛型类型 Tree
存储的是类型参数 T
的值。泛型类型可以有方法,比如本例中的 Lookup
方法。为了使用一个泛型,它必须被实例化;Tree[string]
是一个用类型参数 string
来实例化 Tree
的例子。
让我们深入了解一下可以用来实例化一个类型参数的类型参数。
一个普通的函数对每个值参数都有一个类型;该类型定义了一组值。例如,如果我们有一个 float64
类型,就像上面非泛型函数 Min
那样,允许的参数值集合是可以用 float64
类型表示的浮点值集合。
同样地,类型参数列表中的每个类型参数都有一个类型。因为一个类型参数本身就是一个类型,所以类型参数的类型定义了类型的集合。这种元类型被称为类型约束。
在泛型函数 GMin
中,类型约束是从约束包中导入的。Ordered
约束描述了所有具有可排序值的类型的集合。换句话说,也就是用 < 操作符(或 <= , > , 等)进行比较。该约束确保只有具有可排序值的类型才能被传递给 GMin
。这也意味着在 GMin
函数体中,该类型参数的值可以被用于比较操作符 < 的运算。
在 Go 中,类型约束必须是接口。也就是说,一个接口类型可以作为一个值类型,也可以作为一个元类型。接口定义了方法,所以显然我们可以描述方法存在的类型约束。但是 constraints.Ordered
也是一个接口类型,而且 < 操作符不是一个方法。
为了实现这个,我们以一种新的方式来看待接口。
直到最近,Go 规范描述:一个接口定义了一个方法集,大概就是接口中列举的方法集。任何实现了所有这些方法的类型都实现了该接口。
但另一种看法是,接口定义了一个类型集,即实现这些方法的类型。从这个角度来看,任何属于接口类型集的元素的类型都实现了接口。
这两种观点达到了一样的结果。对于每一组方法,我们可以想象出实现这些方法的相应类型集,这就是接口所定义的类型集。
不过对于我们的目的来说,类型集视角比方法集视角有一个优势:我们可以明确地将类型添加到集合中,从而以新的方式控制类型集。
为了发挥作用,我们对接口类型的语法进行了扩展。例如,interface{ int|string|bool }
定义了包含 int
、string
和 bool
的类型集。
另一种说法是,这个接口只被 int
、string
或 bool
所满足。
我们来看一下 contraints.Ordered
的实际定义:
type Ordered interface { Integer|Float|~string}
这个声明表示,Ordered
接口是所有整数、浮点数和字符串类型的集合。|
表示的是类型的联合(或者是类型的集合)。 Integer
和 Float
是接口类型,在 constraints
中也有类似的定义。注意,Ordered
接口没有定义任何方法。
对于类型约束,我们通常不关心一个特定的类型,比如 string
;我们对所有的字符串类型感兴趣。这就是 ~
标记的作用。表达式 ~string
意味着所有基础类型为 string
的类型的集合 —— 这包括 string
类型本身,以及所有用定义声明的类型,如 type MyString string
。
当然,我们仍然希望在接口中指定方法,而且我们希望能向后兼容。在 Go 1.18 中,一个接口可以像以前一样包含方法和嵌入接口,但它也可以嵌入非接口类型、联合体和底层类型的集合。
当作为类型约束使用时,由接口定义的类型集准确地指定了允许作为相应类型参数的类型参数的类型。在一个泛型函数体中,如果操作数的类型是带有约束 C
的类型参数 P
,那么如果操作被 C
的类型集中的所有类型所允许,那么操作就是被允许的(目前这里实现上有一些限制,但是普通代码不太可能遇到这些限制)。
用作约束条件的接口可以被赋予名称(比如 Ordered
),或者它们可以是类型参数列表中的字面接口。比如说:
[S interface{~[]E}, E interface{}]
这里 S
必须是一个切片类型,其元素类型可以是任何类型。
因为这是一种常见的情况,对于处于约束位置的 interface{}
,可以省略。可以简单如下编写:
[S ~[]E, E interface{}]
因为空接口在类型参数列表中很常见,在普通 Go 代码中也是如此,Go 1.18 引入了一个新的预先声明的标识符 any
作为空接口类型的别名。这样一来,我们就得到了这个惯用代码:
[S ~[]E, E any]
接口用于类型集是一个强大的新机制,也是使类型约束在 Go 中发挥作用的关键。目前,使用新语法形式的接口只能作为约束使用。但不难想象,具有明确的类型约束的接口将会非常有用。
最后一个新的主要语言特性是类型推导。在某些方面,这是最复杂的变化,但它很重要,因为它让人们可以通过一种自然的风格来编写调用泛型函数的代码。
有了类型参数,就需要传递类型参数,这就使代码变得冗长。回到我们的泛型函数 GMin
。
func GMin[T constraints.Ordered](x, y T) T { ... }
类型参数 T
用于指定普通泛型参数 x
和 y
的类型。正如我们前面所看到的,可以用一个显式类型参数来调用它
var a, b, m float64m = GMin[float64](a, b) // 显式类型参数
在许多情况下,编译器可以从普通参数中推断出 T
的类型参数。这使得代码更简短。
var a, b, m float64m = GMin(a, b) // 没有 类型参数
其工作原理是将参数 a
和 b
的类型与参数 x
和 y
的类型相匹配。
这种从函数的参数类型中推断出参数类型的推导,被称为函数参数类型推导。
函数参数类型推导只适用于在函数参数中使用的类型参数,不适用于只在函数结果中使用的类型参数或只在函数主体中使用的类型参数。例如,它不适用于像 MakeT[T any]() T
这样的函数,它只在结果中使用 T
。
Go 还支持另一种类型推导,即约束类型推理。为了描述这个,让我们从这个缩放整数切片的例子开始:
// Scale 返回s中每个元素都乘以c的副本。// 这种实现方式有一个问题,后面将会看到。func Scale[E constraints.Integer](s []E, c E) []E { r := make([]E, len(s) for i, v := range s { r[i] = v * c } return r}
这是一个适用于任何整数类型切片的泛型函数。
现在假设我们有一个多维的 Point
类型,其中每个 Point
都是一个给出了点的坐标的简单整数列表。当然,这个类型有一些方法。
type Point []int32func (p Point) String() string { // Details not important.}
有时候,我们想对一个 Point
进行缩放。因为一个 Point
只是一个整数切片,我们可以使用我们之前写的 Scale
函数:
// ScaleAndPrint 将一个 Point 翻倍,并且打印。func ScaleAndPrint(p Point) { r := Scale(p, 2) fmt.Println(r.String()) // 无法编译}
然而,这并没有被编译,而是出现了这样的错误 r.String undefined (type []int32 has no field or method String)
.
问题是 Scale
函数返回了一个 []E
类型的值,其中 E
是参数切片的元素类型。当我们用一个 Point
类型的值调用 Scale
时,其底层类型是 []int32
,我们得到的是 []int32
类型的值,而不是 Point
类型。这是由泛型编程的写法决定的,但这不是我们想要的。
为了解决这个问题,我们必须改变 Scale
函数,使用一个类型参数来表示分片类型。
// Scale 返回s中每个元素都乘以c的副本。func Scale[S ~[]E, E constraints.Integer](s S, c E) S { r := make(S, len(s) for i, v := range s { r[i] = v * c } return r}
我们引入了一个新的类型参数 S
,它是分片参数的类型。我们对它进行了约束,使其底层类型是 S
而不是 []E
,结果类型现在是 S
。由于 E
被限制为整数,其效果与之前相同:第一个参数必须是某个整数类型的片断。函数主体的唯一变化是,现在我们在调用 make
时传递 S
,而不是 []E
。
如果我们用一个普通的片断来调用它,新函数的作用和以前一样,但是如果我们用 Point
类型来调用它,我们现在得到一个 Point
类型的值。这就是我们想要的。有了这个版本的 Scale
,先前的 ScaleAndPrint
函数就会像我们期望的那样编译和运行。
但是我们可以问:为什么写对 Scale
的调用可以不传递明确的类型参数?也就是说,为什么我们可以写 Scale(p, 2)
,没有类型参数,而不是必须写 Scale[Point, int32](p, 2)
?我们的新 Scale
函数有两个类型参数, S
和 E
。在调用 Scale
时没有传递任何类型参数,上面描述的函数参数类型推理让编译器推断出 S
的类型参数是 Point
。但是该函数也有一个类型参数 E
。编译器推断出 E
的类型参数是切片的元素类型的过程被称为约束类型推理。
约束类型推理从类型参数约束中推断出类型参数。当一个类型参数有一个定义在另一个类型参数上的约束时,它就会被使用。当这些类型参数中的一个的类型参数是已知的,该约束被用来推断另一个的类型参数。
通常适用的情况是,当一个约束对某些类型使用 ~ type
的形式时,该类型是用其他类型参数写的。我们在 Scale
这个例子中看到了这一点。S
是 ~[]E
,它是 ~
后面有一个用另一个类型参数写的 []E
类型。如果我们知道 S
的类型参数,我们可以推断出 E
的类型参数。S
是一个切片类型,E
是该切片的元素类型。
这只是对约束类型推导的一个介绍。完整的细节请参见提案文档文件或语言规范。
类型推理的详细过程很复杂,但使用并不复杂:类型推理要么成功要么失败。如果它成功了,类型参数可以被省略,调用泛型函数看起来与调用普通函数没有什么不同。如果类型推理失败,编译器则会给出一个错误信息,在这种情况下,我们直接提供必要的类型参数就可以。
在向语言添加类型推理时,我们在试图平衡推理能力和复杂性。我们希望确保当编译器推断出类型时,这些类型永远不会令人困惑。我们试图小心翼翼地站在未能推断出类型的一边,而不是站在推断出错误类型的一边。我们可能没有完全做到这一点,可能会在后续版本中继续完善它。其效果是,更多的代码可以在没有显式类型参数的情况下编写。当然,今天不需要类型参数的代码,明天也不会需要。
泛型是 1.18 中一个很大的新语言特性。这些新的语言变化需要大量的新代码,这些代码还没有在生产环境中进行过大量的测试。这只会随着越来越多的人编写和使用泛型代码来进行。我们相信这个功能实现得很好,质量很高。然而,与 Go 的大多数方面不同,我们无法用现实的经验来支持这一信念。因此,虽然我们鼓励在有意义的地方使用泛型,但在生产中部署泛型代码时,请使用适当的谨慎措施。
以外,我们也也很高兴能提供泛型,并希望能使 Go 程序员的工作更高效。
]]>准备工具:
创建一个空目录,并通过命令行进入此空目录。
执行以下命令
vagrant init alvistack/kubernetes-1.22
打开自动创建的 Vagrantfile 文件。增加 config.vm.network "public_network", bridge: "enp3s0"
配置。其中 enp3s0
为本机网卡。此配置用于桥接 enp3s0
网卡。如果需要使用 hostonly 模式的网卡,可修改 config.vm.network "private_network"
对应的配置。
在此目录下执行
vagrant up
替换以下脚本中 NEW_IP
参数为本机IP地址,并在 root 账号下执行该脚本。
export NEW_IP=192.168.16.101cd /etc/kubernetes/pkiopenssl x509 -noout -text -in apiserver.crtopenssl genrsa -out apiserver.key 2048openssl req -new -key apiserver.key -subj "/CN=kube-apiserver" -out apiserver.csrcat > apiserver.ext << EOFsubjectAltName = DNS:kubernetes, DNS:kubernetes.default, DNS:kubernetes.default.svc, DNS:kubernetes.default.svc.cluster.local, DNS:ubuntu, IP:10.233.0.1, IP:10.0.2.15, IP:${NEW_IP}EOFopenssl x509 -req -in apiserver.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out apiserver.crt -days 3650 -extfile apiserver.extopenssl x509 -noout -text -in apiserver.crt
重启服务器
本文使用Vagrant和VirtualBox组合,能够快速启动K8s服务供应用中心开发者或其他需要使用到K8s服务的同学们使用。
]]>注:如果你要评论这篇文章的话,请你附上持有加密货币的数量。
从今年的5月1日开始,soucehut 的 CI 服务开始收费,这一改变影响了大约一半的 builds.sr.ht 的用户。在过去的几个月里,业内所有提供任何形式的 CPU 计算资源的服务商都在处理大规模爆发的滥用加密货币挖矿行为。行业服务商们还专门一起成立了非正式的工作组,互相交流,避免在应对这种 CPU 计算资源滥用事情上浪费太多的时间。
加密货币发明了一个全新的互联网资源滥用类别。而且资源滥用的形式也是非常之多:JavaScript挖矿程序、僵尸网络以及各种各样用以解决一些无意义的运算问题的非法循环。有人会说,对于任何一个提供免费的公共服务来说滥用是不可避免的。但是,在加密货币出现以前,有一些什么样的滥用行为呢?比如:垃圾邮件亦或是在一些虚拟机上挂载站点。然而在加密货币出现以后,就有人找到了将免费CPU计算资源转化成利益的方式,导致大家就不能正常使用这些算力进行正常的服务了,比如自动构建应用等。如果不是因为加密货币的出现,这些服务仍然可用。
你可别以为这些人都是一些“脚本小子”。他们有一个人数众多的高智商团队,有各种各样的滥用资源策略,比如就我所知的有:
其实还有着很多很多的骚操作来滥用这些资源的,但是我得为一些服务商保密这其中的细节,甚至是大概都不能说,这些黑客脑动太大了,稍微提示一下他们就能想出各种各样的法子。
而加密货币本身的问题则比资源滥用更加严重。正是因为加密货币的出现,整个软件行业的诚信和信任度急剧下降。加密货币给新项目设立了不正当的奖励机制,太多的公司打着加密货币的幌子出来招摇撞骗。到了现在,一项新的有前途的技术出现时候,我不免有一种越来越强烈的恐惧感,总会想:“这会不会又是一个扯淡的加密货币项目?” 加密货币是一种科技界的多层营销模式。“嘿!好久不见啊。我一直在研究一种很酷的分布式数据存储技术,我们下周要做一个ICO。” 然后我就径直走开了。其实很多项目不使用区块链技术就能够运行得很好。
有数以百计甚至是数以千计得加密货币骗局和庞氏骗局伪装得跟个合法产品一样。即使是你正在做的项目相当的不错,但是可能就有上百个项目正在抄袭你的项目,然后通过各种各样的方式设法把用户的钱搞到创始人那里去。你觉得投资者更有可能投资哪一个呢?很明显是那个更有利可图的项目。
这就是加密货币的全部内容:不是新技术,不是赋权,而是赚钱。除了在一些落败的经济体独立的国家之外,加密货币作为实际货币显然是已经失败了。不,加密货币根本就不是一种货币:它是一种投资工具。一个让富人更富有的工具。在现实中,加密货币与庞氏骗局有着很多共同点,而不是真正的投资。加密货币所谓的解决数学问题(一些无意义的数学问题),而实际上为谁提供了什么 “价值”呢?简直就是个扯淡的事。
还有那几个失败的经济体,他们的人民拼命使用加密货币来维持他们命运之轮的转动?这倒是一个非常不错的标题,而在那些落后的农村,他们的税收补贴政策正好吸引了矿工们蜂拥而至,真好补贴了矿工们建发电厂。那里的人们正在遭受停电的事,因为他们的电力都被矿工们的矿场吸走了。同时还排放大量的二氧化碳。
即使是世界最大的经济体也难逃加密货币带来的影响。在过去的几个月里,显卡价格急剧上升。顺便说一下,显卡的用途是什么?循环计算SHA-256,速度越快越好。然而不仅仅是显卡的价格,CPU的价格,甚至是硬盘的价格都受到了影响。
或许你会说你的加密货币是不同的。但是你看,你就这么一个小破公司。就好比你身处一个只有你一个人诚实守信的环境中,那你就得换个环境了。网上那些对加密货币的评论大多来自于一些对加密货币有大量投资的人,他们喜欢通过他们的言论来改变大众对加密货币的看法并且引诱你们去入坑,其本质目的在于投资加入这场庞氏骗局,然后骗你们从中捞取钱财。更不用说任何改革的尝试,如股权证明,都会被那些当权者(即那些有钱的人)恶狠狠地阻止,因为这对减少他们的底线有风险。于是,你的区块链并没有什么不同。
可以说,加密货币是21世纪最糟糕的发明之一。我为与这种剥削性的骗局共处一个行业而感到羞耻。加密货币并没有称为一种有用的货币,然而却发明了一种新的互联网资源滥用行为,进一步加剧了贫富差距,浪费大量的电力,还加速了气候变化,也毁掉了大量非常有前途的项目,也为很多骗局项目提供了蓬勃发展的环境,造成了硬件供货短缺和价格上涨。
]]>为了这个实验的目的,我选择了最新的Windows 10 Insider Build(截至2021年2月6日),即Windows 10 build 21301。
所以,来不及解释了,赶紧上车。
首先,我们从最新和最伟大的,流畅设计开始。流利设计于2017年宣布,并与Windows 10 1803更新一起推出,是现代设计语言2(MDL2)的重大重新设计,旨在带来光、深度、运动、材料和规模等元素。它还引入了揭幕效果和亚克力半透明背景。
截至目前,大多数收件箱(UWP)应用程序已经升级,以利用Fluent元素,以及一些更面向前台的元素,如开始菜单、活动中心和登录屏幕。
尽管”Fluent Design”得到了好评,但大多数爱好者认为此举太少、太晚了,因为只有一小部分功能通过这种新的设计风格得到了加强。
就在我们深入挖掘操作系统的时候,我们可以看到自Windows 8/8.1以来没有升级过的元素。
其中一些是明显的遗漏,如音量、USB弹出以及登录屏幕的一些元素。
其他Metro元素,虽然没有那么突出,但有开机画面(很快将被更新的画面取代)和WinRE。
你知道吗:第一次引入旋转圆点是在Windows 8 build 7989。
就像Windows 10一样,Windows 8也被不一致的问题所困扰(无论好坏)。然而,Windows 8为主要的用户元素增加了有意义的改进,如Windows Explorer或任务管理器。虽然它们在随后的Windows 10更新中会得到一些生活质量的改善,但变化是最小的。
此外,Windows 8带来的一个重要变化是重新设计了文件传输对话框。
其中一些变化从Windows 7开始,这使我们来到了第四层。
毫无疑问,Windows 7是有史以来最受喜爱的Windows版本之一,因其在Windows Vista基础上的巨大提升而受到称赞。它带来了许多新功能,虽然没有Vista引入的那些功能那么重要,但使Windows 7成为一个非常坚实的操作系统,是Windows XP的真正继承者。然而,Windows 7带来的最臭名昭著的变化之一是Ribbon UI,这是一个从Office 2007移植的功能。用新的Ribbon UI更新的一些应用程序是Paint和Wordpad。
虽然在某些时候,微软决定废弃经典的画图,而采用新的画图3D(与Windows 10创意者更新一起推出),但在重大的反响之后,他们推翻了自己的决定。
其他在Windows 7中更新并一直保持不变的功能有:Windows Media Player 12、远程桌面连接和一些文件对话框。
Windows Vista是一个具有纪念意义的Windows版本,它给平台带来了急需的现代化。几乎所有操作系统的基本要素都以某种方式得到了改进,从启动器到驱动模型。然而,正如我们现在都知道的,Windows Vista将成为有史以来最糟糕的Windows版本之一,从一开始就被各种问题所困扰。不过,为数不多的被称赞的功能之一是用户界面。它重新设计了一些自Windows 95以来没有更新过的基本要素。这一变化的主要推动者之一是引入了所谓的Aero Wizards,取代了以前的Wizard标准,即Wizard97。
在Windows Vista中重新设计的其他功能,在Windows 10中基本相同:控制面板、搜索程序、Windows传真和扫描。
说到 Windows Vista:你知道在某些特殊情况下,Windows 10会退回到Vista的启动画面吗?比如当你的显卡不支持标准启动屏幕使用的视频模式时,就会发生这种情况。
信不信由你,没有那么多XP元素嵌入到Windows 10中。这可能是因为大多数基本要素已经被更新到Windows 2000中。然而,Windows 10包含一些来自XP的文件对话框,在安装驱动程序时可以看到这些对话框。
Windows 2000是微软NT系列操作系统的一个重要里程碑。它也是一块垫脚石,标志着开始向新的、统一的Windows愿景过渡。然而,Windows 2000仍然是一个面向商业的操作系统,这意味着它带来了许多为专家设计的新功能。
最重要的一个新增功能就是管理控制台(MMC),其UI元素自那时起,至今几乎没变。
在Windows 2000中引入的另一个功能是Windows安装程序,它的图标仍然与最初时一样。
还有另一个没有变化过的UI元素(当然除了品牌效应)是winver,它的设计是在Windows 2000 build 1946中被引入的。
虽然Windows 2000引入了许多针对高级用户的功能,但Windows 95可能是迄今为止最重要的Windows版本。它所引入的基本范式至今仍然有效。它引入了诸如开始菜单、上下文菜单、任务栏和回收站等功能。虽然这些功能经过多年的更新,但有些功能几乎完全保持不变。
一个基本上是老式计算习惯的遗留物的元素,即人们必须保护他们宝贵的CRT屏幕,这就是屏保设置。
另一个惊人相似的元素是“运行”对话框。
还有一个通过时间考验的常见UI元素是文件夹属性窗口。
还有很多UI元素自Windows 95以来就没有被碰过。这是一个永恒的设计的案例吗?
好吧,这其实算不上真正的“UI层”,因为我还没在Windows 10中发现Windows 95以前的任何界面元素(尽管我感觉肯定有)。然而,在Windows 10中有一个奇特的文件,叫做 moricons.dll,它包含了很多DOS时代的老图标。
嗯,这么多就是这样。可能你也知道了,微软正计划用他们的“太阳谷”更新来彻底重构Windows系统的用户界面,目的是统一操作系统的设计。然而,正如我们今天所看到的,Windows是一个庞大的操作系统。他们为最终形成一个统一的用户体验所做的努力会成功吗?让我们拭目以待吧。
谢谢您的关注。