SegmentFault 一起开启技术漂泊之旅最新的文章
2023-06-07T17:34:33+08:00
https://segmentfault.com/feeds/blogs
https://creativecommons.org/licenses/by-nc-nd/4.0/
在 Golang 中执行 Shell 命令
https://segmentfault.com/a/1190000043875324
2023-06-07T17:34:33+08:00
2023-06-07T17:34:33+08:00
宇宙之一粟
https://segmentfault.com/u/yuzhoustayhungry
0
<p>原文标题:<a href="[Executing" title="Shell Commands in Golang (sohamkamani.com">Executing Shell Commands in Golang</a>](<a href="https://link.segmentfault.com/?enc=iDy9%2BGvMyeV9M9xR9eanHQ%3D%3D.RsT%2BL5Q4WtE6l3iv%2BSrQjUVWrJK1pk%2FI5fOVsOIbrIgqFA55n7aob0MBxCm60Wovnx5AeNS5HC0hK5sqoTmN9w%3D%3D" rel="nofollow">https://www.sohamkamani.com/golang/exec-shell-command/</a>))</p><p>作者:Soham Kamani</p><blockquote>之前自己也写过 <code>os/exec</code> 包执行 Shell 命令的文章,但是没有这篇讲的详细,感兴趣可以看看,点<a href="https://link.segmentfault.com/?enc=k594wpjFXKbP%2F0Rb%2Bc0sEw%3D%3D.ZprwZZZhmjr8WJtXCcizsGUH0%2B0sbnROIiCtul4kMfwL2N1UV4b139JfSmVHBXD5" rel="nofollow">此处</a>。</blockquote><p>在本教程中,我们将学习如何在 Golang 中执行shell命令(如 <code>ls</code>、<code>mkdir</code> 或 <code>grep</code> )。我们还将学习如何通过 <code>stdin</code> 和 <code>stdout</code> 传递 I/O 到正在运行的命令,以及管理长时间运行的命令。</p><p><img src="/img/remote/1460000043875326" alt="banner.drawio_hubba78aceeb57281c0399677d5d745a8f_105444_720x0_resize_q75_h2_box_3.webp" title="banner.drawio_hubba78aceeb57281c0399677d5d745a8f_105444_720x0_resize_q75_h2_box_3.webp"></p><blockquote>如果只是想看代码,可以在 <a href="https://link.segmentfault.com/?enc=sIBg2PUTezwFniOMKEEl3g%3D%3D.NBp9ZIZrd2by%2BPIegUvoaZNrGMOSPy2YSgMUruYxBs8D1MwiKb87Mo0tiyR9dnrnXCeu7oNKypvp3PtyGN1%2Bbe3m3HhRTgXB0E7jI7RcGAg%3D" rel="nofollow">Github</a> 上查看</blockquote><h2><code>Exec</code> 包</h2><p>我们可以使用官方的 <a href="https://link.segmentfault.com/?enc=%2Fpc2IaTO7amZZTLhhdzo5Q%3D%3D.W%2FlPMT7RkFxh3cx1R9rf%2Be%2FWyJPBQt3pGmefUcbArYw%3D" rel="nofollow">os/exec</a> 包来运行外部命令。</p><p>当我们执行 shell 命令时,我们是在 Go 应用程序之外运行代码。为此,我们需要在子进程中运行这些命令。</p><p><img src="/img/remote/1460000043875327" alt="child-process-thread.drawio.svg" title="child-process-thread.drawio.svg"></p><p>每个命令都作为正在运行的 Go 应用程序中的子进程运行,并公开我们可以用来从进程读取和写入数据的 <code>Stdin</code> 和 <code>Stdout</code> 属性。</p><h2>运行基本的 Shell 命令</h2><p>要运行一个简单的命令并读取其输出,我们可以创建一个新的 <code>*exec.Cmd</code> 实例并运行它。在此示例中,让我们使用 <code>ls</code> 列出当前目录中的文件,并打印代码的输出:</p><pre><code class="go">// 创建了一个新的 *Cmd 实例
// 使用 "ls" 命令和 "./" 参数作为参数
cmd := exec.Command("ls", "./")
// 使用 `Output` 方法执行该命令并收集其输出
out, err := cmd.Output()
if err != nil {
// 如果执行命令时出现错误,则输出错误信息
fmt.Println("could not run command: ", err)
}
// 否则,输出运行该命令的输出结果
fmt.Println("Output: ", string(out))
</code></pre><p>由于我在<a href="https://link.segmentfault.com/?enc=iZgtZaZDvXZkd2zApWRA0Q%3D%3D.bh4t%2Bqcm1awT9qrOeWu1sN1mOhqnFnQFbnLbLV4cQAyZj%2BmuUHs2LMxj308p5%2BVz" rel="nofollow">示例仓库</a>中运行此代码,因此它会打印项目根目录中的文件:</p><pre><code class="shell">> go run shellcommands/main.go
Output: LICENSE
README.md
go.mod
shellcommands
</code></pre><p><img src="/img/remote/1460000043875328" alt="cmd-output-execution.drawio.svg" title="cmd-output-execution.drawio.svg"></p><p>请注意,当我们运行 <code>exec</code> 时,我们的应用程序不会生成 shell,而是直接运行给定的命令。这意味着将不会执行任何基于 shell 的处理,例如 <em>glob</em> 模式或扩展。</p><h2>执行持久运行的命令</h2><p>前面的示例执行了 <code>ls</code> 命令,该命令立即返回了它的输出。那些输出是连续的,或者需要很长时间才能检索的命令呢?</p><p>例如,当我们运行 <code>ping</code> 命令时,我们会定期获得连续输出:</p><pre><code>➜ ~ ping google.com
PING google.com (142.250.77.110): 56 data bytes
64 bytes from 142.250.77.110: icmp_seq=0 ttl=116 time=11.397 ms
64 bytes from 142.250.77.110: icmp_seq=1 ttl=116 time=17.646 ms ## this is received after 1 second
64 bytes from 142.250.77.110: icmp_seq=2 ttl=116 time=10.036 ms ## this is received after 2 seconds
64 bytes from 142.250.77.110: icmp_seq=3 ttl=116 time=9.656 ms ## and so on
# ...
</code></pre><p>如果我们尝试使用 <code>cmd.Output</code> 执行此类型的命令,我们将不会得到任何输出,因为 <code>Output</code> 方法等待命令执行,而 <code>ping</code> 命令执行时间无限。</p><p>相反,我们可以使用自定义的 <code>Stdout</code> 属性来连续读取输出:</p><pre><code class="go">cmd := exec.Command("ping", "google.com")
// pipe the commands output to the applications
// standard output
cmd.Stdout = os.Stdout
// Run still runs the command and waits for completion
// but the output is instantly piped to Stdout
if err := cmd.Run(); err != nil {
fmt.Println("could not run command: ", err)
}
</code></pre><blockquote>这段代码使用 Go 语言的 <code>exec</code> 包来执行 <code>ping</code> 命令并将输出重定向到标准输出流(<code>os.Stdout</code>)。具体来说,它创建了一个命令对象(<code>cmd</code>),该对象包含要执行的命令("ping"和"google.com")。然后将命令的标准输出流(<code>cmd.Stdout</code>)设置为应用程序的标准输出流(<code>os.Stdout</code>)。最后,使用 <code>cmd.Run()</code> 方法运行该命令,并等待其完成。如果运行命令时出现错误,将在控制台输出错误信息。</blockquote><p>输出结果:</p><pre><code>> go run shellcommands/main.go
PING google.com (142.250.195.142): 56 data bytes
64 bytes from 142.250.195.142: icmp_seq=0 ttl=114 time=9.397 ms
64 bytes from 142.250.195.142: icmp_seq=1 ttl=114 time=37.398 ms
64 bytes from 142.250.195.142: icmp_seq=2 ttl=114 time=34.050 ms
64 bytes from 142.250.195.142: icmp_seq=3 ttl=114 time=33.272 ms
# ...
# and so on
</code></pre><p>通过直接分配 <code>Stdout</code> 属性,我们可以捕获整个命令生命周期的输出,并在收到后立即处理。</p><p><img src="/img/remote/1460000043875329" alt="cmd-stdout-workflow.drawio.svg" title="cmd-stdout-workflow.drawio.svg"></p><h2>自定义输出写入程序</h2><p>与使用 <code>os.Stdout</code> 不同,我们可以创建实现 <code>io.Writer</code> 接口的自己的编写器。</p><p>让我们创建一个编写器,在每个输出块之前添加一个 <code>"received output:"</code> 前缀:</p><pre><code class="go">type customOutput struct{}
func (c customOutput) Write(p []byte) (int, error) {
fmt.Println("received output: ", string(p))
return len(p), nil
}
</code></pre><p>现在我们可以指定一个新的 <code>customWriter</code> 实例作为输出写入器:</p><pre><code class="go">cmd.Stdout = customOutput{}</code></pre><p>如果我们现在运行应用程序,我们将得到以下输出:</p><pre><code>received output: PING google.com (142.250.195.142): 56 data bytes
64 bytes from 142.250.195.142: icmp_seq=0 ttl=114 time=187.825 ms
received output: 64 bytes from 142.250.195.142: icmp_seq=1 ttl=114 time=19.489 ms
received output: 64 bytes from 142.250.195.142: icmp_seq=2 ttl=114 time=117.676 ms
received output: 64 bytes from 142.250.195.142: icmp_seq=3 ttl=114 time=57.780 ms
</code></pre><h2>使用 <code>STDIN</code> 将输入传递给命令</h2><p>在前面的示例中,我们在不提供任何输入(或提供有限的输入作为参数)的情况下执行命令。在大多数情况下,输入是通过 <code>STDIN</code> 流给出的。</p><blockquote>译注:就是外部给命令,然后去执行</blockquote><p>一个著名的例子是 <code>grep</code> 命令,我们可以通过管道从另一个命令输入:</p><pre><code>➜ ~ echo "1. pear\n2. grapes\n3. apple\n4. banana\n" | grep apple
3. apple</code></pre><p>在这里,输入通过 <code>STDIN</code> 传递给 <code>grep</code> 命令。在本例中,输入是一个水果列表,<code>grep</code> 过滤包含 <code>" apple"</code> 的行。</p><p><code>Cmd</code> 实例为我们提供了一个可以写入的输入流。让我们使用它向 <code>grep</code> 子进程传递输入:</p><pre><code class="go">cmd := exec.Command("grep", "apple")
// Create a new pipe, which gives us a reader/writer pair
reader, writer := io.Pipe()
// assign the reader to Stdin for the command
cmd.Stdin = reader
// the output is printed to the console
cmd.Stdout = os.Stdout
go func() {
defer writer.Close()
// the writer is connected to the reader via the pipe
// so all data written here is passed on to the commands
// standard input
writer.Write([]byte("1. pear\n"))
writer.Write([]byte("2. grapes\n"))
writer.Write([]byte("3. apple\n"))
writer.Write([]byte("4. banana\n"))
}()
if err := cmd.Run(); err != nil {
fmt.Println("could not run command: ", err)
}
</code></pre><p>输出:</p><pre><code>3. apple</code></pre><p><img src="/img/remote/1460000043875330" alt="stdin.drawio.svg" title="stdin.drawio.svg"></p><h2>Kill 一个子进程</h2><p>有几个命令会无限期地运行,或者需要明确的信号才能停止。</p><p>例如,如果我们使用 <code>python3 -m http.server</code> 启动 Web 服务器或执行 <code>sleep 10000</code>,则生成的子进程将运行很长时间(或无限运行)。</p><p>要停止这些进程,我们需要从应用程序发送终止信号。我们可以通过向命令添加一个<a href="https://link.segmentfault.com/?enc=UB1FxudnUzdlA3H%2BxDiG6g%3D%3D.X9R7KGVF2c3OJm6PfYzH7ai99aRUyx5dyvnrC0JcpGJqC16YaWXdO7bgYesnDB0%2Fm3J%2Bc9ltZubJLjYTHGr3ileXSuNyJQK0HPPIVUDpEJE%3D" rel="nofollow">上下文实例</a>来做到这一点。</p><p>如果上下文被取消,命令也会终止。</p><pre><code class="go">ctx := context.Background()
// The context now times out after 1 second
// alternately, we can call `cancel()` to terminate immediately
ctx, cancel = context.WithTimeout(ctx, 1*time.Second)
cmd := exec.CommandContext(ctx, "sleep", "100")
out, err := cmd.Output()
if err != nil {
fmt.Println("could not run command: ", err)
}
fmt.Println("Output: ", string(out))
</code></pre><p>这将在 1 秒后给出以下输出:</p><pre><code>could not run command: signal: killed
Output: </code></pre><p>当您想要限制运行命令所花费的时间或想要创建回退以防命令未按时返回结果时,终止子进程很有用。</p><h2>总结</h2><p>到目前为止,我们学习了多种执行 unix shell 命令并与之交互的方法。使用 <code>os/exec</code> 包时需要注意以下几点:</p><ul><li>当您想要执行通常不会提供太多输出的简单命令时,请使用 <code>cmd.Output</code></li><li>对于具有连续或长时间运行输出的函数,您应该使用 <code>cmd.Run</code> 并使用 <code>cmd.Stdout</code> 和 <code>cmd.Stdin</code> 与命令交互</li><li>在生产应用程序中,如果某个进程在给定的时间内没有响应,那么保持超时并终止该进程是非常有用的。我们可以使用<a href="https://link.segmentfault.com/?enc=Dz4WajMUOjCGEI8sGp9%2F7w%3D%3D.s%2BhCl7N1KuMPqUdz76j%2FnikaPoeurEZjiV3BfCnNU8reRJmbryayw8TN%2BPVYzYM0LQLdsrUJrXvEwhe%2FV3SlG810g%2BlWDed1tS4dQt%2BxvJZ6ufz80jeKoWZvPq%2BE6wA9" rel="nofollow">上下文取消</a>发送终止命令。</li></ul><p>如果您想了解更多关于不同功能和配置选项的信息,可以查看<a href="https://link.segmentfault.com/?enc=Fz88ssTH9UhPYSX0f5l1GQ%3D%3D.5FM1cqs3cDtAJ9fR2Nc19yTzb15a4UGzvgLxuid7hp4%3D" rel="nofollow">官方文档页面</a>。</p><p>您可以在 <a href="https://link.segmentfault.com/?enc=fvAF5LxpEs7pB7mGN%2FwkQQ%3D%3D.Omlirg9fsgC9eB%2Baf4lijyB5XnRGVXZ4bGdnN6aEfa7CF6QkHrOq8RuqZI6EF2fljTdDeKaA%2Fi0PpQC%2BKceQXKo286pAXUn0ga667ZbBEpE%3D" rel="nofollow">Github</a> 上查看所有示例的工作代码。</p>
AWS CodeWhisperer 上手初体验安装与使用
https://segmentfault.com/a/1190000043871007
2023-06-06T18:05:41+08:00
2023-06-06T18:05:41+08:00
宇宙之一粟
https://segmentfault.com/u/yuzhoustayhungry
0
<h2>引言</h2><p>代码生成器最近有点多,为了提高效率最近在我的 VSCode 上先后安装了 Bito、Codeium、Tabnine AI Assistant 等等,还有本文中刚有 AWS 推出的 <a href="https://link.segmentfault.com/?enc=Kh2TlPXwNj03PrhXGhAbKg%3D%3D.HCQ%2FKrCeh%2FAqRQHb1kggX2Sr8dbK3rs3ATE2rECtT7OLFT9VaURpTiBFZqXvAkPDrzbB6xwQt5SZ%2F7G8y51OXI%2BOn4BQRZ5Tm95RCG0Q7enrjSMcbG3cVNGv7SY%2FW%2BzB" rel="nofollow">CodeWhisperer</a>。接下来就来看看这款 AI 代码生成器如何使用吧。</p><h2>介绍</h2><p>Amazon CodeWhisperer 是一款通用的、基于机器学习的代码生成器,可为您真实的提供代码建议。当你编写代码时,CodeWhisperer 会根据你现有的代码和注释自动生成建议。您的个性化建议可以在大小和范围上有所不同,从单行注释到完整的功能。、</p><p>CodeWhisperer 还可以扫描您的代码,以突出和定义安全问题。</p><p>目前 CodeWhisperer 支持了 15 种常见编程语言的代码生成,据官网介绍,就训练数据的质量来看,支持最好的语言是以下几种:</p><ul><li>Java</li><li>Python</li><li>JavaScript</li><li>TypeScript</li><li>C#</li></ul><p>不出意外,这款代码生成器未来肯定要收费,但是目前亚马逊云科技推出了测评活动,可以免费个人使用,注册然后在开发环境 VSCode、JupyterLab 或 JetBrains 设置好就能使用。(当然 AWS 自己的开发环境Amazon SageMaker、AWS Cloud9、AWS Lambda 也行,但是没有 AWS EC2 实例可不行,即使有,明人不说暗话,本人就是喜欢白嫖的)</p><p><img src="/img/bVc8eQK" alt="image.png" title="image.png"></p><p>活动链接,点击<a href="https://link.segmentfault.com/?enc=yfznUC83qNrLPlwEYmby9A%3D%3D.38Bx7YTc57b4GWRjI26j%2F6rxzxsdC%2FymJKR1V%2BapXNZkTu8df%2BPq3TzThyh0XkLOAZMzUNmo5NgmxzYUYEt9Pw%3D%3D" rel="nofollow">CodeWhisperer测评活动</a> 立马上车~</p><h2>VS Code 设置</h2><p>假设您的电脑上已经安装好 VS Code 编辑器,然后我们需要安装 AWS Toolkit,只有这样才能将 CodeWhisperer 与 VS Code 结合使用,在 VS Code 扩展中搜索 AWS 或者全称 AWS Toolkit,点击安装(Install),如下:</p><p><img src="/img/bVc8eSd" alt="image.png" title="image.png"></p><p>安装成功后就会在我们的编辑器的最左边出现 aws 图标,这个就是我们的工具了,如图:</p><p><img src="/img/bVc8eSj" alt="image.png" title="image.png"></p><p>在选择身份验证方式登录这里,针对本地编辑器 VS Code 和 JetBrains,我们可以通过使用 AWS Builder ID 或 IAM Identity Center 进行身份验证。</p><blockquote>将 CodeWhisperer 与 AWS Cloud9 或 AWS Lambda 配合使用,则必须使用 IAM 进行身份验证</blockquote><p>本文就是通过 AWS Builder ID 进行登录的。</p><h2>使用 AWS 生成器 ID 登录</h2><p>AWS Builder ID 是一种面向所有 AWS 客户的新的身份验证形式。AWS Builder ID 是免费的。您只需为自己在 AWS 账户 中使用的 AWS 资源付费。AWS Builder ID 允许访问 Amazon 上的工具 CodeCatalyst 和本文介绍的生成器服务 CodeWhisperer。</p><p><img src="/img/bVc8eSF" alt="image.png" title="image.png"></p><p>接着会弹出这个框,有一个随机的请求码,这里我们点击 <code>Copy Code and Proceed</code>:</p><p><img src="/img/bVc8eS3" alt="image.png" title="image.png"></p><p>接着会弹出到这个链接 <code>https://device.sso.us-east-1.amazonaws.com/</code>:</p><p><img src="/img/bVc8eTc" alt="image.png" title="image.png"></p><p>点击 Open,将打开默认浏览器,把上面随机的请求码复制进去:</p><p><img src="/img/bVc8eTg" alt="image.png" title="image.png"></p><p>点击 Next,VSCode 的左下角会弹出正在登录中的进度条,稍微等一会:</p><p><img src="/img/bVc8eTh" alt="image.png" title="image.png"></p><p>在浏览器中输入我们的邮箱地址:</p><p><img src="/img/bVc8eVG" alt="image.png" title="image.png"></p><p>然后输入密码,点击允许:</p><p><img src="/img/bVc8eVI" alt="image.png" title="image.png"></p><p>等浏览器出现这个界面,就说明登录和配置成功了:</p><p><img src="/img/bVc8eVJ" alt="image.png" title="image.png"></p><p>恭喜,接着让我们来探索 CodeWhisper 的代码生成能力吧!</p><h2>打开 CodeWhisperer 上手体验</h2><ol><li>在 VS Code 的 aws 的 DEVELOPER TOOLS -> CodeWhisperer,点击 Start:</li></ol><p><img src="/img/bVc8eWw" alt="image.png" title="image.png"></p><ol start="2"><li>新建一个 <code>hello.py</code> 程序,写下注释帮我们写一个 HelloWorld 程序,可以按下 <code>alt+c</code> 快捷键弹出提示:如下图:</li></ol><p><img src="/img/bVc8eWM" alt="image.png" title="image.png"></p><p>按下 <code>Tab</code> 键接受 CodeWhisper 的代码生成,最后生成的代码如下:</p><pre><code class="python"># generate a HelloWorld program with a function
def hello_world():
print("Hello World")
# call the function HelloWorld
hello_world()
</code></pre><p>其他快捷方式:</p><p><img src="/img/bVc8eXa" alt="image.png" title="image.png"></p><h3>单行代码补全</h3><p>当你开始输入单行代码或注释时,CodeWhisperer 会根据你当前和之前的输入来提出建议,比如说使用 ORM 通过 <code>book_id</code> 来删除一本书的函数,会这样给你提示:</p><p><img src="/img/bVc8eYz" alt="image.png" title="image.png"></p><h3>完整函数生成</h3><p>比如,我们想写一个读取 TXT 文件的函数:</p><p><img src="/img/bVc8eYI" alt="image.png" title="image.png"></p><pre><code class="python"># write a function to read a txt file
def read_txt(file_name):
with open(file_name, 'r') as f:
return f.read()
# main function
def main():
content = read_txt('a.txt')
print(content)
if __name__ == '__main__':
main()</code></pre><p>测试效果如下:</p><p><img src="/img/bVc8eYT" alt="image.png" title="image.png"></p><h3>代码块补全</h3><p>为了更好的写逻辑控制,比如 <code>if</code>、<code>for</code>、<code>while</code> 语句也能支持补齐:</p><p><img src="/img/bVc8eZc" alt="image.png" title="image.png"></p><pre><code>book_list = ["Head First Python", "Python Crash Course", "Python Cookbook"]
for i, v in enumerate(book_list):
if i == 0:
print("The first book is", v)
elif i == 1:
print("The second book is", v)
else:
print("The third book is", v)
</code></pre><p>运行测试:</p><p><img src="/img/bVc8eZf" alt="image.png" title="image.png"></p><h3>其它代码示例</h3><p>除了上面演示的基本三种补全方式,还支持在 Java 代码中,用户输入了一个文档字符串。CodeWhisperer 建议了一个函数来完成文档字符串。</p><p><img src="/img/bVc8eZq" alt="image.png" title="image.png"></p><p>还能支持对代码注释和函数文档的补全,如图:</p><p><img src="/img/bVc8eZT" alt="image.png" title="image.png"></p><h2>总结</h2><p>本文介绍了 AWS CodeWhisperer 的安装、配置步骤,并在最基本的代码编写过程中体验了它的补全功能,但是实际使用过程中,还是发现补全功能有点延迟,经常需要使用 <code>Alt+C</code> 来唤出。</p><p>总体而言,体验下来,有如下几点缺陷:</p><ol><li>对比 Copilot AI 收费而言,最大的特点是个人开发者<strong>免费</strong></li><li>相对于结合 ChatGPT 的代码工具如 Bito 还少了代码解释和互动的功能</li><li>相对于 Codeium 而言,响应速度有点慢,没有 VS Code 编辑器与 Codeium 插件的结合使用更佳。</li></ol><p>总而言之,CodeWhisperer 满足了个人开发过程中基本的代码功能的补缺,提高效率,这些工具的出来终于让文档不用自己想怎么描述了,还是很好用的,应该针对后续会有很大更新与迭代,让我们来期待一下吧~</p><p>后话:汽车的发明下岗的不是马夫而是马,代码工具更好用也无法替代善于运用工具程序员。马夫可以转型司机,同理,期待程序员转型为真正的 CTRL + C/V 操作员。</p><p>希望本文能对你有所帮助,如果喜欢本文,可以点个关注。</p><p>下一篇文章见!宇宙古今无有穷期,一生不过须臾,当思奋争。</p><p>参考链接:</p><ul><li><a href="https://link.segmentfault.com/?enc=zPbhgdmVWZ3drK7mjOoACw%3D%3D.UEVv2OApK%2BOI8QUGs8I7T8IypDGalpIZHJUb%2B5nKeudgGAHvE0MIH61joVngy7RfQH8eYJga1TflYBH9WburNvTv4xiqhPqiD%2FeHH7SVqFv0lfud%2FwrQ53MhfIkqDIzH" rel="nofollow">Setting up CodeWhisperer for individual developers</a></li></ul>
如何在 Go 中验证一个字符串是否是 URL?
https://segmentfault.com/a/1190000043869237
2023-06-06T13:28:11+08:00
2023-06-06T13:28:11+08:00
宇宙之一粟
https://segmentfault.com/u/yuzhoustayhungry
0
<h2>前言</h2><p>在实际开发过程中,有时候会遇到 URL 的校验问题,其实我也是直接调用了第三方库,但是也引发了一个思考,Go 语言中有哪些方法去验证一个字符串是否满足 URL 格式呢?</p><p>URL 代表唯一资源定位符,是 URI 的子类型(尽管许多人可以互换使用这两个术语)。URL 是对网络资源的引用,通常被视为网址(例如 <code>https://golang.org</code>)。</p><p>下面你可以看到一个 URL 的结构,它符合 URI 的结构</p><pre><code>URI = scheme:[//authority]path[?query][#fragment]
authority = [userinfo@]host[:port]</code></pre><h3>官方 URL 包</h3><p>在 Golang 中利用 <a href="https://link.segmentfault.com/?enc=mlgevkYB2tA1fTY7WvlYew%3D%3D.iHy7eumsCdEsxuUrKzdG6mzitvmMQZvUcZnvsxEscWtSyz93QHhwPeOt5dZG3%2B54" rel="nofollow"><code>url.ParseRequestURI</code></a> 可以简单验证我们的 URL。</p><pre><code>func ParseRequestURI(rawurl string) (*URL, error)</code></pre><blockquote>ParseRequestURI 将 rawurl 解析成 URL 结构。它假定在 HTTP 请求中接收到 rawurl,因此 rawurl 仅被解释为绝对 URI 或绝对路径。假定字符串 rawurl 没有 #fragment 后缀。(Web 浏览器在将 URL 发送到 Web 服务器之前去除 #fragment。)</blockquote><h4>ParseRequestURI 与 Parse</h4><p>还有另一种方法应该用于解析 URL 字符串,但有一些注意事项。它允许相对 URL 使验证更加宽松。它是<a href="https://link.segmentfault.com/?enc=IZyG1aXFkiCaC1%2Fti%2FQkBQ%3D%3D.p5W1VBWf1Oa5ghjP%2FfIqTGYontsmM%2BHQh7ALxosxMZg6zM20dIOoTs%2BXlk5smWdG" rel="nofollow"><code>url.Parse</code></a></p><pre><code>func Parse(rawurl string) (*URL, error)</code></pre><p>如文档中所述:</p><blockquote>Parse 将 rawurl 解析成 URL 结构。 <br>rawurl 可以是相对的(路径,没有主机)或绝对的(以方案开头)。尝试在没有方案的情况下解析主机名和路径是无效的,但由于解析歧义,不一定会返回错误。</blockquote><p>比如如下的例子:</p><pre><code class="go">package main
import (
"fmt"
"net/url"
)
func main() {
str := "//yuzhou1u.com"
var validURL bool
_, err := url.Parse(str)
if err != nil {
fmt.Println(err)
validURL = false
} else {
validURL = true
}
fmt.Printf("%s is a valid URL : %v \n", str, validURL)
}
</code></pre><p><img src="/img/remote/1460000043869239" alt="image.png" title="image.png"></p><h3>使用 ParseRequestURI</h3><p>在 Google 解决方法的时候,根据这篇教程中 <a href="https://link.segmentfault.com/?enc=Kp9JS8%2Bx6ToTzsr%2BEuzNPg%3D%3D.BchxQ2f617glZvymqCtRTb1%2FuN5lo9GiJQWTB%2FK4OhkRMPJY4mKO26viAcJ00vR1vEKJfnbyvlBgfkXFrygQ0l55aTBaCoGIax2pkWIQLnBzCy2wj2baC89CZxnKf20j" rel="nofollow">How to check if a string is a valid URL in Golang?</a> 提供的方法,写了一个函数:</p><pre><code class="go">func isValidUrl(u1 string) bool {
_, err := url.ParseRequestURI(u1)
if err != nil {
return false
}
u, err := url.Parse(u1)
if err != nil || u.Scheme == "" || u.Host == "" {
return false
}
// Check if the URL has a valid scheme (http or https)
if u.Scheme != "http" && u.Scheme != "https" {
return false
}
return true
}
</code></pre><p>使用这个方法也有个缺陷,如果是多个 schema:<code>https</code>,也是检查不出来的,例如下面的示例:</p><pre><code class="go">package main
import (
"fmt"
"net/url"
)
func main() {
fmt.Println(isValidUrl("testURL"))
fmt.Println(isValidUrl("test.test/"))
fmt.Println(isValidUrl("http://goglang.org"))
fmt.Println(isValidUrl("https://goglang.org"))
fmt.Println(isValidUrl("https://https://https://../google.com"))
}
func isValidUrl(u1 string) bool {
_, err := url.ParseRequestURI(u1)
if err != nil {
return false
}
u, err := url.Parse(u1)
if err != nil || u.Scheme == "" || u.Host == "" {
return false
}
// Check if the URL has a valid scheme (http or https)
if u.Scheme != "http" && u.Scheme != "https" {
return false
}
return true
}
</code></pre><p>运行结果如图:</p><p><img src="/img/remote/1460000043869240" alt="image.png" title="image.png"></p><h3>使用 <code>url-verifier</code> 包</h3><p>安装:<code>go get -u github.com/davidmytton/url-verifier</code></p><pre><code class="go">package main
import (
"fmt"
urlverifier "github.com/davidmytton/url-verifier"
)
func main() {
url := "https://https://https://../google.com"
verifier := urlverifier.NewVerifier()
ret, err := verifier.Verify(url)
if err != nil {
fmt.Errorf("Error: %s", err)
}
fmt.Printf("Result: %+v\n", ret)
}
</code></pre><p>运行结果:</p><p><img src="/img/remote/1460000043869241" alt="image.png" title="image.png"></p><p>后面在研究这部分 <code>verifier.go</code> 源码时,发现这个用了 <code>govalidator</code> 这个包,如图:</p><p><img src="/img/remote/1460000043869242" alt="image.png" title="image.png"></p><p>于是,我们何不直接使用 <code>govalidator</code> 包来判断一个字符串是否是 URL 呢?</p><h3>使用 <code>govalidator</code> 包</h3><p><code>govalidator</code> 是一个针对字符串、结构体和集合的验证器和包。基于 <a href="https://link.segmentfault.com/?enc=Y4hExb86rnfvqat4apJ7bQ%3D%3D.hBFs%2FYXB%2B0DRNhEhLPLQHyKooqLFR7ucc31zJNMT%2F%2Fd4HJhRAVBcd%2FX83lV8UgYd" rel="nofollow">validator.js</a>。</p><p>GitHub 地址:<a href="https://link.segmentfault.com/?enc=5nPq4kwiFz7l2vF%2Bc0xTbw%3D%3D.D68HndjIJ1eJxRfahGR4PlnzQTIKm29rvw1Zv5p%2FaFiPDvWu3U3lhyv4qEF1jBpr" rel="nofollow">https://github.com/asaskevich/govalidator</a> , 目前收获了 5.7k 的 star</p><p>安装:<code>go get github.com/asaskevich/govalidator</code></p><p><img src="/img/remote/1460000043869243" alt="image.png" title="image.png"></p><pre><code class="go">package main
import (
"fmt"
"github.com/asaskevich/govalidator"
)
func main() {
str := "https://https://https://../google.com"
validURL := govalidator.IsURL(str)
fmt.Printf("%s is a valid URL : %v \n", str, validURL)
}
</code></pre><p>运行结果如下:</p><p><img src="/img/remote/1460000043869244" alt="image.png" title="image.png"></p><h3>正则表达式匹配</h3><p>本来想自己写正则表达式匹配的,然后发现 <code>govalidator</code> 包的作者背后的原理也是用了正则表达式的,</p><p>然后就偷懒了,直接把他源码中的部分集合到一个 <code>main.go</code> 函数中:</p><pre><code class="go">package main
import (
"fmt"
"net/url"
"regexp"
"strings"
"unicode/utf8"
)
const (
URLSchema string = `((ftp|tcp|udp|wss?|https?):\/\/)`
URLUsername string = `(\S+(:\S*)?@)`
URLPath string = `((\/|\?|#)[^\s]*)`
URLPort string = `(:(\d{1,5}))`
URLIP string = `([1-9]\d?|1\d\d|2[01]\d|22[0-3]|24\d|25[0-5])(\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])){2}(?:\.([0-9]\d?|1\d\d|2[0-4]\d|25[0-5]))`
IP string = `(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))`
URLSubdomain string = `((www\.)|([a-zA-Z0-9]+([-_\.]?[a-zA-Z0-9])*[a-zA-Z0-9]\.[a-zA-Z0-9]+))`
URL = `^` + URLSchema + `?` + URLUsername + `?` + `((` + URLIP + `|(\[` + IP + `\])|(([a-zA-Z0-9]([a-zA-Z0-9-_]+)?[a-zA-Z0-9]([-\.][a-zA-Z0-9]+)*)|(` + URLSubdomain + `?))?(([a-zA-Z\x{00a1}-\x{ffff}0-9]+-?-?)*[a-zA-Z\x{00a1}-\x{ffff}0-9]+)(?:\.([a-zA-Z\x{00a1}-\x{ffff}]{1,}))?))\.?` + URLPort + `?` + URLPath + `?$`
/*MaxURLRuneCount : maxima cantidad de runas por contar*/
MaxURLRuneCount = 2083
/*MinURLRuneCount : minima cantidad de runas por contar*/
MinURLRuneCount = 3
)
var rxURL = regexp.MustCompile(URL)
// IsURL checks if the string is an URL.
func IsURL(str string) bool {
if str == "" || utf8.RuneCountInString(str) >= MaxURLRuneCount || len(str) <= MinURLRuneCount || strings.HasPrefix(str, ".") {
return false
}
strTemp := str
if strings.Contains(str, ":") && !strings.Contains(str, "://") {
// support no indicated urlscheme but with colon for port number
// http:// is appended so url.Parse will succeed, strTemp used so it does not impact rxURL.MatchString
strTemp = "http://" + str
}
u, err := url.Parse(strTemp)
if err != nil {
return false
}
if strings.HasPrefix(u.Host, ".") {
return false
}
if u.Host == "" && (u.Path != "" && !strings.Contains(u.Path, ".")) {
return false
}
return rxURL.MatchString(str)
}
func main() {
fmt.Println(IsURL("testURL"))
fmt.Println(IsURL("test.test/"))
fmt.Println(IsURL("http://goglang.org"))
fmt.Println(IsURL("https://goglang.org"))
fmt.Println(IsURL("https://https://https://../google.com"))
}
</code></pre><p>运行结果:</p><p><img src="/img/remote/1460000043869245" alt="image.png" title="image.png"></p><p>除了校验 URL,这个包还提供了众多的字符串校验方法,例如校验邮箱、信用卡格式、IP...</p><p><img src="/img/remote/1460000043869246" alt="image.png" title="image.png"></p><h2>总结</h2><p>数据校验是每个程序员日常开发过程的一部分,尤其是在从事后端服务时,数据验证必须严格,保持正确。</p><p>在这篇文章中,我们讨论了如何在 Go 语言中正确验证一个字符串是否是 URL,当然利用了官方包和优秀的第三方包,在实际过程中,可能我们为了简便会直接使用别人开发好的工具,但是在学习过程中,不妨也去思考别人实现的原理,结合实际业务需要,进而扩展成自己的工具包。 </p><p>希望本文能对你有所帮助,如果喜欢本文,可以点个关注.</p><p>下一篇文章见!宇宙古今无有穷期,一生不过须臾,当思奋争。</p><blockquote><p>参考链接:</p><ul><li><a href="https://link.segmentfault.com/?enc=2c0KXLPBNujzyoMMe7eUYw%3D%3D.U1lBxuyYyialw5HcAThsOJieQgL4btk24lNgM%2FvIOcbALDdrtaWhl1ANBHWVXwL8" rel="nofollow">url-verifier</a>: A Go library for URL validation and verification: does this URL actually work?</li><li><a href="https://link.segmentfault.com/?enc=MGRLogO8N9ddDSOnnnWg6g%3D%3D.98hqi4GQcWcdS5XfDZWazH3BKKWzTpDKnI28oT%2BKIlPzUZn%2FjMIqlnlHyzLgWTnMa%2BiDBZkq2iTrxke5E1FEnw%3D%3D" rel="nofollow">How To Validate Url In Go</a></li><li><a href="https://link.segmentfault.com/?enc=8PyX3tNutM2Tv4nRoE%2Bmag%3D%3D.KDC6YffLaEuBKG6DHK3WA%2Bdb3zxhQQrVa1Zl4lIJQQeTebeUBKrrV5uISEnBVNUY" rel="nofollow">govalidator</a>: [Go] Package of validators and sanitizers for strings, numerics, slices and structs</li></ul></blockquote>
面向 Web、微服务应用的 Serverless 托管平台云应用引擎 CAE
https://segmentfault.com/a/1190000043864573
2023-06-05T11:07:13+08:00
2023-06-05T11:07:13+08:00
宇宙之一粟
https://segmentfault.com/u/yuzhoustayhungry
0
<h2>1. 业务诉求:决定发布一个应用,我需要思考什么?</h2><p>假如你们公司目前有一个绝妙的 IDEA 急需落地! 为了将业务方案准备好!你需要考虑但不限于以下几个动作:</p><ul><li>搭建服务器、配置组网环境</li><li>开发技术栈造型,搭建软件开发项目框架</li><li>搭建多类型如测试、灰度、生产环境,版本在多环境之间人工流转</li><li>为了应对业务突发,提前准备冗余的资源</li><li>业务上量后,运维/运营复杂度提升,业务可靠性面临极大挑战,招收更多专家技术人员</li></ul><p>针对上述的思考,你可能将面临的问题有:</p><ul><li>高昂的基础设置管理成本</li><li>复杂、低效产品研发--上线流程;推高研发成本和业务迭代周期</li><li>高昂人力/技术成本;极具挑战的运维复杂度,加剧降低业务迭代效率</li></ul><p>如果此时提供了一种服务不需要运维和管理技术资源的技术,你会不会考虑使用。<br>这就是本文即将将介绍的一种适时而生的云应用引擎——云托管服务。</p><h2>2. 云应用托管的演进趋势</h2><p>先来看看传统应用托管的演进趋势,云应用托管从应用架构的演进大概有以下三个过程:</p><ol><li>单体应用架构,这种方式上云工期:周。对应的云端托管是在虚拟机托管的,对应用进行封装,但是这种虚机托管的方式需要手动部署应用,手动做负载均衡,手动弹性伸缩。云托管解决“快速交付”问题</li><li>云原生架构,这种方式上云周期:天。容器化托管:微服务化+容器化效率倍增,极具挑战的运维复杂度。半自动弹性伸缩(资源自动伸缩),按资源付费,资源有时是独占的,在这个过程中,资源利用率比较低。</li><li>Serverless 架构,上云工期:小时,Serverless 托管:基础设置免运维,除了关心业务应用的基础逻辑之外,不需要关心网络、存储、计算、部署和容器管理等等。这个时候应用与运行时环境解耦,从业务应用到最底层的基础设施全部实现自动弹性伸缩(从部署到上线),按需付费,最大化提高资源利用率。</li></ol><p><img src="/img/remote/1460000043864575" alt="图片" title="图片"></p><h3>2.1 云应用托管解决的问题</h3><ul><li>ECS:虚机托管(单体)</li></ul><p>云厂商提供操作系统 OS,机房、存储、网络、计算。对于客户来说,需要 CI/CD,云端应用业务及负载均衡,手动(虚拟机+应用业务)扩容伸缩</p><ul><li>CCI/CCE:容器托管</li></ul><p>云厂商除了提供同样的操作系统、机房、存储、网络、计算,还能够实现自动容器弹性伸缩。对于客户来说,仍旧需要 CI/CD,云端的应用业务及负载均衡,手动应用业务扩容缩容</p><ul><li>CAE:Serverless 托管</li></ul><p>在这种方式下,云厂商能增加了 CI/CD 流水线,实现全自动弹性伸缩、负载均衡。此时用户只需要关注云端的应用业务。Serverless 托管的优势在于屏蔽了复杂的基础设施管理和业务运维,客户可专注于业务。</p><p><img src="/img/remote/1460000043864576" alt="图片" title="图片"></p><h2>3. CAE 产品介绍</h2><p>CAE(Cloud Application Engine 云应用引擎),一个面向应用的 Serverless 托管服务,可提供极简集成与部署、极低使用成本、免运维能力(免基础设施运维)的一站式应用托管方案,支持从源码、软件包、镜像包等集成发布应用,提供秒级弹性伸缩能力,是一款按量付费的云服务产品。可做到基础设施免运维,根据可观测的运行指标对应用进行生命周期管理。<br>CAE 面向多个行业:互联网(直播、教育、游戏)、大企业(车联网、智能制造、商超)、政务(交通门、空间信息、医疗)、金融(互联网金融、风控、资管)等。</p><p><img src="/img/remote/1460000043864577" alt="图片" title="图片"></p><h3>3.1 特点介绍</h3><ul><li>上云简单:提供环境标签和增加<strong>用户体验。</strong>一键创建资源隔离的运行环境:环境命名,不同命名环境之间资源隔离;用户一键开通环境,一站式极简上云。</li><li>运维高效:支持丰富的弹性策略、产品组合以及规划中的参考架构。提供资源、连接、周期性、时间等多维度因素;屏幕 ECS、CCE、ELB、VPC 等基础云服务,内置 CSE/RDS 等常见中间件的配置管理,集成 AOM 监控能力,apm 拓扑告警能力</li><li>安全可信:华为可信经验和安全特性升级。内置华为多年可信经验,支持出海业务,基于虚机、容器等资源层安全特性强化升级业务安全</li></ul><p><img src="/img/remote/1460000043864578" alt="图片" title="图片"></p><h3>3.2 CAE 逻辑概念</h3><p>CAE 可以创建多个环境,比如测试环境、灰度环境和生产环境。每个环境下可以配置多个应用,每个应用由多个组件构成,每个组件就是一个个源代码、镜像或者软件包。</p><p><img src="/img/remote/1460000043864579" alt="图片" title="图片"></p><p>举一个例子:</p><p><img src="/img/remote/1460000043864580" alt="图片" title="图片"></p><h2>4. 关键特性</h2><h3>4.1 从源托管</h3><p>支持常见业务形态极简部署上云:</p><p><img src="/img/remote/1460000043864581" alt="图片" title="图片"></p><h3>4.2 传统部署流程与CAE部署流程的区别</h3><p>与传统的部署流程相比,CAE 提供一站式部署:</p><p><img src="/img/remote/1460000043864582" alt="图片" title="图片"></p><h3>4.3 极致弹性伸缩</h3><ol><li>痛点一:为应对周期性业务洪峰,提前准备资源,稳定期资源利用率低<br>诉求:资源按需弹性和伸缩,按使用量计费</li><li>痛点二:促活流量激增,预留资源预估不准确、手动扩容稳定性差<br>诉求:丰富的弹性伸缩策略,极低弹性时延,全自动弹性实施</li><li>痛点三:为维持业务稳定,面临从基础设施到业务的复杂维度<br>诉求:基础设置免运维,聚焦业务层运维</li></ol><p><img src="/img/remote/1460000043864583" alt="图片" title="图片"></p><h2>5. 典型案例</h2><h3>5.1 全国连锁的综合性行业平台</h3><p>背景:XXX 为服务行业的综合型大企业,公司成立数十年,从刀耕火种的小企业到如今全国性的行业平台,是比较典型的业务驱动技术改革的客户。</p><p>从业务起步阶段到上量阶段到规模阶段,随着业务的增长迅速,逐渐发现痛点,并进行改善,最后发现冗余资源成本高昂,复杂运维态势威胁业务,技术门槛高,加剧风险。</p><p>最后进行了云化变革,业务托管上云,基础设置免运维,丰富的弹性策略,秒级弹性能力,资源使用按需付费,流量峰期过去可以立马释放流量,极大降低成本,利用 CAE 丰富的弹性策略和秒级弹性能力,无忧应对业务涌动。</p><p><img src="/img/remote/1460000043864584" alt="图片" title="图片"></p><h2>6. CAE 实践:5 分钟体验“疾”速上云</h2><p><img src="/img/remote/1460000043864585" alt="图片" title="图片"></p><h3>前提条件</h3><p>注册华为云帐号,并登录成功。</p><p>在GitHub官网注册帐号,并创建私人令牌,以便服务后期的授权使用。</p><h3>创建环境</h3><p>登录CAE控制台,在您首次使用本服务时,页面会提醒您尚未创建环境。</p><p>点击“立即创建”,进入创建环境页面<br>单击“确定”,界面跳转显示“环境创建中”及进度,创建环境完成。</p><h3>新增应用</h3><p>登录CAE控制台,左方菜单栏点击“组件列表 ”,顶部“应用”右侧单击的 “+”号,可新增应用。</p><p>输入应用名称。</p><p>单击“确定”,新增应用完成。</p><p><img src="/img/remote/1460000043864586" alt="图片" title="图片"></p><h3>新增组件</h3><p>在组件界面,填充组件名称和版本号,支持选择实例规格:内核(0.5、1 或 2)和 1 GiB 和实例数量。<br>选择主流的代码源,支持连接主流的开源代码托管仓库:DevCloud、GitHub、GitLab、Gitee 等等。</p><p><img src="/img/remote/1460000043864587" alt="图片" title="图片"></p><p>还支持各种镜像(包括用户打包的自定义镜像、开源镜像和共享镜像)或者是软件包:</p><p><img src="/img/remote/1460000043864588" alt="图片" title="图片"></p><p>填写完相应的信息之后,可以点击配置组件:</p><p><img src="/img/remote/1460000043864589" alt="图片" title="图片"></p><p>然后对刚刚导入的组件进行运维管理:如环境变量设置、访问方式设置、伸缩策略设置等等:</p><p><img src="/img/remote/1460000043864590" alt="图片" title="图片"></p><h3>环境变量配置演示</h3><p><img src="/img/remote/1460000043864591" alt="图片" title="图片"></p><h3>访问方式配置演示</h3><p><img src="/img/remote/1460000043864592" alt="图片" title="图片"></p><h3>伸缩策略配置演示</h3><p><img src="/img/remote/1460000043864593" alt="图片" title="图片"></p><p>回到界面上方,单击“配置并部署组件”,可以看到配置变更信息,单击“确定”,确认配置信息。:</p><p><img src="/img/remote/1460000043864594" alt="图片" title="图片"></p><p>配置完毕之后点击“确定”,完成一键配置。如果组件已经部署,单击生效配置。</p><p><img src="/img/remote/1460000043864595" alt="图片" title="图片"></p><p>通过一个脚本访问可以查看弹性伸缩配置是否成功:</p><p><img src="/img/remote/1460000043864596" alt="图片" title="图片"></p><h3>访问应用</h3><p>登录 CAE,选择“组件配置”。</p><p>在上方下拉框中选择所需访问的环境,应用和组件。</p><p>在“运维管理中 > 访问方式”找到 ip 和端口号。</p><p>若您选择外网访问方式,可直接在“访问方式”模块中单击 ip 进行跳转,也可以在“组件列表”页面,直接单击“外部访问地址”栏的 ip 进行跳转。若您选择内网访问方式,则需要登录集群节点使用 curl 命令访问。</p><h3>应用运维</h3><p>登录 CAE 控制台,在“概览”页面可以查看组件健康状况以及资源使用情况等。</p><p>选择想要查看的环境,应用和组件。</p><p>单击“组件事件”显示组件的整个活动过程。</p><p>单击“组件监控”,显示当前组件运行的实例数,以及各个实例的cpu和内存的使用情况。</p><p>单击“组件日志”,显示单个实例的日志信息。</p><p><img src="/img/remote/1460000043864597" alt="图片" title="图片"></p><h2>总结</h2><p>本文介绍了云计算时代的新业务诉求,并介绍了云应用托管趋势,着重对华为 CAE 产品的介绍和关键特性</p><p>,并展示了一个传统服务行业典型案例,对更多企业有着借鉴和参考意义,最后通过一个小白如何上手操作CAE 产品,加深用户的实践体验。希望能对你有所帮助!</p><p>参考链接:快速体验CAE</p><blockquote>本文参与华为云社区【内容共创】活动第22期。<br>任务20:[新手开服——美女讲师带你玩转serverless云应用引擎](</blockquote>
FastAPI 快速开发 Web API 项目: 路径参数和查询参数
https://segmentfault.com/a/1190000043630411
2023-04-06T16:48:10+08:00
2023-04-06T16:48:10+08:00
宇宙之一粟
https://segmentfault.com/u/yuzhoustayhungry
0
<p><strong>FastAPI 快速开发 Web API 项目</strong>学习笔记:</p><ul><li>第一篇:<a href="https://link.segmentfault.com/?enc=6J68Dk1KTxJC0J0%2BmtslEw%3D%3D.W6nU2l9W%2B2XnhyJrY8aBwp0i0N343%2FafN7Dgb5W1RdWJgbtFFvbWpop4uDTmXJIGZiXfBQYDMlgtJ63cAieUBg%3D%3D" rel="nofollow">通过 Python FastAPI 开发一个快速的 Web API 项目</a></li><li>第二篇:<a href="https://link.segmentfault.com/?enc=Im2VJQ6Y%2B8EGNJR9p2KAsw%3D%3D.JOFiXdmPxidrno9Cud7l4ZCmj5E3K9nKaZ3U8dSolVsy7VuGnVI0LKx3G7l%2BPdvvY%2FuTO4cEIa4s%2BKBQhCBX6w%3D%3D" rel="nofollow">FastAPI 的路由介绍与使用</a></li><li>第三篇:<a href="https://link.segmentfault.com/?enc=Zcsbwz%2FGTnIVwWsgN3RqSA%3D%3D.cE3s%2BE3Z29%2FNqE%2BQereokScm0E3JBXm85AYfkfeNM7J80ljGn8jrDCbGNeaVGJSrsyIzGe6QSHrllX3um0ESyA%3D%3D" rel="nofollow">FastAPI 开发中数据校验利器 Pydantic 介绍与集成使用</a></li></ul><h2>1 介绍</h2><p>FastAPI 允许您定义客户端在向 API 或 Web 应用程序发出请求时可以包含在 URL 中的参数和变量。这些参数可用于<strong>查询数据库</strong>、<strong>排序</strong>和<strong>过滤数据</strong>以及影响返回响应的许多其他事情。</p><p>在本篇文章中,将会介绍 FastAPI 的路径和查询参数以及它们在 FastAPI 的工作方式。</p><h2>2 路径参数</h2><p>路径参数是什么?</p><blockquote>可以作为URL的一部分的变量,通常会应用在指定路径的URI</blockquote><h3>2.1 定义</h3><p><strong>定义:</strong>路径参数通过预先定义好的位置来接受参数。路径参数是包含在API路由中的参数,用于识别资源。这些参数作为一个标识符,有时也是一个桥梁,可以在网络应用中进行进一步的操作。</p><p>根据 Restful 的设计风格,在设计一个 GET 方法 API 时,如果是要读取单一资源,可以通过提供 URI 并带入路径参数的方式,来获取特定资源。比如,在我们的 todo 项目中,如果想要获取 <code>id=1</code> 的待办事项的详细信息,就可以通过将数字 1 作为路径参数,然后将其作为参数传递给路径操作函数:</p><pre><code>http://localhost:8888/todo/1</code></pre><p>在 FastAPI 中,我们可以使用与 Python 格式字符串相同的语法声明路径“参数”或“变量”,使用 <code>Path Parameter</code> 时,使用 <code>{}</code> 在路径中设定一个参数,例如官方文档中的这个例子:</p><pre><code class="python">from fastapi import FastAPI
app = FastAPI()
@app.get("/items/{item_id}")
async def read_item(item_id):
return {"item_id": item_id}</code></pre><p>上面代码中路径参数 <code>item_id</code> 的值将作为参数 <code>item_id</code> 传递给您的函数 <code>read_item()</code>。</p><p>在<code>/items/{id}</code>路由装饰器中,我们使用花括号来声明路径参数_id_。这个参数被传递到路径操作函数中,我们可以用它作为键从库存字典中获取相应的项目。</p><h3>2.2 带类型的路径参数</h3><p>路径参数也可以声明类型,通过使用 Python 的类型注解,比如定义 <code>async def read_item(item_id: int):</code> 此处的 <code>item_id</code> 的类型就被声明为 <code>int</code> 类型。如果我们访问端点<strong><a href="https://link.segmentfault.com/?enc=RqEzVXYCtXJHFtDhp157gQ%3D%3D.ojvWJ3pbRiGtGq6Rw4WRDAYw36Cdr1LIHXmEn0IP8TM%3D" rel="nofollow">http://127.0.0.1:8888/todo/test</a></strong>,我们会得到以下错误响应:</p><pre><code class="json">{
"detail": [
{
"loc": [
"path",
"todo_id"
],
"msg": "value is not a valid integer",
"type": "type_error.integer"
}
]
}</code></pre><p>如图:</p><p><img src="/img/remote/1460000043630413" alt="" title=""></p><h3>2.3 路径顺序</h3><p>通常我们需要制作具有相似结构的路径。当固定路径相同时会发生这种情况。例如,假设我们有两个端点,如下所示:</p><pre><code>/todo/default
/todo/{todo_id}</code></pre><p>第一个返回默认待办事项。<strong>第二个返回具有特定 todo_id 的</strong>待办事项。在这种情况下,声明这些端点的顺序很重要。路径按顺序评估。因此,我们需要在 <code>/todo/{todo_id}</code> 之前声明 <code>/todo/</code>。见下面的代码:</p><pre><code class="python">@todo_router.get("/todo/")
async def get_default_todo():
if todo_list:
return {
"todo_1": todo_list[0]
}
return {
"message": "Todo is initial."
}
@todo_router.get("/todo/{todo_id}")
async def get_single_todo(todo_id: int) -> dict:
for todo in todo_list:
if todo.id == todo_id:
return {
"todo": todo
}
return {
"message": "Todo with supplied ID doesn't exist."
}
</code></pre><h3>2.4 数字校验</h3><p>FastAPI 能够在将路径参数传递给我们的函数之前验证路径参数中的数据。它通过类来实现这一点<code>Path</code>。</p><p>更新您的 <code>main.py</code>文件以匹配以下内容:</p><pre><code class="python">@todo_router.get("/todo/{todo_id}")
async def get_single_todo(todo_id: int = Path(ge=1, le=len(todo_list),
title="The ID of the todo to be retrieved.")) -> dict:</code></pre><p>我们为 Path 添加了额外的导入。然后在路径操作函数中,我们在 <code>todo_id</code> 上创建一个数字限制,使其仅包含大于或等于 1 且小于或等于我们 todo 列表长度的值。</p><p>有更好的方法来验证库存项目是否存在,我们稍后会讲到,但现在,我们只是演示如何使用 Path 类。</p><p>以下是您可以在 <code>Path</code> 类中设置的数字验证列表。</p><ul><li>gt:大于</li><li>ge:大于或等于</li><li>lt:小于</li><li>le:小于或等于</li></ul><h3>2.5 字符串验证</h3><p>您也可以对字符串参数施加约束。将以下路由添加到您的 <code>main.py</code> 文件中。</p><pre><code class="python">@todo_router.get("/todos/{todo_name}")
async def get_todo(todo_name: str = Path(max_length=20)) -> dict:
for todo in todo_list:
if todo_name in todo["name"] :
return {
"todo": todo
}
return {
"message": "Todo with supplied name doesn't exist."
}</code></pre><p>该路径将参数解释为字符串而不是数字,并将最大长度设置为 6 个字符。可以将其他约束添加到参数,例如最小长度和正则表达式。查看<strong><a href="https://link.segmentfault.com/?enc=6gSaBEpf7T0aJQdaX%2B79Mw%3D%3D.uJu87A7VhJLLddyzAnfnwvZNkcBZLDoPxTbK3PQO84TKvEvoZ2L2Evhl5Yzxn09by3LvtLLGGafolRRtYRA4z3kO%2FxLznkIHEpEM72MlCek%3D" rel="nofollow">FastAPI 文档</a></strong>了解更多。</p><p>我们希望建立一个 GET 请求,通过查询 id 来 获取 todo 的 API,因此可以接受 todo 的 id <code>todo_id</code>,可以通过以下的方式:</p><pre><code class="python">from fastapi import APIRouter, Path
from models.model import Todo
todo_router = APIRouter()
todo_list = []
@todo_router.get("/todo/{todo_id}")
async def get_single_todo(todo_id: int = Path(..., title="The ID of the todo to be retrieved.")) -> dict:
for todo in todo_list:
if todo.id == todo_id:
return {
"todo": todo
}
return {
"message": "Todo with supplied ID doesn't exist."
}</code></pre><p>在上面的例子中,我们在 <code>get_single_todo()</code> 中定义了 <code>todo_id</code> 参数后,FastAPI 会自动注册的路径中寻找 <code>{todo_id}</code> 的位置,当接受到符合条件的 <code>Request(/todo/{todo_id})</code> 时,会自动将 <code>{todo_id}</code> 位置的值当成 <code>get_single_todo()</code> 的参数填入。</p><p>如果是要读取所有资源,并希望能够进行排序、数量限制等操作,则可以通过带入查询参数。在上一节中,我们已经能通过 <code>@todo_router.get("/todo")</code> 获取到所有的待办事项。</p><p>启动 todo 项目:</p><pre><code>uvicorn main:app --port 8888 --reload</code></pre><p>启动成功如下:</p><p><img src="/img/remote/1460000043630414" alt="" title=""></p><p>打开另外一个 Bash 终端:</p><pre><code>$ curl -X 'GET' 'http://127.0.0.1:8888/todo/2' -H 'accept: applicaion/json'
{"message":"Todo with supplied ID doesn't exist."}</code></pre><p>说明此时通过查询 id 为 2 的待办事项,并不存在,因此,我们通过 POST 请求新建一个 id 为 2 的代办事项,命令如下:</p><pre><code>curl -X POST http://127.0.0.1:8888/todo -H 'accept: application/json' -H 'Content-Type: application/json' -d '{"id": 2, "item": "learning path and query"}' </code></pre><p>建立成功之后,截图如下:</p><p><img src="/img/remote/1460000043630415" alt="" title=""></p><p>再通过查询来获取这个 id 为 2 的待办事项,可以看到刚刚新建的 <code>item</code> 信息为 "learning path and query" :</p><pre><code>$ curl -X 'GET' 'http://127.0.0.1:8888/todo/2' -H 'accept: applicaion/json'
{"todo":{"id":2,"item":"learning path and query"}}(fastwebprojects) </code></pre><p>后台服务器也是显示 200 正常:</p><p><img src="/img/remote/1460000043630416" alt="" title=""></p><h2>3 查询参数</h2><h3>3.1 定义</h3><p>查询参数对于构建灵活的 API 至关重要。顾名思义,带有查询参数的端点通常可以帮助客户端根据动态参数查询特定数据。查询参数是一个<strong>可选的参数</strong>,通常出现在 URL 的<strong>问号</strong> <code>?</code> 之后。它用于过滤请求,并根据提供的查询返回特定的数据。</p><p>查询参数是什么?</p><blockquote>通常会放于URL最后面,并以 <code>?</code> 区隔开来。查询参数用来接受 URL 所附加的参数,与 <code>Path Parameter</code> 最大的差别在于没有预先定义位置。</blockquote><p>在一个路由处理函数中,当我们声明一个函数时,其参数不作为路径参数出现,它们将被解释为查询参数。你也可以通过在函数参数中创建 FastAPI <code>Query()</code> 类的一个实例来定义一个查询,比如下面的例子:</p><pre><code class="python">async def query_route(query: str = Query(None)):
return query</code></pre><p>回到我们的 todo 项目中,如何定义一个查询参数呢?查看下面的例子:</p><pre><code class="python">@todo_router.get("/todos/")
async def get_default_todo(skip: int = 0, limit: int = 10):
if todo_list:
return todo_list[skip : skip + limit]
return {
"message": "Todo is initial."
}</code></pre><p>我们有两个参数 <code>skip</code> 和 <code>limit</code> 。这些参数不是路径参数的一部分。相反,它们以查询参数的形式提供,同时以 <code>http://localhost:8000/todos?skip=0&limit=10</code> 调用 API 端点。</p><p>此处,<code>skip</code> 的初始值为 0,<code>limit</code> 的初始值为 10。由于它们是 URL 的一部分,因此它们被视为字符串。但是,我们使用 Python 类型声明它们。因此,它们会自动转换并根据它进行验证。</p><h3>3.2 查询参数默认值</h3><p>如我们所见,查询参数不是固定路径的一部分。因此,它们是可选的。此外,它们可以具有默认值,以防我们不传递任何值作为输入。</p><p>换句话说,如果我们简单地调用 <code>http://localhost:8888/todos</code>,这将意味着 <code>skip</code> 将采用默认值 0,而 <code>limit</code> 将采用默认值 10。</p><h3>3.3 声明一个可选查询参数</h3><p>同样,您可以通过将其默认设置为 None 来声明可选查询参数:</p><p>在 Python 3.10 和 Python 3.6 还有一点点不一样,这一点建议多看看官方文档,这里以 3.10 为例:</p><pre><code class="python">from fastapi import FastAPI
app = FastAPI()
@app.get("/items/{item_id}")
async def read_item(item_id: str, q: str | None = None):
if q:
return {"item_id": item_id, "q": q}
return {"item_id": item_id}</code></pre><p>在这种情况下,函数参数 <code>q</code> 是可选的,默认情况下为 <code>None</code>。</p><p>更改 <code>todo_router.py</code> 文件:</p><pre><code class="python">from fastapi import APIRouter, Path
from models.model import Todo
todo_router = APIRouter()
todo_list = [
{"id":1,"item":"write a todo project"},
{"id":2,"item":"learning path parameters"},
{"id":3,"item":"learning query parameters"},
]
@todo_router.post("/todo")
async def add_todo(todo: Todo) -> dict:
todo_list.append(todo)
return {
"message": "Todo added successfully"
}
@todo_router.get("/todo")
async def retrieve_todos() -> dict:
return {
"todos": todo_list
}
@todo_router.get("/todo/{todo_id}")
async def get_single_todo(todo_id: int = Path(..., title="The ID of the todo to be retrieved.")) -> dict:
for todo in todo_list:
if todo.id == todo_id:
return {
"todo": todo
}
return {
"message": "Todo with supplied ID doesn't exist."
}
@todo_router.get("/todo/")
async def get_default_todo():
if todo_list:
return {
"todo_1": todo_list[0]
}
return {
"message": "Todo is initial."
}
@todo_router.get("/todos/")
async def get_default_todo(skip: int = 0, limit: int = 10):
if todo_list:
return todo_list[skip : skip + limit]
return {
"message": "Todo is initial."
}</code></pre><p><img src="/img/remote/1460000043630417" alt="" title=""></p><h3>3.4 必输查询参数</h3><p>当我们声明一个具有默认值的查询参数时,我们将其设为可选。此外,当我们保留默认值 None 时,FastAPI 将其视为可选。</p><p>但是,我们也可以强制指定某些查询参数。基本上,我们不必提供默认值。在这种情况下,FastAPI 将查询参数视为必需参数。</p><p>请参见下面的示例:</p><pre><code class="python">from fastapi import FastAPI
app = FastAPI()
@app.get("/items/{item_id}")
async def read_user_item(item_id: str, needy: str):
item = {"item_id": item_id, "needy": needy}
return item</code></pre><p>这里的查询参数 <code>needy</code> 是一个必需的 str 类型的查询参数。</p><h3>3.5 查询参数约束</h3><p>查询参数可以像路径参数一样被分配约束。在上面的例子中,也许我们会限制 <code>max_price</code> 参数只接受正整数。</p><p>该过程与路径约束相同,但导入不同。添加 <code>Query</code> 到您的 <code>fastapi</code> import ,如下所示。</p><pre><code class="python">from fastapi import FastAPI, Path, Query</code></pre><p>然后更新 <code>get_items()</code> 路径函数以使用 Query 类并设置适当的选项。</p><pre><code class="python">@app.get("/items/")
def get_items(vegan: bool = False, max_price: float = Query(default=20, ge=1)):
results = {}
counter = 1
for item in inventory.values():
if item["vegan"] == vegan and item["price"] <= max_price:
results[counter] = item
counter += 1
return results</code></pre><p>现在为参数输入负值 <code>max_price</code> 会返回以下错误消息。</p><pre><code class="json">{"detail":[{"loc":["query","max_price"],"msg":"ensure this value is greater than or equal to 1","type":"value_error.number.not_ge","ctx":{"limit_value":1}}]}</code></pre><p>查看 FastAPI 文档以了解<strong><a href="https://link.segmentfault.com/?enc=95xBxikbW%2BHQLlcb%2B0VBVQ%3D%3D.sC5p8FC5%2FWiZnThRXh0CaCtXN%2FyI8MQz%2BjL%2FpO6InWW6x8t2bS9WKEmpbj77VhJTVXMHKX11Iu9DTpixsBeCYRN1%2BTJcfHnbKmBt2fdzzZE%3D" rel="nofollow">查询参数和字符串验证</a></strong>看看您还可以用查询参数做什么。</p><h2>4 总结</h2><p>路径和查询参数都允许开发人员接收变量输入、处理它并返回基于该输入的响应。</p><p>在设计 API 路由时,有时您可以使用路径或查询参数来完成相同的任务。以下是这方面的例子。</p><pre><code># 获取 id 为 1 的 todo
http://localhost:8888/todos/1
http://localhost:8000/todos/?id=1</code></pre><p>但是路径参数更适合创建对特定信息的请求。查询参数更适合过滤和排序数据。</p><p>在这篇文章中,我们了解了什么是路径和查询参数以及如何在 FastAPI 中实现它们。然后,我们研究了如何通过限制数字和字符串值来对这些参数创建约束。我们还回顾了路径参数和查询参数之间的主要区别,并在 todo 应用中运用了展示了如何去使用路径参加和查询参数。</p><p>如果想了解更多的这两个参数的细节,读者可以读取下面的参考资料,PS:官方文档才是第一手学习资料。</p><blockquote><p>希望本文能对你有所帮助,如果喜欢本文,可以点个关注.</p><p>下一篇文章见!宇宙古今无有穷期,一生不过须臾,当思奋争。</p></blockquote><p>参考链接:</p><ul><li><a href="https://link.segmentfault.com/?enc=Nfi3tTDmS35P4bRaGj0JJg%3D%3D.TOMQFVCFD2zT8y5FS1lJQDKIc6YPByOgpb8UzvJtecNOZh2Vo3CwUlg4gJ1h3i9pMcvgNea1hgenxFtXNXpLPg%3D%3D" rel="nofollow">Path Parameters</a></li><li><a href="https://link.segmentfault.com/?enc=TWz8QE7yrMNWI8fM6pMagg%3D%3D.il8jZn7Fw%2FDiZ44WLs%2FijdEvmGPHRN3Z%2B3neGYE%2B%2FF9jHcISmvTWWjn8y%2FmAAxyFO768fVkaWP0H4DnS0AzvWg%3D%3D" rel="nofollow">Query Parameter</a></li><li><a href="https://link.segmentfault.com/?enc=J2hl1h%2BFKZeJTs5746w%2FdQ%3D%3D.87%2FYbIL56kxW8wPfcAjHgrbC1o%2BCCgm8fpLdS5Khbc%2FlILJwjnpIk%2BQ%2B2chdk6D%2Fyu5yDBXE4IWK4bg2H%2Bzytg%3D%3D" rel="nofollow">Path and Query Parameters</a></li></ul>
FastAPI 开发中数据校验利器 Pydantic 介绍与集成使用
https://segmentfault.com/a/1190000043624073
2023-04-04T15:39:55+08:00
2023-04-04T15:39:55+08:00
宇宙之一粟
https://segmentfault.com/u/yuzhoustayhungry
0
<h2>前言</h2><p>当我们在处理一些数据来自系统外部的时候,可能来自 API、终端用户输入或者其他途径,Web 开发中有句话叫做:<strong>永远不要相信用户的输入</strong>。</p><p>所以,我们可能需要检查、校验这些数据,将这些数据格式化、标准化,以至于这些数据满足我们真实程序的数据输入,保证项目的正确执行。</p><h2>Pydantic 介绍</h2><p><a href="https://link.segmentfault.com/?enc=eTye%2B4pxJE7SN7U8TmgbOQ%3D%3D.5TWZWRL%2FvXsR%2F81SMiO5TLsa5%2F%2FYDtRJe7OzVHbLuaD%2BJ3U0y23hxfSfSBPLsWcY" rel="nofollow">Pydantic</a> 是一个 Python 库,它提供了一种简单方便的方法来验证和操作数据。它的创建是为了帮助简化数据验证过程并提高开发人员的效率。 Pydantic 与 Python 的数据结构无缝集成,并提供灵活且用户友好的 API 来定义和验证数据。使用 Pydantic,开发人员可以定义他们的数据结构和验证规则,库将自动验证传入数据并在不满足任何规则时引发错误。这有助于确保项目中使用的数据是一致的并符合要求的标准。</p><p><img src="/img/remote/1460000043624075" alt="image.png" title="image.png"></p><blockquote>官方介绍:使用 Python 类型注释的数据验证和设置管理。 pydantic 在运行时强制执行类型提示,并在数据无效时提供用户友好的错误。定义数据应该如何在纯正的、规范的 Python 中;用 pydantic 验证它。</blockquote><p>Pydantic 提供 <a href="https://link.segmentfault.com/?enc=M%2BGcsz7PZ2Y6%2BivHFwbDLw%3D%3D.z30HJErsFubvpqWBy4B3Pl0XgUVnjQfZMzGorrHLrp1fvPxWakLTX8HIuy9gzp1pCEwf7j1a3buT6DZHZNJS7A%3D%3D" rel="nofollow">BaseModel</a> 让开发者能够通过继承该类并且利用 <a href="https://link.segmentfault.com/?enc=KsOcozrrwsLsFFnsW5FQvg%3D%3D.TKm3AvL5tbGTpBzYEcx3eZnduk6Ox7ZlDXE6B%2BrIxNoosgfbOyjMHj7BjDAhfu6l" rel="nofollow">typing</a> 注记类别属性的数据类型,保证我们不用写过多的代码就拥有基本的<strong>数据验证</strong>功能。</p><p>定义模型时,被用作请求主体对象和请求-响应对象的类型提示。在本文中,我们将简单看一下在请求体中使用Pydantic 模型来处理 POST 请求。</p><h3>Pydantic 安装</h3><pre><code class="shell">pip install pydantic</code></pre><p><img src="/img/remote/1460000043624076" alt="image.png" title="image.png"></p><p>但是如果你是直接安装好了 FastAPI ,这一步可以跳过,因为 FastAPI 框架就使用了 Pydantic。</p><h3>Pydantic 优点</h3><ul><li>易于使用: Pydantic 很容易安装与使用,并且有一个简单的 API,使得所有开发者都可以快速上手使用。</li><li>快速验证: Pydantic 快速有效地执行数据验证,使其适合于在高性能的应用程序中使用。</li><li>自动生成文档: Pydantic 可以为你的数据模型自动生成文档,节省时间,并且更容易理解你的数据结构。</li><li>类型提示支持: Pydantic 支持类型提示,使开发人员更容易定义数据结构,避免在代码中出现错误。</li><li>与 FastAPI 集成: Pydantic 可以很容易地与FastAPI(一个高性能的 Python 网络框架)集成,为 API 提供自动请求和响应验证。</li><li>自定义验证规则: Pydantic 允许开发人员定义自定义的验证规则,使得在需要的时候可以实现复杂的验证逻辑。</li><li>一致的数据: Pydantic 确保项目中使用的数据是一致的,并符合所需的标准,减少了错误的风险,使代码库的维护更加容易。</li></ul><h3>什么是类型注解</h3><p>Python 是动态类型的语言,所以在同一个命名空间中,变量的值可以是字符串也可以是数组,比如:</p><pre><code class="python">>>> x = 4
>>> type(x)
<class 'int'>
>>> x = "hello, 宇宙之一粟"
>>> type(x)
<class 'str'></code></pre><p>大约在 Python 3.5 起引入了 type hints 类型注解,虽然 Python 在运行时不强制执行函数和变量类型注解,但这些注解可用于类型检查器、IDE、静态检查器等第三方工具,这些工具就可以帮助我们侦测类型上的错误。</p><p>最简单的注解方式如下:</p><pre><code class="python">def hello(name: str) -> str:
return 'Hello ' + name</code></pre><p>这里面的 <code> : str</code> 声明 name 字段为字符串,如果传入 int 类型,编译器就会得到报错:</p><p><img src="/img/remote/1460000043624077" alt="image.png" title="image.png"></p><h3>利用 Pydantic 定义模型</h3><p>我们在定义如下 Book 模型的时候,我们声明了 id 为数字类型,Name 到 ISBN 都为字符串类型,Tags 为列表类型,通过继承自 <code>BaseModel</code> 的特性,Pydantic 会自动帮我们验证型态的正确性:</p><pre><code class="python">from pydantic import BaseModel
class Book(BaseModel):
id: int
Name: str
Author: str
Publisher: str
ISBN: str
Tags: list[str]
HeadFirstPython = Book(
id = 1,
Name = 'Head First Python, 2nd Edition',
Author = 'Paul Barry',
Publisher ="O'Reilly Media, Inc.",
ISBN = "9781491919538",
Tags = ["Python", "Head Frist"]
)</code></pre><p>如果这样定义之后,我们的上述的 HeadFirstPython 不会报任何错误,但是如果我们的 id 还没定义,如下:</p><pre><code class="python">from pydantic import BaseModel
class Book(BaseModel):
id: int
Name: str
Author: str
Publisher: str
ISBN: str
Tags: list[str]
HeadFirstPython = Book(
id = "notdefineyet",
Name = 'Head First Python, 2nd Edition',
Author = 'Paul Barry',
Publisher ="O'Reilly Media, Inc.",
ISBN = "9781491919538",
Tags = ["Python", "Head Frist"]
)</code></pre><p>则会报一个 id 类型错误的报错:</p><p><img src="/img/remote/1460000043624078" alt="image.png" title="image.png"></p><p>但是如果你定义 id 为 <code>id = "1",</code> 则不会报错,因为 Pydantic 帮助我们自动实现了类型转换,如果想要严格控制 int 类型,需要导入 <code>StrictInt</code>,<code>StrictString</code> 同理,代码如下:</p><pre><code>from pydantic import BaseModel, StrictInt, StrictStr
class Book(BaseModel):
# id: int
id: StrictInt
# Name: str
Name: StrictStr
Author: str
Publisher: str
ISBN: str
Tags: list[str]
HeadFirstPython = Book(
# id = "1",
id = 1,
# Name = 'Head First Python, 2nd Edition',
Name = 27546, # 此处会报错,
Author = 'Paul Barry',
Publisher ="O'Reilly Media, Inc.",
ISBN = "9781491919538",
Tags = ["Python", "Head Frist", 1]
)
print('output>', HeadFirstPython.dict())</code></pre><p>报错信息如下:</p><pre><code>pydantic.error_wrappers.ValidationError: 1 validation error for Book
Name
str type expected (type=type_error.str)</code></pre><p>更多 Pydantic 的类型详细说明,请查看官方文档,点<a href="https://link.segmentfault.com/?enc=F7k9gvXoDQ8HR7%2BD%2FszZ2Q%3D%3D.gtYzGF1QS6yBV%2FBzkAAZ%2BBLTfSjuLNf5FnpbVE6ngKKgW4V3bV8H0QBG8%2BPZ%2B2GD" rel="nofollow">此处</a>。</p><h3>Pydantic 的工作方式</h3><p>Pydantic 的工作方式是允许开发人员使用 Python 类来定义数据模型。这些类继承自 Pydantic 提供的 BaseModel 类,可以包括类型提示、默认值和验证规则。当收到数据时,Pydantic 使用数据模型来验证传入的数据,并确保其符合所定义的要求。</p><p>在验证过程中,Pydantic 对照数据模型中定义的类型提示和验证规则,检查数据中的每个字段。如果数据不符合要求,Pydantic 会提出一个错误,并停止验证过程。如果数据是有效的,Pydantic 就会创建一个数据模型的实例,用传入的数据来填充它,并将其返回给用户。</p><p>Pydantic 还提供了一些高级功能,例如字段别名,自定义验证函数,以及对嵌套数据模型的支持,使得它可以处理广泛的数据验证场景。此外,Pydantic支持序列化和反序列化,允许根据需要将数据转换为Python数据结构、JSON和其他格式。</p><h2>Pydantic 与 FastAPI 集成开发 Demo:写一个 todo 应用</h2><p>假设我们通过 FastAPI 集成 Pydantic 的优点,写一个 todo 应用的接口实现 Todo 应用的创建和查看功能,文件目录结构如下:</p><p><img src="/img/remote/1460000043624079" alt="image.png" title="image.png"></p><p>首先,我们通过 Pydantic 新建 todo 应用的 <code>model.py</code>:</p><pre><code class="python">from pydantic import BaseModel, StrictInt
class Todo(BaseModel):
id: StrictInt
item: str</code></pre><p>先简单定义了两个字段:</p><ul><li>id:数字类型,作为唯一标识</li><li>item: 字符串类型</li></ul><p>然后新建路由文件夹 <code>router</code>,在其中建立一个 <code>todo_router.py</code> 文件,写入如下代码:</p><pre><code>from fastapi import APIRouter
from models.model import Todo
todo_router = APIRouter()
todo_list = []
@todo_router.post("/todo")
async def add_todo(todo: Todo) -> dict:
todo_list.append(todo)
return {
"message": "Todo added successfully"
}
@todo_router.get("/todo")
async def retrieve_todos() -> dict:
return {
"todos": todo_list
}</code></pre><p>回到 <code>main.py</code>:</p><pre><code>from fastapi import FastAPI
from routers.todo_router import todo_router # importing router
app = FastAPI() # create an app instance
@app.get('/')
async def home() -> dict:
return { "message": "Welcome to my Page"}
app.include_router(todo_router)</code></pre><h2>验证 Pydantic 是否生效</h2><p>执行命令如下:</p><pre><code>$ uvicorn main:app --reload --port 8888
INFO: Will watch for changes in these directories: ['C:\\Users\\Wade\\Desktop\\FastAPI\\fastwebprojects']
INFO: Uvicorn running on http://127.0.0.1:8888 (Press CTRL+C to quit)
INFO: Started reloader process [22288] using StatReload
INFO: Started server process [25008]
INFO: Waiting for application startup.
INFO: Application startup complete.</code></pre><p>另外打开一个终端,执行 GET 请求,得到空的 todo 列表:</p><pre><code class="shell">$ curl -X 'GET' 'http://127.0.0.1:8888/todo' -H 'accept: applicaion/json'
{"todos":[]}</code></pre><p>接着执行 POST 请求,如下:</p><pre><code class="shell">curl -X POST http://127.0.0.1:8888/todo -H 'accept: application/json' -H 'Content-Type: application/json' -d '{}'
{"detail":[{"loc":["body","id"],"msg":"field required","type":"value_error.missing"},{"loc":["body","item"],"msg":"field required","type":"value_error.missing"}]}</code></pre><p>如果我们传入一个空的数组,将会得到一串由 Pydantic 校验生成的 JSON 数据,格式化如下:</p><pre><code class="json">{
"detail": [
{
"loc": [
"body",
"id"
],
"msg": "field required",
"type": "value_error.missing"
},
{
"loc": [
"body",
"item"
],
"msg": "field required",
"type": "value_error.missing"
}
]
}</code></pre><p>该 JSON 数据提示我们 id 和 item 都是必须字段 <code>"msg": "field required",</code> 因此,我们更改一下 POST 请求,如下:</p><pre><code>$ curl -X POST http://127.0.0.1:8888/todo -H 'accept: application/json' -H 'Content-Type: application/json' -d '{"id": 1, "item": "write a todo project"}'
{"message":"Todo added successfully"}
</code></pre><p>此时我们可以看到创建一个 todo 事项成功,返回了 <code>{"message":"Todo added successfully"}</code> !</p><p>再执行一次 curl 的 GET 请求,结果如下,可以看到刚刚创建成功的今日 todo—— <code>item</code> 为 <strong>"write a todo project"</strong>, <code>id</code> 值为 1:</p><pre><code>$ curl -X 'GET' 'http://127.0.0.1:8888/todo' -H 'accept: applicaion/json'
{"todos":[{"id":1,"item":"write a todo project"}]}</code></pre><p>打开 <code>http://127.0.0.1:8888/docs</code> ,可以看到如下界面:</p><p><img src="/img/remote/1460000043624080" alt="" title=""></p><p>此处 Todo 就是我们刚刚定义的 Model 中限定的字段类型:</p><p><img src="/img/remote/1460000043624081" alt="" title=""></p><h2>总结</h2><p>本文介绍了 Pydantic 以及它的安装与工作方式, Pydantic 提供了自动的数据验证,提高了性能,并且支持复杂的数据结构,使得构建健壮和可维护的应用程序更加容易。虽然本文只是简单的校验数据的存在性,还有更多比如邮箱、密码格式等等, Pydantic 都会提供,希望本文抛砖引玉,让大家探索和学习 Pydantic 的更多功能。</p><blockquote><p>希望本文能对你有所帮助,如果喜欢本文,可以点个关注.</p><p>下一篇文章见!宇宙古今无有穷期,一生不过须臾,当思奋争。</p></blockquote><p>参考链接:</p><ul><li><a href="https://link.segmentfault.com/?enc=lheyImOwqeC7FrHXh71LVg%3D%3D.0GrQ0b%2BQf%2FqmP3QjM80dh3Ed5KMcJqbyCW6rz0jxa4RgshoscUTAzzIB689HL%2F3%2FwAp0hmTzdSuP4Hob2dvDVg%3D%3D" rel="nofollow">What Is Pydantic And How Is It Used</a></li></ul>
FastAPI 的路由介绍与使用
https://segmentfault.com/a/1190000043620457
2023-04-03T16:59:10+08:00
2023-04-03T16:59:10+08:00
宇宙之一粟
https://segmentfault.com/u/yuzhoustayhungry
0
<p><a href="https://segmentfault.com/a/1190000043617545">上一篇</a>文章中,我介绍了 FastAPI 框架的安装和 HelloWorld 项目搭建方式。本文将介绍如何使用 Router 路由处理 FastAPI 中的请求。</p><h2>什么是路由</h2><p>路由 Router 就像是一个流水线上的线长,协调生产,下达命令给不同的组长进行分工,然后执行基本的任务。路由器的工作目的是,在团队中工作时,您可能必须在团队成员(这里的团队负责人是队长)之间分配复杂性,这将有助于更快地完成项目,正确的 SME 将在该分支/路由器上工作.</p><p><img src="/img/remote/1460000043620460" alt="路由器接口" title="路由器接口"></p><p>路由是构建网络应用的一个重要部分。FastAPI 中的路由是灵活和方便的。路由是处理从客户端发送到服务器的 HTTP 请求的过程。HTTP 请求被发送到定义的路由,这些路由有定义的处理程序来处理请求和响应。这些处理程序被称为 Route Handler。</p><h2>FastAPI 中的路由</h2><p>参考 FastAPI 文档对路由器的介绍:如果你正在构建一个应用程序或一个 Web API,你很少会把所有东西都放在一个文件中。 FastAPI 提供了一个方便的工具来构建您的应用程序,同时保持所有的灵活性。</p><p>先来看一个例子:</p><pre><code class="python">from fastapi import FastAPI
app = FastAPI()
@app.get('/')
async def welcome() -> dict:
return { "message": "Welcome to my Page"}</code></pre><p>uvicorn 工具指向 FastAPI 的实例,为应用程序服务:</p><pre><code>uvicorn main:app --port 8888 --reload</code></pre><p><img src="/img/remote/1460000043620461" alt="" title=""></p><p>访问</p><pre><code>$ curl http://127.0.0.1:8888
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 32 100 32 0 0 11279 0 --:--:-- --:--:-- --:--:-- 16000{"message":"Welcome to ma Page"}</code></pre><p><code>FastAPI()</code>实例可用于路由操作,正如前面所见。然而,这种方法通常用于在路由过程中只能处理单一路径的应用程序。在使用 <code>FastAPI()</code> 实例创建一个执行独特功能的单独路由的情况下,应用程序将无法运行两个路由,因为 <code>uvicorn</code> 工具只能运行一个入口点。</p><h2>如果有多个路由</h2><p>让我们了解一下如何用代码来创建路由器,下面是我们的基本(非路由器)代码,在这里,我创建了一个例子:主页、添加数字页面和添加字符串页面。由于这是一个例子,我只取了两个父路径为 <code>'/add/'</code> 的函数,但在现实生活中,你可能会发现 20-30 个这样的函数,然后你将需要创建路由器,因为在一个文件中处理太多复杂的东西会变得很麻烦。</p><pre><code class="python">from fastapi import FastAPI
app = FastAPI()
@app.get('/')
async def welcome() -> dict:
return { "message": "Welcome to my Page"}
@app.get('/add/numbers')
def add_numbers():
return { "message": "we are adding numbers"}
@app.get('/add/strings')
def add_strings():
return { "message": "we are adding strings"}</code></pre><p>那么,问题来了,我们如何处理需要一系列路由执行不同功能的广泛应用程序呢?答案是 <strong>APIRouter</strong> 类。</p><h2>利用 APIRouter 类实现路由</h2><p>APIRouter 类属于 FastAPI 包,为多个路由创建路径操作。APIRouter 类鼓励应用程序路由和逻辑的模块化和组织化。</p><p>APIRouter 类从 fastapi 包中导入,并创建一个实例。路由方法被创建并从创建的实例中分发,例如如下:</p><pre><code class="python">from fastapi import APIRouter
# create router
router = APIRouter(
prefix='/add',
tags = ['addition']
)</code></pre><p>上面的代码将创建一个路由器实例,它可以带有一些参数,比如下面两个的含义:</p><ul><li>prefix:在特定页面中 fastapi 提供的每个装饰器中添加前缀</li><li>tags:这将帮助我们找到属于哪个类别的功能(想想我们可以找到相关文章的主题标签)</li></ul><p>然后可以利用 APIRouter 类创建一个新的路径操作,创建一个新文件 <code>add_router.py</code>:</p><pre><code class="python">from fastapi import APIRouter
# create router
router = APIRouter(
prefix='/add',
tags = ['addition']
)
@router.get('/numbers')
def add_numbers():
return { "message": "we are adding numbers"}
@router.get('/strings')
def add_strings():
return { "message": "we are adding strings"}</code></pre><p>APIRouter 类的工作方式与 FastAPI 类的工作方式相同。然而、 uvicorn 不能使用 APIRouter 实例为应用程序服务,这与 FastAPI 不同。使用 APIRouter 类定义的路由需要被添加到 FastAPI 实例中,以实现它们的功能。</p><p>为了使刚刚定义的路由可见,我们将使用 <code>include_router()</code> 方法把 <code>add_router</code> 路径操作处理程序到主 FastAPI 实例中,如下:</p><pre><code class="python">from fastapi import FastAPI
from src import add_router # importing router
app = FastAPI() # create an app instance
@app.get('/')
async def welcome() -> dict:
return { "message": "Welcome to my Page"}
app.include_router(add_router.router)</code></pre><p><code>include_router(router, ...)</code> 方法负责在主程序的实例中加入用 APIRouter 类定义的路由添加到主应用程序的实例中,以使路由变得可见。</p><p>最终的文件目录结构如下:</p><p><img src="/img/remote/1460000043620462" alt="" title=""></p><h2>测试 Router 功能</h2><p>启动我们的 uvicorn 服务:</p><pre><code>uvicorn src.main:app --reload --port 8888</code></pre><p>在控制台看到如下信息,表示服务启动成功:</p><pre><code>$ uvicorn src.main:app --reload --port 8888
INFO: Will watch for changes in these directories: ['C:\\Users\\Wade\\Desktop\\FastAPI\\fastwebprojects']
INFO: Uvicorn running on http://127.0.0.1:8888 (Press CTRL+C to quit)
INFO: Started reloader process [23508] using StatReload
INFO: Started server process [30600]
INFO: Waiting for application startup.
INFO: Application startup complete.</code></pre><p>使用浏览器或者终端发送 GET 请求:</p><pre><code>$ curl http://127.0.0.1:8888
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 32 100 32 0 0 12255 0 --:--:-- --:--:-- --:--:-- 16000{"message":"Welcome to my Page"}</code></pre><p>浏览器如下,访问 <code>http://127.0.0.1:8888/</code>:</p><p><img src="/img/remote/1460000043620463" alt="" title=""></p><p>访问 <code>http://127.0.0.1:8888/add/numbers</code> :</p><p><img src="/img/remote/1460000043620464" alt="" title=""></p><p>访问 <code>http://127.0.0.1:8888/add/strings</code> :</p><p><img src="/img/remote/1460000043620465" alt="" title=""></p><p>最后,通过访问 <code>http://127.0.0.1:8888/docs</code> 来查看我们刚刚定义的接口,我们将看到自动 API 文档,包括来自所有子模块的路径,使用正确的路径(和前缀)和正确的标签名:</p><p><img src="/img/remote/1460000043620466" alt="" title=""></p><h2>总结</h2><p>我们已经学会了 FastAPI 中的 APIRouter 类是如何工作的,以及如何将其包含在 <code>main</code> 应用实例中,以实现所定义的路径操作的使用。希望本文能对你有作用,咱们下一篇文章再见!</p><blockquote><p>希望本文能对你有所帮助,如果喜欢本文,可以点个关注.</p><p>下一篇文章见!宇宙古今无有穷期,一生不过须臾,当思奋争。</p></blockquote><p>参考链接:</p><ul><li><a href="https://link.segmentfault.com/?enc=AKhgJjN6LQ4Axd%2BCTY98%2FA%3D%3D.njFkqf8fA4deHgOU7AEPcVE1eZ83RwOUutiPy6fw7TO5x1S83dJgWdJn%2BJYBY1bcBTUAICr6e4w1TGB5UswOXLNcZYA94tYDzJyAxeRtDfhUdLzDDNBgiAUZt8j0JBQHRNUbVH3nPF292FgySCcfJIWzLzilrDzfrhoa5Rqr1zmwK4EQOidhzYKaI9Qr2%2BJu" rel="nofollow">Bigger Applications - Multiple Files</a></li><li><a href="https://link.segmentfault.com/?enc=IHp1HO2EGz8ScToNBgT7eA%3D%3D.%2FX8qURraoHg2THn25htQ7IOBM%2Bc3VjHzvgWMz5xvMMdoYJQyADScYnf26%2FipK0pKeaCfkZAMz4V8jfjt9DhzvZTD%2BjU3vPbb4te4qG%2F6la0%3D" rel="nofollow">Router API</a></li><li><a href="https://link.segmentfault.com/?enc=Ue6Az96c4LTay6Y8QUvLFA%3D%3D.D4yBwdxo4WVLvE8mgcL3U7WRGaTeN0c0s7BPM%2BdRvZJj7B1NJXVejDnOmCpGmYOCGcycX9aQYVj1Aidvim98azNrF3mb0J68zGD40K834fQ%3D" rel="nofollow">Routers in FastAPI</a></li><li><a href="https://link.segmentfault.com/?enc=K8dx2MpqSCG6vtrmJ30mDA%3D%3D.HCNwJ9AHLPDEtTdvzauDRj1t2tK%2FXXfdskI1MyCWRchlwnMT3zRSAFm3r0%2Fa6Mm5v9mMAFArXDrdDtlntzdIjh22zIzyhWftOXVpUBfq1%2BXIJE%2Bt4nNHyeF4JgTxNCr3" rel="nofollow">FastAPI Error loading ASGI app. Could not import module 'main'</a></li></ul>
Go 语言设计模式之建造者模式
https://segmentfault.com/a/1190000043617583
2023-04-03T10:14:47+08:00
2023-04-03T10:14:47+08:00
宇宙之一粟
https://segmentfault.com/u/yuzhoustayhungry
0
<h2>前言</h2><p>你去买车,你不会只买一个轮胎、一个发动机、一个方向盘,你买的是一辆包括轮胎、方向盘、发动机、底盘、电气系统和车身等多个部件组成的完整骑车。</p><p>在设计模式中,建造者模式就是解决如何将这些部件组装成一辆完整的汽车并返回给用户的设计模式。建造者模式为客户端返回的不是一个简单的产品,而是一个由多个部件组成的<strong>复杂</strong>产品。</p><h2>思考如下问题</h2><p>假设让我们思考如何创建一个房屋对象。建造一栋简单的房屋,你需要建造地板和四面的强,安装房门和窗户,然后再建造一个漂亮的屋顶。</p><p>但如果想要一个更宽敞舒适的别墅,还需要有院子、游泳池、植物和其他设施(例如中央空调、排水、供电设备),那又该怎么办呢?</p><p><img src="/img/remote/1460000043617585" alt="" title=""></p><p>最简单的方法就是扩展房屋的积累,然后创建一系列涵盖所有参数组合的子类。随着房子越复杂,子类越多,任何新增的参数都会让这个层次结构更加复杂。</p><p><img src="/img/remote/1460000043617586" alt="" title=""></p><p>另外一种方式则无需生成子类,我们可以在房屋基类中创建一个包含所有可能参数的超级构造函数,并用它来控制房屋对象,这种方法确实可以避免生成子类,但它却会造成另外一个问题——构造函数参数太多。</p><pre><code>House(windows, doors, rooms, hasSwimPool, hasGarden,...)</code></pre><p>但并不是所有的房子都需要游泳池,导致绝大部分的参数都没有使用,使得<strong>构造函数的声明复杂,调用不整洁</strong>。</p><p>解决方式就是今天要介绍的建造者模式。</p><h2>建造者模式概念</h2><p>建造者模式(Builder Pattern),又称生成器模式,是较为复杂的创建者模式,它将客户端与包含多个组成部分的复杂对象的创建过程分离,客户端无需知道复杂对象的内部组成部分与装配方式,只需要知道所需的建造者类型即可。</p><p>定义:将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。</p><p>结构图如下:</p><p><img src="/img/remote/1460000043617587" alt="" title=""></p><p>由上图可以知道,建造者模式包含 4 个角色:</p><ul><li><code>Builder</code> 抽象建造者:它为创建一个产品 Product 对象的各个部件指定抽象接口,这个接口一般包括两类方法:</li><li><code>buildPartX()</code> :用于创建复杂对象的各个部件</li><li><code>getResult()</code> :用于返回复杂对象</li><li><code>ConcreteBuilder</code> 具体建造者:它实现了 <code>Builder</code> 接口,实现各个部件的具体构造和装配方法,定义并明确其所创建的复杂对象,也可以提供一个方法返回创建好的复杂产品对象</li><li><code>Product</code> 产品角色:它是最终被构建的复杂对象,包含多个组成部件,具体建造者创建该产品的内部表示并定义其装配过程。</li><li><code>Director</code> 主管、指挥者:指挥者又被称为导演类,定义调用构造步骤的顺序。它负责安排负责对象的建造次序,指挥者和抽象建造者之间存在关联关系,可以在其 <code>construct()</code> 建造方法中调用建造者对象的部件构造与装配方法,完成负责对象的建造。</li></ul><h2>Go 代码示例</h2><p>代码组织结构如下:</p><p><img src="/img/remote/1460000043617588" alt="" title=""></p><ol><li>首先创建 <code>house.go</code> 文件, 建立 <code>House</code> 这个产品基类,代码如下;</li></ol><pre><code class="go">package main
type House struct {
windowType string
doorType string
swimPool string
floor int
}</code></pre><p>正像前文所说一眼,房子有窗户、门、游泳池、楼层等部分组成。</p><ol start="2"><li>然后创建抽象创建者 <code>iBuilder.go</code> 文件,也是我们的建造者接口,分别定义 4 个 <code>set</code> 和 1 个 <code>getHouse()</code> 方法,代码如下:</li></ol><pre><code class="go">package main
type IBuilder interface {
setWindowType()
setDoorType()
setNumFloor()
setSwimPool()
getHouse() House
}
func getBuilder(builderType string) IBuilder {
if builderType == "normal" {
return newNormalBuilder()
}
if builderType == "cottages" {
return newCottagesBuilder()
}
return nil
}</code></pre><ol start="3"><li>新建具体建造者:普通房子 <code>normalBuilder.go</code>,在这个文件中,因为 Go 语言没有继承的概念,所以也需要我们定义跟 <code>House</code> 相同的结构体,然后实现 normalHouse 的构建 :</li></ol><pre><code>package main
type NormalBuilder struct {
windowType string
doorType string
swimPool string
floor int
}
func newNormalBuilder() *NormalBuilder {
return &NormalBuilder{}
}
func (b *NormalBuilder) setWindowType() {
b.windowType = "Wooden Window"
}
func (b *NormalBuilder) setDoorType() {
b.doorType = "Wooden Door"
}
func (b *NormalBuilder) setNumFloor() {
b.floor = 3
}
func (b *NormalBuilder) setSwimPool() {
b.swimPool = "None"
}
func (b *NormalBuilder) getHouse() House {
return House{
doorType: b.doorType,
windowType: b.windowType,
swimPool: b.swimPool,
floor: b.floor,
}
}</code></pre><ol start="4"><li>跟上一步同理,新建别墅具体建设者 <code>cottagesBuilder.go</code> 文件,代码如下:</li></ol><pre><code class="go">package main
type cottagesBuilder struct {
windowType string
doorType string
swimPool string
floor int
}
func newCottagesBuilder() *cottagesBuilder {
return &cottagesBuilder{}
}
func (b *cottagesBuilder) setWindowType() {
b.windowType = "Glass Window"
}
func (b *cottagesBuilder) setDoorType() {
b.doorType = "Steel Security Door"
}
func (b *cottagesBuilder) setNumFloor() {
b.floor = 1
}
func (b *cottagesBuilder) setSwimPool() {
b.swimPool = "Swimming Pool"
}
func (b *cottagesBuilder) getHouse() House {
return House{
doorType: b.doorType,
windowType: b.windowType,
swimPool: b.swimPool,
floor: b.floor,
}
}</code></pre><ol start="5"><li>新建主管 <code>director.go</code> ,主管结构体内也是抽象建造者,其次主管有着 <code>setBuilder()</code> 和 <code>buildHouse()</code> 的职责,最后主管负责安排负责对象的建造次序,比如先确定门、窗、楼层,再考虑是否需要加装泳池。最终代码如下:</li></ol><pre><code class="go">package main
type Director struct {
builder IBuilder
}
func newDirector(b IBuilder) *Director {
return &Director{
builder: b,
}
}
func (d *Director) setBuilder(b IBuilder) {
d.builder = b
}
func (d *Director) buildHouse() House {
d.builder.setDoorType()
d.builder.setWindowType()
d.builder.setNumFloor()
d.builder.setSwimPool()
return d.builder.getHouse()
}</code></pre><p>6.新建一个 <code>main.go</code> 文件,测试我们的创建者模式是否正确:</p><pre><code class="go">package main
import (
"fmt"
)
func main() {
normalBuilder := getBuilder("normal")
cottagesBuilder := getBuilder("cottages")
director := newDirector(normalBuilder)
normalHouse := director.buildHouse()
fmt.Printf("Normal House Door Type: %s\n", normalHouse.doorType)
fmt.Printf("Normal House Window Type: %s\n", normalHouse.windowType)
fmt.Printf("Normal House SwimPool: %s\n", normalHouse.swimPool)
fmt.Printf("Normal House Num Floor: %d\n", normalHouse.floor)
director.setBuilder(cottagesBuilder)
cottagesHouse := director.buildHouse()
fmt.Printf("\nCottage House Door Type: %s\n", cottagesHouse.doorType)
fmt.Printf("Cottage House Window Type: %s\n", cottagesHouse.windowType)
fmt.Printf("Cottage House SwimPool: %s\n", cottagesHouse.swimPool)
fmt.Printf("Cottage House Num Floor: %d\n", cottagesHouse.floor)
}</code></pre><ol start="7"><li>最后,我们使用命令运行整个包 <code>go run .</code></li></ol><p>这是输出结果图:</p><p><img src="/img/remote/1460000043617589" alt="" title=""></p><h2>优缺点</h2><p><strong>优点:</strong></p><ul><li>你可以分步创建对象, 暂缓创建步骤或递归运行创建步骤。</li><li>生成不同形式的产品时, 你可以复用相同的制造代码。</li><li>单一职责原则。 你可以将复杂构造代码从产品的业务逻辑中分离出来。</li></ul><p><strong>缺点:</strong></p><ul><li>由于该模式需要新增多个类, 因此代码整体复杂程度会有所增加。</li></ul><blockquote><p>希望本文能对你有所帮助,如果喜欢本文,可以点个关注.</p><p>下一篇文章见!宇宙古今无有穷期,一生不过须臾,当思奋争。</p></blockquote><p>参考链接:</p><ul><li><a href="https://link.segmentfault.com/?enc=VPVWYRkUmIHo%2BObRXXDXdA%3D%3D.EgIR5cSGUUZLhZWTf5col97yM2QScoP4I19LwyC%2FmtTIZnDTX7cVIBKDEZ6djNRDhQLa6R5MtXFkx11cbJXZmw%3D%3D" rel="nofollow">建造者设计模式(生成器模式)</a></li><li>《设计模式的艺术》,作者:刘伟</li></ul>
通过 Python FastAPI 开发一个快速的 Web API 项目
https://segmentfault.com/a/1190000043617545
2023-04-03T10:09:21+08:00
2023-04-03T10:09:21+08:00
宇宙之一粟
https://segmentfault.com/u/yuzhoustayhungry
0
<h2>前言</h2><p>Python 如此受欢迎的众多原因之一是 Python 有大量成熟和稳定的库可供选择:</p><ul><li>网页开发有:Django 和 Flask,提供了很好的网络开发体验和大量的有用文档</li><li>机器学习有:scikit-learn、Keras 等,提供了丰富的机器学习的包和数据处理和可视化工具。</li></ul><p>FastAPI 是一个快速、轻量级的现代 API,与其他基于 Python 的 Web 框架(如 Flask 和 Django )相比,有一个更容易的学习曲线。FastAPI 相对较新,但它有一个不断增长的社区。它被广泛地用于构建网络 API 和部署机器学习模型。<br>正是因为大量的库和框架保证了 Python 拥有良好的开发速度和便利性,使 Python、Go 和 Rust 这样的新语言能够并驾齐驱,赢得大量的开发者的喜爱。</p><h2>什么是 FastAPI ?</h2><p>FastAPI 是一个用于开发网络 API 的新的 Python 框架,在过去几年中得到了普及。如果你打算使用 Python 进行 Web 开发,熟悉 FastAPI 将对你有好处。</p><p><img src="/img/remote/1460000043617547" alt="图片" title="图片"></p><p>文档: <a href="https://link.segmentfault.com/?enc=NIoTJee5I6hJ8GdQ24TcSA%3D%3D.q9DgHLtQnUPR0wCzmGubQ%2FCXLNiXAJZ6VewIK4NcH%2Fc%3D" rel="nofollow">https://fastapi.tiangolo.com</a><br>源码: GitHub - tiangolo/fastapi: FastAPI framework, high performance, easy to learn, fast to code, ready for production<br>从官方文档来看,FastAPI 具有如下几个关键特性:</p><ul><li>快速:可与 NodeJS 和 Go 比肩的极高性能(归功于 Starlette 和 Pydantic)。最快的 Python web 框架之一。</li><li>高效编码:提高功能开发速度约 200% 至 300%。*</li><li>更少 bug:减少约 40% 的人为(开发者)导致错误。*</li><li>智能:极佳的编辑器支持。处处皆可自动补全,减少调试时间。</li><li>简单:设计的易于使用和学习,阅读文档的时间更短。</li><li>简短:使代码重复最小化。通过不同的参数声明实现丰富功能。bug 更少。</li><li>健壮:生产可用级别的代码。还有自动生成的交互式文档。</li><li>标准化:基于(并完全兼容)API 的相关开放标准:OpenAPI (以前被称为 Swagger) 和 JSON Schema。</li></ul><h2>安装 FastAPI</h2><pre><code class="shell">python -m pip install fastapi uvicorn[standard]</code></pre><p>有了这些,你已经安装了 FastAPI 和 Uvicorn,并准备学习如何使用它们。FastAPI 是你用来建立你的 API 的框架,而 Uvicorn 是使用你建立的 API 来服务请求的服务器。</p><h2>创建虚拟环境和新建项目</h2><p>首先,在 Python 项目中创建一个新的文件夹,然后创建一个新的虚拟环境:</p><pre><code>mkdir fastwebprojects
cd fastwebprojectsls
python3 -m venv env/</code></pre><p>这将确保我们安装的 Python 包与项目保持隔离。</p><p><strong>激活虚拟环境</strong></p><pre><code>source env/bin/activate</code></pre><p>现在你可以安装 FastAPI 和 uvicorn,一个 ASGI 服务器。</p><pre><code>pip install fastapi uvicorn</code></pre><p>如下图:</p><p><img src="/img/remote/1460000043617548" alt="图片" title="图片"></p><h2>开始 HelloWorld 项目</h2><p>在深入研究 Web 项目如何开发之前,我们可以在 FastAPI 中建立并运行传统的 "Hello World "应用程序。这可以证明我们的初始设置是正常工作的。</p><p>打开 Vim 或其他的 Python 编辑器,把下面的代码粘贴到一个叫做 main.py 的文件中。写入如下代码:</p><pre><code class="python">from fastapi import FastAPI # 1. 导入 FastAPI
app = FastAPI() # 2. 创建一个 FastAPI 实例
@app.get('/') # 3. 创建一个路径操作
async def root(): # 4. 定义路径操作函数
return {'message': 'Hello World!'} # 5. 返回内容</code></pre><p>这五行代码中,你已经创建了一个工作的 API 。如果你曾经使用过 Flask ,这看起来应该非常熟悉。</p><p><img src="/img/remote/1460000043617549" alt="图片" title="图片"></p><ul><li>路由 @app.get('/'): 它告诉 FastAPI,当用户请求根/ 路径时,应该运行以下方法。</li><li>方法定义 async def root() : 注意这个异步定义,这个方法将作为一个 Python3 协程 运行。如果你想了解更多关于并发性和 async 的信息,FastAPI 官方文档对其有一个很好的解释,以及是什么让 FastAPI 框架变得如此快速。</li></ul><p>最后是返回语句,我们将数据发送到浏览器。</p><pre><code> return {'message': 'Hello World!'}</code></pre><p>正如你所期望的,访问这个端点将返回一个与上述字典相匹配的 JSON 响应数据。<br>最后,说得够多了,运行服务器看看它的运行情况吧!</p><pre><code>uvicorn main:app --reload</code></pre><p>启动成功如图所示:</p><p><img src="/img/remote/1460000043617550" alt="图片" title="图片"></p><p><code>uvicorn main:app</code> 命令含义如下:</p><ul><li><code>main</code>:main.py 文件(一个 Python「模块」)。</li><li><code>app</code> :在 main.py 文件中通过 app = FastAPI() 创建的对象。</li><li><code>--reload</code>:让服务器在更新代码后重新启动。仅在开发时使用该选项。</li></ul><p>在输出中,会有一行信息像下面这样:</p><pre><code>INFO: Uvicorn running on http://127.0.0.1:8000 (Press CTRL+C to quit)</code></pre><p>该行显示了你的应用在本机所提供服务的 URL 地址。</p><p>说了这么多,我们打开浏览器访问 <strong><a href="https://link.segmentfault.com/?enc=0un6S%2FNlurlg%2BGWkTUCRWA%3D%3D.alvetDwtuIoEdf6yKdpEEmFbkCQxMuNx%2FGlMBA5pers%3D" rel="nofollow">http://127.0.0.1:8000</a></strong>,可以看到如下的 JSON 响应:</p><pre><code>{ "message": "Hello World!" }</code></pre><p>也可以运行 <code>http://127.0.0.1:8000/docs</code> ,运行这个服务器文档,可以看到基于 <a href="https://link.segmentfault.com/?enc=xYDbficwv8x%2FBvxRX06xPQ%3D%3D.jwKip0yl4bY76RG%2BOT7VbbAR6B0GHbI2NMPbf47wa8CguE2bk0YHdYuC7bx62EYz" rel="nofollow">Swagger UI</a> 自动生成的交互式 API 文档:</p><p><img src="/img/remote/1460000043617551" alt="图片" title="图片"></p><p>本框架还提供了一个可选的 API 文档,在浏览器运行 <strong><a href="https://link.segmentfault.com/?enc=CRFBXcLKEZlJf7QqF8K8YQ%3D%3D.5bRfu27%2FQkEay6JLNNVZt21ci1yVmfXtKxlBR%2FHvOWg%3D" rel="nofollow">http://127.0.0.1:8000/redoc</a></strong>,可以看到如下由 <a href="https://link.segmentfault.com/?enc=52asDO3R11YRm1VM3drWyQ%3D%3D.Cnqo1OMpCEHyra6WR9bgtVYk6pIIdlQbuJh8T3keTPb3pB2XvtSieLnpKTnfP04v" rel="nofollow">ReDoc</a> 自动生成的文档,如图:</p><p><img src="/img/remote/1460000043617552" alt="图片" title="图片"></p><h2>OpenAPI</h2><p>OpenAPI 规范(以前称为 Swagger 规范)是用于描述,生成,使用和可视化 RESTful Web 服务的机器可读接口文件的规范。</p><p>FastAPI 使用定义 API 的 OpenAPI 标准将你的所有 API 转换成<strong>模式 </strong>(schema)。</p><p><strong>模式</strong><br>模式是对事物的一种定义或描述。它并非具体的实现代码,而只是抽象的描述。</p><p><strong>API 模式</strong><br>在这种场景下,OpenAPI 是一种规定如何定义 API 模式的规范。定义的 OpenAPI 模式将包括你的 API 路径,以及它们可能使用的参数等等。</p><p><strong>数据模式</strong><br>模式这个术语也可能指的是某些数据比如 JSON 的结构。在这种情况下,它可以表示 JSON 的属性及其具有的数据类型,等等。</p><p><strong>OpenAPI 和 JSON Schema</strong><br>OpenAPI 为你的 API 定义 API 模式。该模式中包含了你的 API 发送和接收的数据的定义(或称为 Schema,模式),这些定义通过 JSON 数据模式标准 JSON Schema 所生成。</p><h2>FastAPI 中 OpenAPI 的用途</h2><p>驱动 FastAPI 内置的 2 个交互式文档系统的正是 OpenAPI 模式。</p><p>并且还有数十种替代方案,它们全部都基于 OpenAPI。你可以轻松地将这些替代方案中的任何一种添加到使用 FastAPI 构建的应用程序中。</p><p>你还可以使用它自动生成与你的 API 进行通信的客户端代码。例如 web 前端,移动端或物联网嵌入程序。</p><h2>总结</h2><p>本文介绍了 Python 中 FastAPI 这一框架,并利用其建立了一个快速的 HelloWorld 项目 API,最后介绍了关于 OpenAPI 的规范和其在 FastAPI 中的作用。希望能引入 Python 爱好者对这一框架的学习。</p><p>希望本文能对你有所帮助,如果喜欢本文,可以点个赞或者关注,十分感谢!</p><blockquote>这里是宇宙之一粟,下一篇文章见!<br>宇宙古今无有穷期,一生不过须臾,当思奋争。</blockquote><p>灵感来源:</p><ul><li><a href="https://link.segmentfault.com/?enc=4BpIq8QpmFwLWXdt1bzfOA%3D%3D.E7zHS5GtrwP9mRqDVz%2FFrR9JYbgdMLz%2F6n88bdoNFx7sxjUKuRAKJ%2BY36SCD8A%2BdVPGka7deVyNmR8dPe8Ug4Q%3D%3D" rel="nofollow">FastAPI-First Steps</a></li><li><a href="https://link.segmentfault.com/?enc=yz%2BKnEDgN3XvUsrKiFX6Kg%3D%3D.E8omBiWk3bYV2heY%2FUZ%2F6Y5Rx5dpYrZCmDnEE%2BWenAUql662qr1QSg9u4fK4ugc%2F7SKAO4kJmXvZ3rmX0qHB%2Bg%3D%3D" rel="nofollow">Python FastAPI Tutorial</a></li><li><a href="https://link.segmentfault.com/?enc=Gi2Vui2qdOdWkwkARhp2Bg%3D%3D.piN5KGafDy9jUelFgpjMNd6EGru0agdKDVmQTd%2B4Sdvh3%2FL6tzCmW6e5j3%2FhTXF%2B" rel="nofollow">Using FastAPI to Build Python Web APIs</a></li><li><a href="https://link.segmentfault.com/?enc=GkjGCUrmBqtT1ZiLq7yF%2BQ%3D%3D.W1PLGN%2F22lbe14nmoe0NcL7jal1IrTHTHZKwKZm9jL7oy8cjfdd6irLE%2BLtiMTbI" rel="nofollow">The OpenAPI Specification Repository</a></li><li><a href="https://link.segmentfault.com/?enc=mQEa%2FVylyU7Tz1JO2Ik92A%3D%3D.QtDzom8WH%2B2Inw4s3It1pX8ZQpcyW3zbKphg%2FyqLJfE%3D" rel="nofollow">OpenAPI 规范 (中文版)</a></li></ul>
读书笔记之数据密集型应用的可靠性
https://segmentfault.com/a/1190000043601679
2023-03-29T17:38:28+08:00
2023-03-29T17:38:28+08:00
宇宙之一粟
https://segmentfault.com/u/yuzhoustayhungry
1
<blockquote>宇宙之一粟读书笔记之《数据密集型应用系统设计》,本笔记的内容来自《<a href="https://link.segmentfault.com/?enc=ySzqoAEHqt9Ww9u4UvR4bg%3D%3D.BB6nH3224WzTdHaf3xDJiwTu%2BOh0JCxZ7dFz6u2Zg%2FMvXaWr5fsJDQMeVw8j2goN" rel="nofollow">数据密集型应用系统设计</a>》,该书豆瓣评分高达 9.7。</blockquote><p><img src="/img/remote/1460000043601681" alt="" title=""></p><h2>什么是数据密集型应用</h2><p>数据密集型应用,英文名 Data-Intensive Application。</p><p><strong>数据密集型应用(data-intensive applications)</strong> 正在通过使用这些技术进步来推动可能性的边界。一个应用被称为 <strong>数据密集型</strong> 的,如果 <strong>数据是其主要挑战</strong>(数据量,数据复杂度或数据变化速度)—— 与之相对的是 <strong>计算密集型</strong>,即处理器速度是其瓶颈。数据密集型计算是一类并行计算应用程序,使用数据并行方法处理大量数据。</p><p><strong>在大多数软件系统中很重要的三个问题是:</strong></p><p><strong>可靠性</strong>:即使面对意外情况如硬件、软件故障、人为失误等,系统也应正确工作,虽然系统的性能可能有所降低,但还是能执行正确的功能。</p><p><strong>可扩展性</strong>:随着系统规模的增长(数据,流量量或复杂性),应有合理的方法来匹配该增长。</p><p><strong>可维护性</strong>:随着时间的推移,许多新的人员参与到系统的开发与运维,人们应该能够依然有效维护该系统。</p><p><img src="/img/remote/1460000043601682" alt="" title=""></p><h2>数据密集型应用特性</h2><p>为了实现数据密集型计算的高性能,有必要最大程度地减少数据的移动。这可以通过 reduce 算法在数据居住的节点上执行算法来减少系统开销并提高性能。</p><p>数据密集型计算系统采用独立于机器的方法,其中运行时系统控制程序的调度、执行、负载平衡、通信和移动。</p><p>数据密集型计算非常关注数据的可靠性和可用性。传统的大规模系统可能容易受到硬件故障、通信错误和软件错误的影响,而数据密集型计算旨在克服这些挑战。</p><p>数据密集型计算是为可扩展性而设计的,因此它可以容纳任何数量的数据,因此可以满足时间关键要求。硬件和软件架构的可扩展性是数据密集型计算的最大优势之一。</p><p><img src="/img/remote/1460000043601683" alt="Figure 79" title="Figure 79"></p><h2>可靠性</h2><p>软件的典型的可靠性包括:</p><ul><li>应用程序执行用户所期望的功能</li><li>可以容忍用户出现错误或不正确的软件使用方法</li><li>性能能够应对典型场景、合理负载压力和数据量</li><li>系统可防止任何未经授权的访问和滥用</li></ul><p>故障通常被定义为组件偏离其正常规格, 而失效意味系统作为一个整体停止, 无法向用户提供所需的服务。 况且不太可能将故障概率降低到零, 因此通常设计容错机制来避免从故障引发系统失效。</p><h3>硬件错误</h3><p>硬件错误总是很容易想到:硬盘崩溃、内存故障、停电甚至是人为拔掉网线。</p><p>一般来说,处理硬件故障的标准方法是为硬件组件添加冗余,以便如果硬件发生故障,则随时可以更换。例如:</p><ul><li>对磁盘的 RAID 配置</li><li>服务器配备双电源</li><li>热插拔 CPU</li><li>数据中心添加备用电源、发电机等</li></ul><p>随着数据量和应用程序的计算需求的增加,人们偏向于使用<strong>软件故障容错技术</strong>来容忍硬件错误。这些软件容忍系统的一个优点是:对于单个服务器系统,如果需要重新启动机器(例如,应用操作系统安全补丁),则需要计划停机时间。但是,对于可以忍受机器故障的系统,可以一次修补一个节点然后重启(无需停机整个系统 - 滚动升级)。</p><p>这种方式使得系统更具有操作便利性。</p><h3>软件错误</h3><p>与硬件错误相比,软件错误之间更加具有关联性。这意味着,一个节点中的故障导致系统出现更多的故障。牵一发而动全身,例如:</p><ul><li>由于软件错误,导致当输入特定值时应用服务器总是崩溃</li><li>一个应用进程使用了某个共享资源如 CPU、内存、磁盘或网络带宽,但却不幸失控跑飞了</li><li>系统依赖于某些服务,但该服务突然变慢。甚至无响应或者开始返回异常的响应</li><li>级联故障,其中某个组件的小故障触发另一个组件故障,进而引发更多的系统问题</li></ul><p>解决方法(软件故障有时没有快速的解决方法,只能多考虑软件设计的细节):</p><ul><li>认真检查依赖的假设条件与系统之间交互</li><li>进行全面的测试,测试覆盖率争取达到 100%</li><li>进程隔离,允许进程崩溃并自动重启</li><li>监控并分析生产环节的种种数据</li></ul><h3>人为错误</h3><p>设计和构建软件系统总是由人完成的,但人类是不可靠的。尽管人类不可靠,我们如何使系统可靠?通过多种方法的组合,例如:</p><ul><li><strong>巧妙的软件设计</strong>:通过精心设计的抽象,API 和管理界面来最大程度地限制错误机会的方式设计系统。</li><li><strong>开发、测试、生产环境分离</strong>:分离最容易出错的地方、容易引发故障的接口。提供一个功能齐全但非生产用的沙箱环境(<strong>测试系统</strong>),使人们可以放心的尝试 、 体验, 包括导入真实的数据, 万一出现问题, 不会影响真实用户。</li><li><strong>充分的测试</strong>:从单元测试到全系统集成测试、手动测试到自动测试。</li><li><strong>快速的恢复机制:</strong>当出现人为失误,可以快速回复,以最大程度减少故障的影响。例如:快速回滚配置改动,滚动发布新代码,提供校验数据的工作</li><li><strong>设置详细而清晰的监控子系统:</strong>包括性能指标和错误率</li></ul><h2>总结</h2><p>可靠性不单单是针对核电站和空中交管软件之类的系统很重要,日常的很多应用也需要可靠的工作。商业软件的错误可能会导致效率下降,甚至带来法律风险,电子商务网站故障比如出现超卖,既影响网站营收,带来巨大损失。</p><p>总之,开发和软件设计人员应该秉持对用户负责的态度,<strong>设计一个可靠的应用非常关键。</strong></p><p>但有时也会出现其它的情况,牺牲一些可靠性来降低开发成本或运营开销,对此,需要权衡。</p>
记一次从技术简历制作到 Offer 选择的技术面试
https://segmentfault.com/a/1190000043584219
2023-03-25T22:49:57+08:00
2023-03-25T22:49:57+08:00
宇宙之一粟
https://segmentfault.com/u/yuzhoustayhungry
0
<blockquote>文章声明:本文基于真实校招面试经验。</blockquote><p>又是一年的求职季,这篇文章将会介绍从技术简历制作方法和 Offer 选择心得。</p><h2>技术简历制作</h2><p>首先明白一个大道理:<strong>大道至简</strong>。先来看看一个可能大厂喜欢的简历:</p><p><img src="/img/bVc62nf" alt="resume" title="resume"></p><p>清晰明了的简历看起来就很舒服有没有!</p><p>找工作就像相亲(毕竟现在相亲的公园里也需要简历了....)双方都得满意才有下一步的可能,所以我们看看我们需要准备啥东西?</p><h3>首先要做好简历的态度</h3><h4>简历就是敲门砖,别藏着掖着</h4><p>学历好的话,请亮剑 -- 现实告诉我们学历就是牌面,如果学校是985、211、海外高校的大佬们,请别隐藏自己了。小弟只有瞻仰....</p><p>学历不够,项目来凑 -- 如果你也像我一样双非,那么请多做个几个项目,最好是几个能体现技术的项目。</p><p>项目不够,抓紧做 -- 一定要吃透至少1个项目(两个及以上最好)。这个项目一定是自己写了代码,改过 Bug 的那种。别面试官一问就说这个部分不是我做的,我不知道。那样估计离凉凉不远了...</p><h4>简历要好看点、清晰明了</h4><p>好看的简历就是你对该工作的态度。对一个想要从事技术的同学来说,简历不需要做的多花哨。</p><p>简历不是贴便利贴,贴的越多反而没有核心内容,而且贴的越多越难看。</p><h4>简历要有针对性</h4><p>体现你对该公司的关注度,告诉看简历的人我就是你们公司需要招的那个人。</p><p>简历千万不能作假,作假就是往自己贴创可贴,贴的多,贴的不稳容易露馅。</p><p>说完做简历的态度,我们来说说我们的简历可能需要经过多少关。</p><h3>让 HR 满意</h3><p>问了身边的 HR 小姐姐,一天看那么多份简历,期待看到一份正中下怀的简历。</p><p>什么叫正中下怀?</p><p>站在 HR 的角度来看:用人部门缺什么样的人,我就需要招什么样的人。</p><p>首先是这些简历的基本内容:</p><ul><li>基本信息(姓名、电话、专业、邮箱)</li><li>求职意向</li><li>GitHub、个人博客、个人网站(可有可无)</li><li>教育经历(本科、硕士)</li><li>技能清单(学的、会的)</li><li>实习经历(最好有)</li><li>项目经历(重点项)</li><li>在校活动(学生干部)</li><li>荣誉奖励(评奖评优)</li><li>自我评价(可有可无)</li></ul><p>我个人的习惯就是先把<strong>基本信息</strong>开头就放清楚,(邮箱和手机号别填错了,不然看中你了也白搭)。</p><p>然后放<strong>技能清单</strong>+<strong>实习经历</strong>。因为想先让 HR 和面试官先通过技术匹配度记得我,这是我的小私心,大家可以试试哈。</p><blockquote>我不把学历放太前,是不想因为学校双非而给HR一个“不是那么好”的印象。如果你的学历好,请摆在前面,要多显眼就摆多显眼。</blockquote><p><strong>项目经历</strong>大家首先要把自己的项目吃透,然后一定要体现自己的技术使用特点。</p><p>思路就是:做了什么,怎么做的,最后做的结果如何?</p><p>如果只想把简历控制在一页,其实后面的部分可以不写了。但是其实因为大家都是网上投PDF简历,我觉得简历 1-2 页也算正常。</p><p>HR 就会在简历中挑关键词,满足这些关键词的被看中的几率就大了。</p><p>打个比方,招 Java 后端开发:Java、SpringBoot、Redis、中间件、MySQL....</p><p>所以我们最好突出这些技术栈、技术点、技术名词...</p><p>让 HR 满意的话还有哪些需要注意的</p><ul><li>强调我是谁?(姓名)</li><li>我来自哪里?(教育经历、工作/实习经历)</li><li>对我感兴趣的话你怎么联系到我?(电话、邮箱)</li><li>我有哪些本领?(是会算法、开发还是测试?)</li><li>我获得的哪些荣誉?(国省校奖?三好学生?奖学金...)</li></ul><p><strong>教育经历</strong>放在后面,刚好跟后面的在校活动等联系在一起,起到了承上启下的作用,我觉得很赞。</p><p><strong>在校活动</strong>可写可不写,如果能体现一些组织和沟通能力的活动,或者是比赛能力,我觉得应该写上。</p><p><strong>荣誉奖励</strong>有就写没有就算了。</p><p><strong>自我描述</strong>大概是对自己的其他能力的总结,完全看简历篇幅,项目太多就不用了,项目少我觉得还是有必要的。</p><p><strong>感谢环节</strong>就是表忠心的环节,也不确定 HR 或面试官看了会不会觉得轻松一刻。</p><h3>让技术官满意</h3><p>如果 HR 过了,可能会让你做笔试了,笔试通过就到了技术官(大概率就是你的同事或者直系领导)手里。如果没有笔试,可能直接就到了技术官的手里。所以,简历让技术官满意才是重中之重。</p><h4>那技术官怎么会满意呢?</h4><ul><li>项目要有亮点</li><li>项目要有深度</li><li>项目要有自己的理解</li></ul><blockquote>残酷的现实在于你做什么项目,面试官都可能都会觉得 low!</blockquote><p>重要的事情说三遍。项目最重要,真实面试时候,技术官为了节约时间,客套的让你稍微快速自我介绍,之后就直接项目面试。</p><p>我面试的时候,真的还有面试官让你自我介绍环节都省了。面试官直接问:我看了你做了 XX 项目,说一下你怎么做的?我:???(内心 OS:我还没反应过来呢,也让我先介绍一下我自己...)</p><h4>技术官满意简历的亮点</h4><ul><li>对技术的熟悉度;对技术的深度或思考的深度</li><li>高并发 -- 分片和负载均衡</li><li>高可用 -- 主备、主从、主备/主从切换、集群、分区技术</li><li>高性能 -- 比如亿级流量怎么办?</li></ul><blockquote>虽然秒杀系统被网上的人做烂了,但为什么还是大家选择要做这个项目的原因。即使你的简历不做,面试官还是会问有没有相关的经验。原因就一个字:卷!</blockquote><h4>项目面试的核心:熟悉自己的简历项目!</h4><p>比如 Java 后端来说,下面的东西如果写进了简历,最好要熟悉:</p><ul><li>Java基础:集合、反射</li><li>Java进阶:多线程、多进程、并发</li><li>框架:SpringMVC、SpringBoot、Dubbo</li><li>数据库技术:MySQL、Redis、MyBatis、Druid</li><li>计算机网络:TCP/IP、HTTP</li><li>中间件:RabbitMQ、RocketMQ</li><li>其他:...</li></ul><p>我觉得技术点不是越堆越多,就一定越好。技术往往层出不穷,但是底层的交互逻辑往往亘古不变。</p><p>当年本科做 Web 开发还是 JSP、然后是 SSH、SSM、SpringMVC、SpringBoot、SpringCloud...</p><p>现在不用 Spring 家族的产品都不好意思说自己是一个 Java 开发。</p><p>所以,吃透一门技术才是核心,往深度去学,然后再去思考横向对比一下不同技术的优劣。</p><p>如果面的 Python 后端,面试就可能会考察这些:</p><ul><li>Python 基础:Python 数据类型(列表、、字典、集合、元组)、生成器、迭代器...</li><li>Python 进阶:深浅拷贝、参数传递、协程、闭包、Lambda...</li><li>Python 框架:Flask、Django、Tornado...</li><li>通用的后端技术:数据库、中间件</li><li>其它技术:自动化测试、数据分析、机器学习(如果会最佳)</li></ul><p>总结一下,所以项目面试该注意的几个点:</p><ul><li>为什么开发这个项目?体现你对项目的思考,做这件事是有意义的</li><li>能说清楚哪里用了什么技术?技术的核心是什么?可能这就是为什么大厂会喜欢问有没有看过源码</li><li>为什么用这个技术?体现你对该技术的思考,用了它带来什么好处,用其他横向对比技术行不行?</li><li>项目开发的难点,怎么解决的?遇到了什么样的 Bug,然后怎么想办法解决的?</li><li>其他考察点</li></ul><p>另外,项目面试中可能也会考察一下手撕代码,那个可能就需要真正的刷题了,留到算法篇再说。</p><h3>让领导满意</h3><p>简历上除了写技术名词和项目,对于后面的三面/四面的领导,这时候就差临门一脚。</p><p>简历上一定要体现你的人才特质,建议放到个人描述上:</p><ul><li>学习能力</li><li>业务本领</li><li>技术热情</li><li>沟通能力</li><li>钻研能力</li><li>动手能力</li><li>团队合作能力</li><li>长久工作能力</li></ul><p>有些人觉得自己的项目够漂亮,够自信,<strong>个人描述</strong>这一部分也的确可以省略。</p><p>但是对于大多数没有十足信心的人来说,加上会使得简历更丰满一点。看个人喜欢</p><ul><li><a href="https://link.segmentfault.com/?enc=LmEU5yJzyZOdiYke7UVNSw%3D%3D.mdXigMGPjQhbZGBMoGPQRUHUdLj%2FCsUNroo1pupjQPXqwtOICqe5UJUW%2FB31%2BvZH" rel="nofollow">awesome-resume</a>:如果你不知道怎么描述项目,这个项目教你怎么写技术介绍</li></ul><p>Tips:除了做好简历导出为 PDF 准备在网站上投递以外,还可以导出为 HTML,然后配合 Github Page 或者 Gitee 上线,装逼得装到位一点。至少 Cyc2018 郑永川大哥做的简历就挺牛的,点<a href="https://link.segmentfault.com/?enc=%2FEnNfpOEjhVejruViPMwPQ%3D%3D.%2FwuJAJar5va9UodUTZbshryPVhhzYiovXGARIty7r84tltMkc%2F2%2FSTGoagU1oZs7" rel="nofollow">此处</a>看看。</p><h3>想做什么不重要,能做什么才重要</h3><p>上面是对技术简历制作的介绍,接下来谈谈 Offer 选择的问题。</p><p>相信很多小伙伴经过多轮笔试和面试的考验,到手拿了很多 Offer 了,到了进行 Offer 选择的阶段时,最后不得不问自己或朋友,又或者去网上发帖咨询:该去 A 还是 B,我该如何做选择呢?</p><p>高薪不稳定 vs 低薪稳定,应该怎么选?</p><p>程序员 35 岁容易被淘汰?</p><p>考公务员?回家当老师?</p><p>相信你也一定被上述各种声音所包围,我到底该去哪?到底该怎么做选择?</p><p>职业生涯的起初,第一份工作真的很重要,第一份工作往往能够确定今后的职业生涯。这个时候,你要明白一个道理:<strong>想做什么不重要,能做什么才重要</strong>。</p><p>大家都想拿“钱多事少离家近”的工作,可是这样的工作一大堆人挤破头想去,又有那么容易轮到自己吗?</p><p>那还不如问问自己:</p><ul><li><strong>我能做什么?</strong></li><li><strong>为了你想要的,你需要放弃什么?</strong></li><li><strong>如何能做到自己想做的?</strong></li></ul><p>拿开发举例,相信大家也看到,成为开发的门槛越来越低,大多数同学看中了互联网公司的高薪,转行成为互联网公司的一员。因此,公司技术人员的素养越来愈高,也越来越卷了,面试难度也上来了。因此能不能做开发还真不是像之前那么容易了。</p><p>就算你进了公司,也发现比不过同学,可能你也不一定能胜任开发的工作。我同学就有工作半年也从腾讯离职的,所以还真不是外界描绘的那样美好。</p><p>同样,身边也有公务员的同学会觉得生活也会枯燥;有老师的同学也有学生成绩的压力,有班主任的考核,甚至也需要经常加班,朝 7 晚 10,还有家长的各种电话。</p><p><strong>行业就像一座围城,外面的人想进来,里面的人想出去。</strong></p><p>既然这样,有没有好的方法让我决定自己该去哪呢?除了薪资,我们还应该关注哪些点呢?</p><h3>看城市</h3><p>每座城市都有它的特点。而这些特点如果不是生活在那里的人,根本也感受不到。最关键的就是吃喝住行,就办公城市而言,需要考虑出行、房租、下班生活等特点:</p><ul><li>有没有地铁到公司?</li><li>打车方便吗?</li><li>如果自己买车了,能开车上班吗?</li><li>租房贵不贵?</li><li>如果下班了还有没有休闲放松空间呢...</li></ul><p>大城市更有一个隐藏的优势是:你从大城市待累了可以选择回家,但是如果一开始选择回家,可能再想出来就没那么容易了。城市选对了,后面错了影响不大,但是城市选错了,后面的努力可能事倍功半。</p><p>选择一个高速发展的城市,薪资高、机会多,个人成长速度也快。拿我自己举例,在广州读的大学,投的公司几乎也是广东省内的公司,我个人是根据城市的发展和文化包容性来决定自己办公城市的。</p><ul><li>城市有发展有众多的公司和机会,意味着自己今后也有机会可以尝试不同的公司甚至不同的行业。</li><li>文化包容性就是在这里你能看到生活的多样性,能迅速找到自己的志同道合的人。</li></ul><blockquote>像我喜欢看书就能在深圳体会到各种图书馆的便利性,各种书城读书会的乐趣。喜欢宠物,也能在这个城市看到各种品种的狗狗,还可以逗逗。</blockquote><h3>选行业</h3><p>2020 年互联网教育突飞猛进,就像去年选了教育的以为进入了红利期,谁能想到 2021 年直接大量裁员,对应届生来说可能就容易被影响。第一份工作还没来得及上手,就失去了应届生的身份。</p><p>时至今日,互联网的红利期已经过了大半,去一家初创公司拿期权,公司飞速发展,实现财务自由的可能性越来越小。</p><p>针对计算机专业的同学来说,如果能去互联网,尽量不要去传统行业,毕竟互联网公司的实力摆在那里,产品都、业务更新快,成长也快。</p><p>选择互联网行业的话,最好去一些用户量大,甚至在未来十年也还能有发展的行业,比如直播,电商,游戏等行业,了解未来社会和技术的趋势,从而构建更完善的思维认知,也有机会在后面突破职业壁垒。</p><h3>选公司</h3><p>选大公司,所有的资源都在向大公司倾斜,优秀的人、成熟的机制、更成熟的业务。未来的两极分化会越来越严重,中小公司越来越难,而大公司越来越有头部优势。不管怎样,能去大厂,就尽量不要去中小型公司;</p><p>大平台决定了与你工作的人,大概率从能力和素质上都是比较厉害的人,跟着这样的人在一起工作,确实能够学习到更多的东西,提升更快。</p><p>不能选大公司,选薪资高的。薪资高的可能不一定代表什么,但是是对你的认可;月薪高的优于年终奖多的。总之一句话,选择一个能够给自己最多现金流的,而不是选择一个所谓最有前景的公司,所有的 HR 都会告诉你自己的公司很有前景。</p><p>选公司中选团队也很重要。有没有好的创始人,有没有大佬,一般优秀的人都是互相吸引的。一个好的团队就算领导他跳槽了,也是会想着如何带着你的。</p><h3>做选择的方法</h3><p>没有一个完美的工作,任何工作必须是为了让自己能找到一个平衡点,让自己过的舒服点。如果能财富自由,还用打啥工呢?</p><p>我们必须对自己所期望得到的东西,进行轻重排序,从而选择那些能够给我们最重要东西的工作。有一个帮助做选择的方法叫做<strong>决策矩阵</strong>,可以做一个决策矩阵的表格,把自己会考虑的点都放进关键因素,然后把现有的选择放进去做对比,如下图:</p><p><img src="/img/remote/1460000043584222" alt="" title=""></p><p>最后选择出得分比较高的那个选择。如果还是不知道怎么选,求助圈内人吧,比如前辈,师兄师姐。还是不知道的话,去让别人帮你选,去牛客,脉脉发帖,甚至一些大佬的公众号加到大佬的微信去咨询。</p><h2>总结</h2><p>大公司内卷大家有目共睹,毕竟 996.ICU 和 Working Time 公司作息时间表两个项目的大火不是没有原因的。很多朋友在选择岗位的时候一定会考虑加班的问题。我也是不太喜欢强制加班,但是自愿加班却不抵触。</p><p>卷是认识常态,在大厂和小厂,各有各的卷。在卷中找到生活平衡吧,如果外企不加班,那就卷一年下一年去外企吧。</p><p>生活就像滚雪球,不直面问题,自己就会成为问题。</p><p>最后,如果你已经选定好了 Offer,那么就尽量调整好自己的心态,直面挑战。也别再听别人讲情怀了,打工是为了賺钱的。选着一个好的 Offer 让自己开心,让家人放心。</p><p>很多时候,我们跳槽,选行业,</p><p><strong>不仅仅是选择一份工作,也是选择一种生活方式。</strong></p><p>那么,起码要选一种我喜欢的生活方式。</p><p>想清楚这个问题,你可能就不会有那么多困扰了。</p><p><strong>即使我正在卷。</strong></p><p>最后附上简历制作工具:</p><ul><li><a href="https://link.segmentfault.com/?enc=vz7tW9Uqmk09rnM%2Bt9%2FSbQ%3D%3D.n51v1xr0IsrImMht623s9phX%2BM0w1jPgOy4jSFsYxH4%3D" rel="nofollow">一纸简历</a></li><li><a href="https://link.segmentfault.com/?enc=6lQ1vCauxVewNxjiVk7dXw%3D%3D.qWshWiSJ1nMpytG6R1K0%2FGlbuGVaQN8kT%2Fcf%2FPtXc%2Bg%3D" rel="nofollow">在线Markdown书写工具</a>:冷熊简历</li><li><a href="https://link.segmentfault.com/?enc=Lsyij6XQvs8cFivvzvGXIA%3D%3D.N0K4jT8xpf9%2FuKeSAMss8OEEC4CfKn%2BOxwW8Ef7v7qA%3D" rel="nofollow">Markdown简历排版工具</a>:有四个模板</li><li><a href="https://link.segmentfault.com/?enc=bRf%2FZdLoSrrcgKXzm8c1CA%3D%3D.KDXW%2F5TDMyG%2B0hJX2napWzWvUyL3VE0WeTettChzI2AY1ddqI7w%2Fj46i%2FKgwUJVP" rel="nofollow">Markdown-Resume</a>:结合 Typora 使用最佳</li><li><a href="https://link.segmentfault.com/?enc=gbQa83Crh5g0XJuxvRW7Tg%3D%3D.mnFa0Q02Fk8s%2BBZOKzD7VlORA6Ho2giZ646ERn4Ct2koIyQJo1CP%2BbgmBbi9ZyQQ" rel="nofollow">animating-resume</a>:一个会动的简历模板</li><li><a href="https://link.segmentfault.com/?enc=Uow8qPg2dMVL6oaAqOLQGg%3D%3D.PBJau2XbNYeSR6HOi%2F4mm%2BHXvPUPSmp7a4wMhFlhpj9R2ibXVFDF0%2FJRfTuZhUtY" rel="nofollow">resumake</a>:用 LaTeX 做简历,科研狗应该懂得</li><li><a href="https://link.segmentfault.com/?enc=mt37pp07Kl1OrIO7igrrcQ%3D%3D.HUElzgw13RAY9yKReAavJJAlGi84%2FHIyQPJ4l%2F%2Fw4BxpSvbBVr2%2FNPXYWlUpGpHT" rel="nofollow">木及简历</a>、 <a href="https://link.segmentfault.com/?enc=8CPCCqkxVqhrlNKe%2B4nCPQ%3D%3D.%2BULDMVGz7pXRg1tv9EO6zN7P4nMgRRWoFz7pnHM9TFOfKbdQVQoIzT2qOsPNRWH4" rel="nofollow">超级简历</a>、<a href="https://link.segmentfault.com/?enc=27I4iymznRwyXPoIbGVMMA%3D%3D.SeUAr%2FKYEKtInrqlutn1MQc48w%2FddfLraLubFqMA0DM%3D" rel="nofollow">简历本</a>、<a href="https://link.segmentfault.com/?enc=E7wLPo0l5Th3Bks4dUGWfQ%3D%3D.n5aBuK4Q5jZbsBVTB3X6z4dm5RQScBnGqG7nJRuIedA%3D" rel="nofollow">五百丁</a>、<a href="https://link.segmentfault.com/?enc=LJIRSDIwgi01vnRu3XDlMg%3D%3D.ZKl%2BK6labkZsiUYwVYXPOBiSAF6EN1VVIJ6jtlbgZwk%3D" rel="nofollow">乔布简历</a></li></ul><blockquote>本文参与了<a href="https://segmentfault.com/a/1190000043508712">SegmentFault 思否面试闯关挑战赛</a>,欢迎正在阅读的你也加入。</blockquote>
Go 语言解析 JSON
https://segmentfault.com/a/1190000043291457
2023-01-10T15:55:25+08:00
2023-01-10T15:55:25+08:00
宇宙之一粟
https://segmentfault.com/u/yuzhoustayhungry
1
<p>本文将说明如何利用 Go 语言将 JSON 解析为结构体和数组,如果解析 JSON 的嵌入对象,如何将 JSON 的自定义属性名称映射到结构体,如何解析非结构化的 JSON 字符串。</p><h2>JSON 解析为结构体</h2><p>JSON 的结构是 key-value,最直观的就是将 JSON 解析为结构体,如下 JSON :</p><pre><code>{
"name": yuzhou1u,
"age": 18
}</code></pre><p>Go 语言中,提供了一个专门的包 <code>encoding/json</code> ,所以我们在使用这个 JSON 包之前需要在头文件导入:</p><pre><code>package main
import (
"encoding/json"
"fmt"
)</code></pre><p>然后,我们需要定义一个 Go 语言的结构体以便我们能与 JSON 一一对应,比如在 JSON 中我们定义了姓名 <code>name</code> 和年龄 <code>age</code> ,所以需要定义一个结构体(命名可以随意,但最好通俗易懂)的字段与 JSON 字符串中的键相匹配:</p><pre><code>type Person struct {
Name string
Age int
}</code></pre><p>然后使用 <code>json.Umarshal()</code> 函数来解析 JSON 字符串,完整代码如下:</p><pre><code>package main
import (
"encoding/json"
"fmt"
)
type Person struct {
Name string
Age int
}
func main() {
var p Person
jsonString := `{"name": "yuzhou1su",
"age" : 18}`
err := json.Unmarshal([]byte(jsonString), &p)
if err == nil {
fmt.Println(p.Name)
fmt.Println(p.Age)
} else {
fmt.Println(err)
}
}</code></pre><p>现在来解释一下上面 main 函数的代码:</p><ul><li>定义一个 Person 的 p 对象</li><li>因为我们没有把文件系统使用上,所以是定义了一个 <code>jsonString</code> 的 JSON 数据</li><li>使用 <code>json.Unmarshal()</code> 函数能够解析 JSON 格式的数据。但需要将 JSON 字符串转换为字节切片,并将结果存储到 p 对象中。 使用需要使用 & 地址运算符传入人员的地址。</li><li>如果解析有效,则 <code>json.Unmarshal()</code> 函数返回 nil,您现在可以找到存储在 person 变量中的值。</li><li>确保将 Person 结构中每个字段的第一个字符大写。 如果字段名称以小写字母开头,则不会导出到当前包之外,并且字段对 <code>json.Unmarshal()</code> 函数不可见。</li></ul><p>运行上述代码,打印在控制台中结果为:</p><pre><code>yuzhou1su
18</code></pre><h2>JSON 解析为数组</h2><p>通常 JSON 数据会包括一系列的对象数组,就像这样一个班级的数据:</p><pre><code>[
{
"id": 1,
"name": "张三"
"age": 20
},
{
"id": 2,
"name": "李翠花"
"age": 18
},
{
"id": 3,
"name": "王老五"
"age": 25
}
]</code></pre><p>我们只需要定义一个 <code>students[]</code> 的数组,代码如下:</p><pre><code>package main
import (
"encoding/json"
"fmt"
)
type Student struct {
Id int
Name string
Age int
}
func main() {
var students []Student
myClass :=
`[
{
"id": 1,
"name": "张三",
"age": 20
},
{
"id": 2,
"name": "李翠花",
"age": 18
},
{
"id": 3,
"name": "王老五",
"age": 25
}
]`
err := json.Unmarshal([]byte(myClass), &students)
if err == nil {
for _, student := range students {
fmt.Print("\t\n", student.Id)
fmt.Print("\t", student.Name)
fmt.Print("\t", student.Age)
}
} else {
fmt.Println(err)
}
}</code></pre><p>使用 <code>for...range</code> 迭代数组,然后运行上述代码:</p><pre><code>$ go run main.go
1 张三 20
2 李翠花 18
3 王老五 25</code></pre><h2>解析 JSON 嵌入对象</h2><p>JSON 字符串有时包含嵌入对象,比如:</p><pre><code>{
"name": "yuzhou1su",
"age": 18,
"address": {
"road": "renmin south road",
"street": "123 street",
"city": "cs",
"province": "hn",
"country": "cn"
}
}</code></pre><p><code>address</code> 就是属于内嵌对象,我们同样需要创建另一个 <code>Address</code> 结构体:</p><pre><code>package main
import (
"encoding/json"
"fmt"
)
type Person struct {
Name string
Age int
Address struct {
Road string
Street string
City string
Province string
Country string
}
}
func main() {
var p Person
jsonString := `
{
"name": "yuzhou1su",
"age": 18,
"address": {
"road": "renmin south road",
"street": "123 street",
"city": "cs",
"province": "hn",
"country": "cn"
}
}`
err := json.Unmarshal([]byte(jsonString), &p)
if err == nil {
fmt.Println(p.Name)
fmt.Println(p.Age)
fmt.Println(p.Address.Road)
fmt.Println(p.Address.Street)
fmt.Println(p.Address.City)
fmt.Println(p.Address.Province)
fmt.Println(p.Address.Country)
} else {
fmt.Println(err)
}
}</code></pre><p>输出结果:</p><pre><code>yuzhou1su
18
renmin south road
123 street
cs
hn
cn</code></pre><h2>自定义属性名称的映射</h2><p>有时 JSON 字符串中的键不能直接映射到 Go 中结构的成员。 比如:</p><pre><code>{
"base currency": "USD",
"destination currency": "CNY"
}</code></pre><p>请注意,此 JSON 字符串中的键中有空格。 如果你尝试将它直接映射到一个结构,你会遇到问题,因为 Go 中的变量名不能有空格。 要解决此问题,您可以使用结构字段标记(在结构中的每个字段之后放置的字符串文字),如下所示:</p><pre><code>type Rates stuct {
Base string `json:"base currency"`
Symbol string `json:"destination currency"`
}</code></pre><ul><li>JSON 的 <code>base currency</code> 映射到 Go 中的 <code>Base</code> 字段</li><li>JSON 的 <code>destination currency</code> 映射到 Go 中 <code>Symbol</code></li></ul><p>整合如下:</p><pre><code>package main
import (
"encoding/json"
"fmt"
)
type Rates struct {
Base string `json:"base currency"`
Symbol string `json:"destination currency"`
}
func main() {
jsonString := `
{
"base currency": "USD",
"destination currency": "CNY"
}`
var rates Rates
err := json.Unmarshal([]byte(jsonString), &rates)
if err == nil {
fmt.Println(rates.Base)
fmt.Println(rates.Symbol)
} else {
fmt.Println(err)
}
}</code></pre><p>运行如下代码:</p><pre><code>$ go run main.go
USD
CNY</code></pre><h2>非结构化数据的映射</h2><p>前面几节展示了相对简单的 JSON 字符串。 然而,在现实世界中,您要操作的 JSON 字符串通常很大且非结构化。 此外,您可能只需要从 JSON 字符串中检索特定值。</p><p>考虑以下 JSON 字符串:</p><pre><code>{
"success": true,
"timestamp": 1588779306,
"base": "USD",
"date": "2022-01-15",
"rates": {
"BNB": 0.00225,
"BTC": 0.000020,
"EUR": 0.879,
"GBP": 0.733,
"CNY": 6.36
}
}</code></pre><p>如果我们还想把美元解析为其他币种,不至于重新定义整个结构体,可以采取定义一个接口:</p><pre><code>var result map[string] interface{}</code></pre><p>上面的语句创建了一个 map 类型的变量 result,它的 key 是 string 类型,每个对应的 value 都是 interface{} 类型。 这个空接口表示该值可以是任何类型:</p><p>为了解析这个 JSON 字符串,我们应该使用 <code>json.Unmarshal()</code> 函数:</p><pre><code>json.Unmarshal([]byte(jsonString), &result)</code></pre><p>因为 result 的类型是接口,所有可以传入任何类型:</p><ul><li>当解析 success 键的话可以使用 <code>result["sucess"]</code>,解析为布尔型。</li><li>当解析 <code>timestamp</code> 时可以解析为数字类型</li><li>解析 rates 使用传入 <code>rates</code> 即可, 即 <code>rates := result["rates"]</code>,解析为 map 类型</li></ul><p>整个代码如下:</p><pre><code>package main
import (
"encoding/json"
"fmt"
)
type Rates struct {
Base string `json:"base currency"`
Symbol string `json:"destination currency"`
}
func main() {
jsonString := `
{
"success": true,
"timestamp": 1588779306,
"base": "USD",
"date": "2022-01-15",
"rates": {
"BNB": 0.00225,
"BTC": 0.000020,
"EUR": 0.879,
"GBP": 0.733,
"CNY": 6.36
}
}`
var result map[string]interface{}
err := json.Unmarshal([]byte(jsonString), &result)
if err == nil {
fmt.Println(result["success"])
rates := result["rates"]
fmt.Println(rates)
} else {
fmt.Println(err)
}
}</code></pre><p>运行代码如下:</p><pre><code>$ go run main.go
true
map[BNB:0.00225 BTC:2e-05 CNY:6.36 EUR:0.879 GBP:0.733]</code></pre><h2>总结</h2><p>JSON 数据作为常见的数据格式,有着非常多的使用场景。本篇文章介绍了如何利用 Go 语言来解析 JSON 数据,如解析为结构体、数组、嵌入对象,解析自定义字段和解析非结构化数据。下一篇文章将介绍一下如果将 Go 语言的数据编码为 JSON 数据,敬请期待!</p>
Go 读取文本文件的三种方式
https://segmentfault.com/a/1190000043291434
2023-01-10T15:51:37+08:00
2023-01-10T15:51:37+08:00
宇宙之一粟
https://segmentfault.com/u/yuzhoustayhungry
0
<h2>Go 读取文本文件</h2><p>工作中时不时需要读取文本,文本文件是最常见的文件类型。</p><p>本文将从逐行、逐个单词和逐个字符三个方法读取文件:</p><ul><li><code>byLine.go</code></li><li><code>byWord.go</code></li><li><code>byCharacter.go</code></li></ul><h3>1 逐行读取文本文件</h3><p>逐行读取文件是最为常见的文本文件,也是最为简单的方式。首先我们需要导入几个常见的包:</p><ul><li>bufio:缓存区读写文件</li><li>flag:命令行参数解析</li></ul><pre><code>package main
import (
"bufio"
"flag"
"fmt"
"io"
"os"
)
func lineByLine(file string) error {
var err error
f, err := os.Open(file)
if err != nil {
return err
}
defer f.Close()
r := bufio.NewReader(f)
for {
line, err := r.ReadString('\n')
if err == io.EOF {
break
} else if err != nil {
fmt.Printf("error reading file %s", err)
break
}
fmt.Print(line)
}
return nil
}
func main() {
flag.Parse()
if len(flag.Args()) == 0 {
fmt.Printf("usage: byLine <file1> [<file2> ...]\n")
return
}
for _, file := range flag.Args() {
err := lineByLine(file)
if err != nil {
fmt.Println(err)
}
}
}</code></pre><p>代码解释:</p><ul><li>主要通过 <code>bufio.NewReader()</code> 函数生成一个新的读取器;</li><li>随后,在 <code>bufio.ReadString()</code> 函数读取字符,通知该函数持续执行读取任务,直到碰到该 "\n" 参数,也就是换行符。读到换行符,执行文本输出。</li><li>如果读取中断了,即 <code>err == io.EOF</code> ,退出文件读取</li><li>或者 <code>err != nil</code>, 打印错误提示,退出文件执行</li></ul><p><code>main()</code> 函数中首先读取命令行参数,如果命令行长度为 0,即没有传入要读取的文件,如果此时执行 <code>byLine.go</code> 文件的话就会给出语法提示,如下:</p><pre><code>$ go run byLine.go
usage: byLine <file1> [<file2> ...]</code></pre><p>我们写一个测试的文本文件 <code>test.txt</code>, 写入如下几行数据,记得在第二行换行(加入空行):</p><pre><code>这是第一行
我是第二行</code></pre><p>运行如下命令后,结果为:</p><pre><code>$ go run byLine.go test.txt
这是第一行
我是第二行</code></pre><p>可以使用 <code>cat test.txt</code> 校验我们的结果的准确性,如下:</p><pre><code>$ cat test.txt
这是第一行
我是第二行</code></pre><h3>2 逐个单词读取文本文件</h3><pre><code>package main
import (
"bufio"
"flag"
"fmt"
"os"
)
func wordByWord(file string) error {
var err error
f, err := os.Open(file)
if err != nil {
return err
}
defer f.Close()
scanner := bufio.NewScanner(f)
scanner.Split(bufio.ScanWords)
var words []string
for scanner.Scan() {
words = append(words, scanner.Text())
}
for _, word := range words {
fmt.Println(word)
}
return nil
}
func main() {
flag.Parse()
if len(flag.Args()) == 0 {
fmt.Printf("usage: byWord <file1> [file2> ...]\n")
return
}
for _, file := range flag.Args() {
err := wordByWord(file)
if err != nil {
fmt.Println(err)
}
}
}</code></pre><p>代码解释:</p><ul><li>其他代码都和 <code>byLine.go</code> 函数一样,主要是利用了 <code>bufio</code> 中的 scanner 来扫描单词,</li><li><code>scanner := bufio.NewScanner(file)</code> 用来扫描读取的文件</li><li><code>scanner.Split(bufio.ScanWords)</code> 用来分割单词</li><li>声明一个单词字符串列表,将读取到的每一个单词放入这个列表中</li><li>循环遍历单词字符串列表,打印每一个单词</li></ul><h4>测试代码</h4><p>写入一个 <code>test.txt</code> 文件:</p><pre><code>Hello World
1 2 3 </code></pre><p>运行代码,结果显示:</p><pre><code>$ go run byWord.go test.txt
Hello
World
1
2
3</code></pre><h3>3 逐个字符读取文本文件</h3><p>逐个字符读取文本的使用场景还是很少,除非开发一个文本编辑器。新建一个 <code>byCharacter.go</code> 文件,然后写入如下代码:</p><pre><code>package main
import (
"bufio"
"flag"
"fmt"
"io"
"os"
)
func charByChar(file string) error {
var err error
f, err := os.Open(file)
if err != nil {
return err
}
defer f.Close()
r := bufio.NewReader(f)
for {
line, err := r.ReadString('\n')
if err == io.EOF {
break
} else if err != nil {
fmt.Printf("error reading file %s", err)
return err
}
for _, x := range line {
fmt.Println(string(x))
}
}
return nil
}
func main() {
flag.Parse()
if len(flag.Args()) == 0 {
fmt.Printf("usage: byWord <file1> [file2> ...]\n")
return
}
for _, file := range flag.Args() {
err := charByChar(file)
if err != nil {
fmt.Println(err)
}
}
}</code></pre><p>运行测试用例得出的最后结果为:</p><pre><code>$ go run byCharacter.go test.txt
H
e
l
l
o
W
o
r
l
d</code></pre><h3>总结</h3><p>本文主要介绍 Go 中的 <code>bufio</code> 包,有些情况下,我们并不只是需要读取整个一大段文件,所以需要把文件通过某种方式读取,并介绍了 Go 读取文本文件中的三种方法:</p><ul><li>逐行读取文本文件 <code>byLine.go</code></li><li>逐个单词读取文本文件 <code>byWord.go</code></li><li>逐个字符读取文本文件 <code>byCharacter.go</code></li></ul><p>其实还有更多读取文本文件的方法,比如通过逗号读取、读取特定数据量的文本,这些方法留到后文再作介绍,下一篇文章见!</p>
Go 容器之数组
https://segmentfault.com/a/1190000043291411
2023-01-10T15:49:12+08:00
2023-01-10T15:49:12+08:00
宇宙之一粟
https://segmentfault.com/u/yuzhoustayhungry
0
<p>在 Java 的核心库中,集合框架可谓鼎鼎大名:<code>Array</code> 、<code>List</code>、<code>Set</code>、<code>Queue</code>、<code>HashMap</code> 等等,随便拎一个出来都值得开发者好好学习如何使用甚至是背后的设计源码(这类文章也挺多,大家上网随便一搜)。</p><p>虽然 Go 语言没有如此丰富的容器类型。</p><h2>1 序列容器</h2><p>序列容器存储特定类型的数据元素。目前有 5 种序列容器的实现:</p><ul><li><code>array</code></li><li><code>vector</code></li><li><code>deque</code></li><li><code>list</code></li><li><code>forward_list</code></li></ul><p>这些序列容易可以用顺序的方式保存数据,利用这些序列容易能够编写有效的代码,重复使用标准库的模块化。</p><h3>1.1 数组</h3><p>Go 语言中的数组类型有点类似 C++ 中的数据,Go 的数组初始化定义后,在编译时就不会再变更。</p><p>定义数组的方式如下:</p><pre><code class="go">var a [10]int
b := [5]string {"H", "e", "l", "l", "o"}</code></pre><p><code>[n]T</code> 类型就表示含有 <code>n</code> 个类型为 <code>T</code> 的数组,本例中就是 a 变量表示含有 10 个 int 类型的整型数组;b 变量表示含有 5 个 string 类型的字符串数组。<br>数组的长度作为其类型的一部分,因此数组的长度是无法调整的。</p><pre><code class="go">package main
import "fmt"
func main() {
var a [10]int
a[0] = 2022
a[1] = 2023
fmt.Println(a[0], a[1])
fmt.Println(a)
b := [5]string {"H", "e", "l", "l", "o"}
fmt.Println(b)
}</code></pre><p>运行结果如下:<br><img src="/img/remote/1460000043291414" alt="image.png" title="image.png"></p><p>Go 语言中,数组是一个长度固定的数据类型,用于存储一段具有相同类型元素的序列(连续块)。在底层中,数组占用的内存是连续的,所以访问起来速度非常块,还可以根据任意的索引找到相应的数据。</p><h3>1.2 数组的结构</h3><p>数组的内部结构有点类似下图,由一个个连续的内存空间组成:</p><p><img src="/img/remote/1460000043291415" alt="" title=""></p><h3>1.3 数组定义</h3><pre><code class="go">var demo[5] int // 数组m demo: 包含5个整型元素</code></pre><p>先声明一个名为 <code>demo</code> 的数组,能够存 5 个 <code>int</code> 类型的值,然后我们给数组存入不同的值,尝试运行如下代码:</p><pre><code class="go">package main
import "fmt"
func main() {
var demo [5]int
demo[0] = 10
demo[1] = 20
demo[2] = 30
demo[3] = 40
demo[4] = 50
fmt.Println(demo)
}</code></pre><p>在终端运行后将会得到:<code>[10 20 30 40 50]</code>。可以看到,数组的索引是从 0 开始,如果数组长度为 5 的话,数组索引只会到 4 。</p><h3>1.4 数组简洁创建方式</h3><p>如果一个一个给数组不同索引单独赋值的方法非常复杂, Go 提供了一种快速创建数组并初始化的方式:使用数组字面量直接赋值,如:</p><pre><code class="go">arrayDemo := [5]int{10, 20, 30, 40, 50} // 声明长度为5的数组并用数值初始化每个元素</code></pre><p>还可以用 <code>... </code> 替代数组的长度,Go 语言会根据初始化数组元素的数量来确定数组的长度,如:</p><pre><code class="go">array := [...]int{10, 20, 30, 40, 50} // 声明整型数组并用数值初始化,数组长度由初始化的数量决定</code></pre><p>如果想指定具体索引位置的值,可以采用:</p><pre><code class="go">array := [5]int{1: 10, 4: 100}</code></pre><h3>1.5 根据索引修改元素值</h3><pre><code class="go">arrayDemo := [5]int{10, 20, 30, 40, 50}
arrayDemo[1] = 15 // 修改索引为2的元素的值,即把20改为15</code></pre><h3>1.6 数组复制</h3><pre><code class="go">var array1 [5]string
array2 := [5]string{"Zhao", "Qian", "Sun", "Li", "Zhou"}
array1 = array2 // 复制array2给array1</code></pre><p>此时,<strong>array1</strong> 和 <strong>array2</strong> 两个数组的值完全一样。</p><pre><code class="go">package main
import "fmt"
func main() {
var array1 [5]string
array2 := [5]string{"Zhao", "Qian", "Sun", "Li", "Zhou"}
array1 = array2
fmt.Println("array1 = array2 is:", array1 == array2) // array1 = array2 is: true
}</code></pre><h3>1.7 数组运算</h3><p>当我们把数据都保存在数组里后,比如考试分数。要求把保存在数组里的值全加起来,先算<strong>总和</strong>,然后算一下<strong>平均分</strong>,可以使用如下的代码:</p><pre><code class="go">package main
import "fmt"
func main() {
demo := [5]int{10, 20, 30, 40, 50}
total := 0
average := 0
for i := 0; i < len(demo); i++ {
total += demo[i]
}
average = total / len(demo)
fmt.Println("Total: ", total)
fmt.Println("Average: ", average)
}
// Total: 150
// Average: 30</code></pre><p>如果使用 for..range 循环会发生什么呢?</p><pre><code class="go">package main
import "fmt"
func main() {
demo := [5]int{10, 20, 30, 40, 50}
total := 0
average := 0
for i, val := range demo {
total += val
}
average = total / len(demo)
fmt.Println("Total: ", total)
fmt.Println("Average: ", average)
}
// 运行后得到:
// # command-line-arguments
// arrays\main.go:11:6: i declared but not used</code></pre><p>Go 编译器不允许您创建从未使用过的变量。 由于我们不在循环中使用 <strong>i</strong> ,因此需要将其更改为下划线 <code>_</code> (代表空)。</p><pre><code class="go">package main
import "fmt"
func main() {
demo := [5]int{10, 20, 30, 40, 50}
total := 0
average := 0
for _, val := range demo {
total += val
}
average = total / len(demo)
fmt.Println("Total: ", total)
fmt.Println("Average: ", average)
}</code></pre><p>单个 <code>_(下划线)</code>用于告诉编译器我们不使用它。 (在这种情况下,我们不需要迭代器变量)。</p><h3>1.8 多维数组</h3><p>定义与操作基本类似与一维数组:</p><pre><code class="go">var array [4][3] int // 声明一个二维数组,4行3列</code></pre><p>对二维数组进行操作:</p><pre><code class="go">package main
import "fmt"
func main() {
var array1 [2][3]int
array2 := [2][3]int{
{1, 2, 3},
{4, 5, 6},
}
fmt.Println(array2)
fmt.Println(array2[1][2])
array2[1][2] = 100 // 设置第二行第三列的值为100
fmt.Println(array2)
array1 = array2 // 将array2的值复制给array1
fmt.Println(array1)
fmt.Println("array1 = array2 is:", array1 == array2)
}</code></pre><p>运行该代码,结果为:</p><pre><code class="shell">[[1 2 3] [4 5 6]]
6
[[1 2 3] [4 5 100]]
[[1 2 3] [4 5 100]]
array1 = array2 is: true</code></pre><h2>总结</h2><p>总结一下,数组的注意事项:</p><blockquote>If you don't understand the data, you don't understand the problem.</blockquote><ul><li>数组会占用连续的内存</li><li>数组有固定的类型和长度,还可以利用 <code>...</code> 推断数组长度</li><li>访问数组元素很方便,速度较快,复杂度为 O(1)</li></ul>
人生的喜悦、不快与成长,都在那一篇篇的文字中得到记录 | 2022 年终总结
https://segmentfault.com/a/1190000043200495
2022-12-29T23:12:11+08:00
2022-12-29T23:12:11+08:00
宇宙之一粟
https://segmentfault.com/u/yuzhoustayhungry
3
<p>又是一年的总结,不知道自己今年又该写点什么。但提笔总是好的,也算对今年的一个交代和对未来的一份期许。</p><p>窗外的阳光正好,对面楼的敲打声叮叮咚咚,窗台上的两只猫睡得依旧奔放和舒适。这样一个看似美好的下午,一个平凡的一天。</p><p>托着刚刚才好起来的身体,动笔写下这一年的小小总结,没有飞黄腾达,也没有跌入谷底。在那全身痛的夜晚,脑子如麻一般搅在一起的时刻,醒来一直在思考今年到底都做了什么,回顾起来又有哪些值得记录的事情呢。</p><h2>如此世界杯,怎么不爱?</h2><p>最先映入回忆的莫过于卡塔尔世界杯,如今再回头看,依然想不起来这一个月是如何度过的:梦幻般的开始,梦幻般的结束。顶着咖啡度过了好几个熬夜看球后的上午,多少次跟同事讨论一个个刺激而又扣人心弦的比赛,每天刷着相关的咨询和集锦,回味无穷。</p><p><img src="/img/bVc5qAg" alt="973c293223714446ae9dfc62c67f746f.jpeg" title="973c293223714446ae9dfc62c67f746f.jpeg"></p><p>为什么如此热爱世界杯?大概就是总得有一个理由会让你能够聚集众多朋友,享受这吃着烧烤喝着啤酒的时刻;大概青春最美好的样子就是见证传奇的落幕与圆梦;大概就是工作之余能够纯粹享受的那一个个值得熬夜的只属于自己与热爱的时光。</p><p>不知道多少次因为看球的激动而狂发的朋友圈,不记得多少次看阿根廷的比赛如同过山车般的心跳加快,万幸最好的结局是见证了煤老板的夺冠的欣喜与激动。正如贺炜的解说词一般:</p><blockquote>“四年前陪你看球的人现在还在联系吗?四年后看球的自己许过的愿望都实现了吗?我们为什么热爱着足球这项运动,因为它不仅展示了球员们励志的奋斗故事,也寄托了我们普通人平凡生活中的英雄梦想。”</blockquote><p>四年前还是一个刚刚考上研的年轻人,一路跌跌撞撞,依旧充满着对未来的憧憬与迷茫。也曾意气风发,也曾颓丧消沉。如今已经是一个工作两年的职场人:起床,上班,下班,刷碗,睡觉,偶尔看看书,刷刷视频,没有跌宕起伏的小说情节,没有可以展示的烟火星辰。</p><p>世界杯就像是没有意义的人生里闪现而过的火花,有人为了喜爱的球队,有人为了手中的彩票,有人凑着看热闹。总之,这场盛会成为了无意义生活的茶余饭后,成为了博主和 up 主的流量与热度。而我也处于其中,也乐在其中。</p><p><strong>球场分胜负,而人生永远都是乘胜前行与败而不馁,无论哪种结局,都是生命的重要组成与动能。</strong> 如此足球,怎能不爱?</p><p>对于平凡的人来说,也会逐渐明白和理解:失败也总是贯穿始终,这就是人生。就像罗曼罗兰曾说:真正的英雄,是那些看清了生活的真相却依然热爱生活的人。即使还没有实现平凡生活的梦想,但心中仍然充满着对生活的希望。祝愿每个平凡的个体都能做自己生活的英雄。</p><h2>在不确定的工作和就业寒冬下往前看</h2><p>今年年初的居家办公,与现在的我同处在家中的这样一个下午,谁都没有想到,前者阴后者是阳,形式没变,但政策变了。</p><p>年初的时候,暗自在心中考虑着给自己换一份工作,也不是因为公司或者同事不太好,而是总想着能去另一块看看风景。毕竟一直以来坚信的就是人挪活,总在一个舒适区待着也不知道究竟是好是坏。最后一场全城封闭的通知又让我打了退堂鼓。</p><p>还不断听着小伙伴的吐槽,安慰他说:没事,反正大家最终都会走的。最后小伙伴加入了另一家公司,而我还在原地坚持,导致后面的几次见面聊天都在调侃我:“这个人把我劝走了,自己还在 XX 待着”。</p><p>然后就是年度最凄冷的就业季和不景气的行业,各种裁员的消息,各种互联网寒冬。小伙伴们互相拉了个群吐槽,也都在预测自己什么时候被优化、被裁员,被毕业。</p><p>师弟也跟我抱怨这一届太惨了,根本都拿不到几个 Offer。连就业中心的老师都跟我说今年都没活干,封校办不了宣讲会和就业展会。</p><p>连我妈都在电话中,好几次问我说:“你们公司还发的起工资吗?” 我:“放心,发得起,还给我涨了点。” 可能在家长眼里,只有铁饭碗才会让人放心吧,或许每个工种的尽头都是体制。</p><p>我们今天面对的很多熟悉的事物已经失去了“确定性”,变得更具有“不确定性”,只要世界的改变没有慢下来,“不确定性”将是我们所有人面临的常态。</p><p>虽然说生活总是充满着不确定,但确实有几件确定的事可以值得一说:</p><ul><li>从年初的工作挨批到公司的 Best Performance Award,从遭受质疑到得到一点点奖金与鲜花,这大概就是成长带来的喜悦与骄傲。</li><li>从劳动节的购房到现在的硬装结束,确定有了一个新房,心里也是有了一点的底气,可能并没有那么满意的装修,但是总归有个地方是让人踏实的。</li><li>从年前的精神紧绷期望过高到年底的躺平,心态也已然发生了变化,面对环境提供的机会和不适变化的形势,人需要调整,在该进的时候进,该停的时候停,该躺的时候躺。</li></ul><p><img src="/img/bVc5qAh" alt="d025362921d079c4b66f3806c000135.jpg" title="d025362921d079c4b66f3806c000135.jpg"></p><p>人的一生总是有限,看似这一辈子总有很多的事情要做,往往重要的就那么几件。况且,再寒冬的日子也会过去,再不会做的考试,也有终考的一刻。</p><p>“<strong>天地生人,有一人应有一人之业;人生在世,生一日当尽一日之勤</strong>。” </p><p>可能有一天真的不再做程序员,但是我相信总会感谢这些写代码做优化的岁月,毕竟这是我目前有限时间内用来吃饭的家伙,也是为数不多的可以用来形容自己的标签——<strong>热爱技术的后端程序员</strong>。</p><h2>只为了参加活动到不为月更而更</h2><p>如果说我的工作像去年一样,实在没有什么可以值得骄傲的,今年唯一能够让我觉得可以拿来一说的趣事就是更文。没想到在这里能停留在 222 篇这个数字,这个数字很二,二可以是一种执着的态度,也是一种自嘲。可以说几乎今年的每一个月都有一些产出,终于体会到了坚持的力量——骐骥一跃,不能十步;驽马十驾,功在不舍。</p><p>回想起那无数个日日夜夜绞尽脑汁的日子,一开始真的只为了参加月更活动,从自己的过往的库存与学习笔记中总结出一篇篇文章,有时候真的也明白自己水了很多文章,生怕被别人评论和引来谩骂。</p><p>可能真的是水到深处自是精品,文章写多了也会反思自己究竟写作的目的是什么?如果说每一篇文章网上都能找到相关的内容,自己重复写的意义在哪?如果说每一个观点都是对前人的重复,这样的表达又还算真正的表达吗?</p><p>技术人可能真的相对于纯粹一点,希望能从网上找到精品,但又不知道如何创造一篇对他人有用的好文。</p><p>进入到下半年为了写出更好一点的文章,会不断琢磨与思考,我写这篇文章的意义在哪?到现在都会好好的看看优秀大佬的博客与文章,总结别人的经验,学习与自身技术栈不同的技术,学习别人的优秀写作风格。</p><p>幸运的是在“技术征文中奖名单公布 丨浅谈 Go 语言框架”活动中,拿到了第 8 名的排名,获得了《Go 语言设计与实现》这本书,也算是给自己最大的鼓励了,</p><p>今年我总是会抱怨自己的忙碌,但停下来之后又会在每个刷视频度过的下午感叹自己荒废了多少时光。</p><p>半年前那个深夜,2022 年 7 月 28 日完成的自己与自己的对话的年中总结——《<a href="https://link.segmentfault.com/?enc=QG5u4GIg1U8Q%2B9DauBkshQ%3D%3D.TjHjm%2BB8GDfSZXDCvuQU1MnmhZfp62rb8GWlOp9lTYSDVJju9pdh5LH5%2BQkkR%2FmAtMWx8bcHkk%2FgUZ3uFj8zcA%3D%3D" rel="nofollow">与自己对话,活在当下,每走一步都算数 | 年中总结</a>》,现在还记得创作这篇文章时的心路历程,这也是写作带给我的感觉,这大概就是真正的热爱文字才会选择在那个深夜坚持写完,才会在众多下班回到家的日子完成当天的更文。</p><p>下半年过完了,确实不如上半年般精彩,可能这也是这篇年终总结并没有什么精彩分享的原因。</p><p>如果你也像我一样,热爱写作,也可以看看我这篇 《<a href="https://link.segmentfault.com/?enc=jMBVSCUw7CvayhdnAt1CwA%3D%3D.5t7vFooK87mu77GazI06wjkHb9UFo21DyCb7QaVd5WNUu18%2BhjFhJlNcXsy7Y2vc" rel="nofollow">代码之外:写作是倒逼成长的最佳方式</a>》,终于能有一篇文章突破 5000 的阅读量了,这也算是实现了年初立下的 Flag 之一。希望明年能创造更多的优质文章和爆款文吧。</p><p><strong>人生的喜悦、不快与成长,都在那一篇篇的文字中得到记录。</strong></p><h2>一代又一代人,生命就像是往复的陀螺,兜兜转转</h2><p>写这篇文章的时候,除了感叹时光的迅速之外,又去反复看了去年的年中总结——《<a href="https://segmentfault.com/a/1190000041173853">代码之外:人生最大的幸运就是努力没有白费 -- 我的2021年度总结</a>》,那会我还是个刚离开校园的小萌新,今年已经搬离了公司宿舍,成为了公司的老员工了。</p><p>对比去年的自己:</p><ul><li>关于行万里路:去年去了鼎鼎大名呼伦贝尔,看了草原的广阔与风吹草低见牛羊;今年去了蓉城,没有在成都的街头走一走,却自己成为了一只“羊”。去年与小伙伴们去了珠海长隆,今年爬山与玩具展。</li></ul><p><img src="/img/bVc5qAi" alt="4f10c646e2f8852c47428e8aa10e85d.jpg" title="4f10c646e2f8852c47428e8aa10e85d.jpg"></p><ul><li>关于读万卷书:去年买了很多书,很多都是放着;今年读了大概 60 本书,虽然没有完成年初立下的 100 本书的小目标,也算很满意了。遗憾的是没有完成每一篇的读书笔记。希望有空能重读并填上这个坑。</li></ul><p><img src="/img/bVc5q3F" alt="" title=""></p><p>今年最值得推荐的书莫过于蔡崇达老师的《命运》,以下是我的读后感:</p><blockquote><p>时代之下,每个人的命运都如河流,时而平缓时而猛烈,但最后归宿都是死亡的海洋。多听听别人的故事,储存着,可以帮咱们自己过好这一辈子和下一辈子。</p><p>再烂的生活,日子也会过去,曾经再不会的考试题,也总会有交卷之时。没有压舱石的船容易翻,没有压舱石的人魂灵也容易打翻。</p><p>人用了一辈子又一辈子,以这一身又一身的皮囊,去装这一个又一个的故事。这人间从来没有生离,没有死别。这人间不过是,天上的人来了,天上的人回天上去了。</p></blockquote><ul><li>关于养的猫:去年的猫猫才几个月大,今年已经成为了两只让人刮目相看的肥猪。</li></ul><p><img src="/img/bVc5qAw" alt="bf8940e845550dcb8666e30ade04b99.jpg" title="bf8940e845550dcb8666e30ade04b99.jpg"></p><p>大概是只有故乡的冬天才有机会烧火烤,所以会格外想念。这几天考研结束,又是赶上圣诞的日子,又是一批追梦的人杀出了战场,仿佛思绪又回到了那年的冬天,在考完试的那晚走在依旧充斥热闹的校园,对未来的路充满着疑惑,一切会如愿吗?这段路只有自己走过之后,才会知道是否值得。看样子,如今也是一样,今后也依旧差不多。</p><p><img src="/img/bVc5qAu" alt="a2676d10b055f86dde420527b90c588.jpg" title="a2676d10b055f86dde420527b90c588.jpg"></p><p>雷总在自己的年度演讲中说:“你所经历的所有挫折、失败、甚至那些看似毫无意义消磨时间的事情,都将成为你最宝贵的财富”。</p><p>一代又一代人,生命就像是往复的陀螺,兜兜转转。这些平凡的日子中所表达出的喜悦、愤怒,都是生命中不可或缺的时光,而这些时光铸就了我。</p><p>去年的我说:人生最大的幸运就是努力没有白费。今年我会说,人生最大的幸运就是身边的人还在。</p><p>希望 2022 年所有的时刻都能常记在心里,并成为我 2023 年行路的动力。</p><p>希望阅读此篇文章的你收获世界的美好,在你所热爱的世界里闪闪发光!</p><blockquote>本文参与了 <a href="https://segmentfault.com/a/1190000042923114">SegmentFault 思否年度征文「一名技术人的 2022」</a>,欢迎正在阅读的你也加入。</blockquote>
Go 语言如何连接并操作 MySQL 数据库
https://segmentfault.com/a/1190000042811879
2022-11-15T10:43:45+08:00
2022-11-15T10:43:45+08:00
宇宙之一粟
https://segmentfault.com/u/yuzhoustayhungry
0
<h2>1 引言</h2><p>MySQL 是一个基于结构化查询语言(SQL)的开源关系数据库管理系统。它是一种关系数据库,可将数据组织到一个或多个表中,其中数据相互关联。MySQL 是行业领先的开源数据库管理系统。它是一个多用户、多线程的数据库管理系统。</p><p><strong>数据库驱动程序:</strong>数据库驱动程序实现了用于数据库连接的协议。驱动程序就像一个适配器,连接到特定数据库的通用接口。</p><p>Go 有 <code>sql</code> 包,它提供了一个围绕 SQL(或类似 SQL)数据库的通用接口。 <code>sql</code> 包必须与数据库驱动程序一起使用。该软件包提供自动连接池。每次查询数据库时,我们都在使用应用程序启动时设置的连接池中的连接。连接被重用。</p><h2>2 如何在 Go 语言中使用 MySQL</h2><ol><li>启动 MySQL 服务器并使用以下命令安装 <a href="https://link.segmentfault.com/?enc=XoViENldWrCTPW3sL8cHzQ%3D%3D.U39YIU%2F0OJ21unFniYOMs8%2BlxO%2FzzI9Pip3z8mxMkRh%2BdlkcBnDrjYOxriNWzPCY" rel="nofollow">go-mysql</a> 驱动程序。</li></ol><p><img src="/img/bVc3Nke" alt="image.png" title="image.png"></p><pre><code class="go">go get -u github.com/go-sql-driver/mysql</code></pre><ol start="2"><li>创建数据库对象:</li></ol><p>使用 <code>sql.Open</code> 创建一个数据库对象。相反,没有与 MySQL 建立连接,它只创建一个可以稍后使用的数据库对象。</p><pre><code class="go">db, err := sql.Open("mysql", "<user>:<password>@tcp(127.0.0.1:3306)/<database-name>")</code></pre><p>使用 <code>sql.Open</code>,我们打开一个由其数据库驱动程序名称和驱动程序特定数据源名称指定的数据库,通常至少由数据库名称和连接信息组成。它不与数据库建立任何连接,也不验证驱动程序连接参数。相反,它只是为以后使用准备数据库抽象。当第一次需要时,将延迟建立与底层数据存储的第一个实际连接。</p><ol start="3"><li>导入 MySQL 驱动</li></ol><pre><code class="go">_ "github.com/go-sql-driver/mysql"</code></pre><p>当导入带有空白标识符前缀 <code>_</code> 的包时,将调用包的 <code>init</code> 函数。该函数注册驱动程序。</p><ol start="4"><li>defer.Close()</li></ol><p>Close 将连接返回到连接池。如果 sql.DB 的生命周期不应超出函数范围,则推迟 db.Close 是常用的。</p><h3>2.1 连接数据库并查询 MySQL 版本号</h3><p>我们来编写一个程序返回 MySQL 的版本。版本由执行 SELECT VERSION() 语句确定,在 MySQL 的终端中运行本地版本号:</p><pre><code class="sql">mysql> SELECT VERSION();
+-----------+
| VERSION() |
+-----------+
| 5.7.25 |
+-----------+
1 row in set (0.00 sec)</code></pre><p>然后我们按照上述的步骤,编写一个 Go 语言的代码查看数据库的版本号:</p><pre><code class="go">package main
import (
"database/sql"
"fmt"
_ "github.com/go-sql-driver/mysql"
"log"
)
func main() {
db, err := sql.Open("mysql", "root:123456@tcp(127.0.0.1:3306)/DB_TEST")
db.Ping()
defer db.Close()
if err != nil {
fmt.Println("数据库连接失败!")
log.Fatalln(err)
}
var version string
err2 := db.QueryRow("SELECT VERSION()").Scan(&version)
if err2 != nil {
log.Fatal(err2)
}
fmt.Println(version)
}</code></pre><p>建好 <code>go.mod</code> 文件:</p><pre><code class="go">$ go mod init main.go
go: creating new go.mod: module main.go
go: to add module requirements and sums:
go mod tidy</code></pre><p>运行该代码:</p><pre><code class="go">$ go run main.go
5.7.25</code></pre><p>与我们在 MySQL 终端中得到的结果一致,说明数据库连接成功,<code>SELECT VERSION()</code>, 也是没有问题的。</p><h2>3 如何在 Go 语言中操作 MySQL</h2><p>在上面的内容中,我们成功连接了 MySQL 数据库,并成功打印出 MySQL 的版本号,接下来就来介绍如何使用 Go 语言操作数据库。</p><h3>3.1 创建数据库表</h3><p>我们在 <code>DB_TEST</code> 数据库中新建一个 <code>user</code> 数据库,包含主键 id 和名字 name,可以使用如下语句:</p><pre><code class="go">_, err2 := db.Exec("CREATE TABLE user(id INT NOT NULL , name VARCHAR(20), PRIMARY KEY(ID));")</code></pre><p>我们可以先在 MySQL 终端中查看我们的数据库表。</p><pre><code class="sql">mysql> use DB_TEST;
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A
Database changed
mysql> show tables;
+-------------------+
| Tables_in_db_test |
+-------------------+
| table_name |
+-------------------+
1 row in set (0.00 sec)</code></pre><p>编写的 Go 语言的代码如下:</p><pre><code class="go">package main
import (
"database/sql"
"fmt"
_ "github.com/go-sql-driver/mysql"
"log"
)
func main() {
db, err := sql.Open("mysql", "root:123456@tcp(127.0.0.1:3306)/DB_TEST")
db.Ping()
defer db.Close()
if err != nil {
fmt.Println("数据库连接失败!")
log.Fatalln(err)
}
_, err2 := db.Exec("CREATE TABLE user(id INT NOT NULL , name VARCHAR(20), PRIMARY KEY(ID));")
if err2 != nil {
log.Fatal(err2)
}
fmt.Print("Successfully Created\n")
}</code></pre><p>运行该程序:</p><pre><code>Successfully Created</code></pre><p>再来查看我们的数据库表 <code>show tables;</code>,会看到多了一个 user 表,说明创建数据库表成功:</p><pre><code class="sql">mysql> show tables;
+-------------------+
| Tables_in_db_test |
+-------------------+
| table_name |
| user |
+-------------------+
2 rows in set (0.01 sec)</code></pre><h3>3.2 插入数据</h3><pre><code class="go">package main
import (
"database/sql"
"fmt"
_ "github.com/go-sql-driver/mysql"
"log"
)
func main() {
db, err := sql.Open("mysql", "root:123456@tcp(127.0.0.1:3306)/DB_TEST")
db.Ping()
defer db.Close()
if err != nil {
fmt.Println("数据库连接失败!")
log.Fatalln(err)
}
_, err2 := db.Query("INSERT INTO user VALUES(1, 'Wade')")
if err2 != nil {
log.Fatal(err2)
}
fmt.Print("Successfully Inserted\n")
}</code></pre><p>回到数据库终端,查看一下 user 表 <code>select * from user;</code>,可以看到我们刚刚插入成功的一条数据:</p><pre><code class="sql">mysql> select * from user;
+----+------+
| id | name |
+----+------+
| 1 | Wade |
+----+------+
1 row in set (0.00 sec)</code></pre><h3>3.3 查询所有数据</h3><p>我们在数据库中再插入一条数据,使用如下命令 <code>INSERT INTO user VALUES(2, 'Kyrie');</code>:</p><pre><code class="sql">mysql> INSERT INTO user VALUES(2, 'Kyrie');
Query OK, 1 row affected (0.00 sec)</code></pre><p>然后使用 <code>db.Query("SELECT * FROM user")</code> 查询 user 数据库表中的所有数据:</p><pre><code class="go">package main
import (
"database/sql"
"fmt"
_ "github.com/go-sql-driver/mysql"
"log"
)
func main() {
db, err := sql.Open("mysql", "root:123456@tcp(127.0.0.1:3306)/DB_TEST")
db.Ping()
defer db.Close()
if err != nil {
fmt.Println("数据库连接失败!")
log.Fatalln(err)
}
result, err2 := db.Query("SELECT * FROM user")
if err2 != nil {
log.Fatal(err2)
}
for result.Next() {
var id int
var name string
err = result.Scan(&id, &name)
if err != nil {
panic(err)
}
fmt.Printf("Id: %d, Name: %s\n", id, name)
}
}</code></pre><p>执行结果:</p><pre><code>Id: 1, Name: Wade
Id: 2, Name: Kyrie</code></pre><h3>3.4 条件查询</h3><p>可以使用 <code>db.Query("SELECT * FROM user WHERE id = ?", mid)</code> 进行条件查询:</p><pre><code class="go">result, err2 := db.Query("SELECT * FROM user WHERE id = ?", mid)</code></pre><p>Go 代码如下:</p><pre><code class="go">package main
import (
"database/sql"
"fmt"
_ "github.com/go-sql-driver/mysql"
"log"
)
func main() {
db, err := sql.Open("mysql", "root:123456@tcp(127.0.0.1:3306)/DB_TEST")
db.Ping()
defer db.Close()
if err != nil {
fmt.Println("数据库连接失败!")
log.Fatalln(err)
}
var mid int = 1
result, err2 := db.Query("SELECT * FROM user WHERE id = ?", mid)
if err2 != nil {
log.Fatal(err2)
}
for result.Next() {
var id int
var name string
err = result.Scan(&id, &name)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Id: %d, Name: %s\n", id, name)
}
}</code></pre><p><code>?</code> 是一个占位符,填充了 <code>mid</code> 变量中的值。在后台,<code>db.Query</code> 实际上准备、执行和关闭准备好的语句。</p><p>执行该代码:</p><pre><code>Id: 1, Name: Wade</code></pre><p>在 MySQL 终端中运行:</p><pre><code class="sql">mysql> SELECT * FROM user WHERE id = 1;
+----+------+
| id | name |
+----+------+
| 1 | Wade |
+----+------+
1 row in set (0.00 sec)</code></pre><h3>3.5 删除数据</h3><p><code>RowsAffected</code> 返回受更新、插入或删除语句影响的行数。</p><p>在代码示例中,我们使用 <code>DELETE SQL</code> 语句删除一条数据。然后我们用 <code>RowsAffected</code> 打印删除的行数。</p><pre><code class="go">package main
import (
"database/sql"
"fmt"
_ "github.com/go-sql-driver/mysql"
"log"
)
func main() {
db, err := sql.Open("mysql", "root:123456@tcp(127.0.0.1:3306)/DB_TEST")
db.Ping()
defer db.Close()
if err != nil {
fmt.Println("数据库连接失败!")
log.Fatalln(err)
}
sql := "DELETE FROM user WHERE id = 1"
res, err2 := db.Exec(sql)
if err2 != nil {
panic(err2.Error())
}
affectedRows, err := res.RowsAffected()
if err != nil {
log.Fatal(err)
}
fmt.Printf("The statement affected %d rows\n", affectedRows)
}</code></pre><pre><code>The statement affected 1 rows</code></pre><p>此时,再回到 MySQL 终端查看:</p><pre><code class="sql">mysql> SELECT * FROM user;
+----+-------+
| id | name |
+----+-------+
| 2 | Kyrie |
+----+-------+
1 row in set (0.00 sec)</code></pre><h3>3.6 修改数据</h3><pre><code class="go">package main
import (
"database/sql"
"fmt"
_ "github.com/go-sql-driver/mysql"
"log"
)
func main() {
db, err := sql.Open("mysql", "root:123456@tcp(127.0.0.1:3306)/DB_TEST")
db.Ping()
defer db.Close()
if err != nil {
fmt.Println("数据库连接失败!")
log.Fatalln(err)
}
sql := "update user set name = ? WHERE id = ?"
res, err2 := db.Exec(sql, "Yuzhou1su", 2)
if err2 != nil {
panic(err2.Error())
}
affectedRows, err := res.RowsAffected()
if err != nil {
log.Fatal(err)
}
fmt.Printf("Update Success, The statement affected %d rows\n", affectedRows)
}</code></pre><p>运行结果:</p><pre><code>Update Success, The statement affected 1 rows</code></pre><p>可以看到 <code>id = 2</code> 的数据 <code>name</code> 修改为 <code>Yuzhou1su</code> ,数据库中验证:</p><pre><code class="sql">mysql> SELECT * FROM user;
+----+-----------+
| id | name |
+----+-----------+
| 2 | Yuzhou1su |
+----+-----------+
1 row in set (0.01 sec)</code></pre><h2>4 总结</h2><p>本文展示了如何在 Go 语言中使用 MySQL,并给出了详细关键步骤的说明。并在查询 MySQL 版本号的程序中验证了 Go 语言连接数据库成功。</p><p>然后介绍了 Go 语言如何进行增删改查,可以看到增删改查的代码并不复杂,最主要就是掌握 MySQL 的 SQL 语句的写法,然后代码框架差不多,也可以把这些函数封装成一个工具,方便今后自己的使用。</p><p>另外 Go 语言还有很多优秀的 go MySQL 驱动和框架,大家可以自行探索与学习。地址如下: <a href="https://link.segmentfault.com/?enc=Qhtvj1LxDOW0ntTkyXyZgw%3D%3D.TFgyUpLRNToTFHVpoU2XtG%2BPJUOiSKYVlS8VR7RjmStoT15OKwrVjcdjl2tCr5Vv" rel="nofollow">Database Drivers - Awesome Go</a></p><p><img src="/img/bVc3Nt9" alt="image.png" title="image.png"></p><p>本篇文章就到此结束了,下一篇文章再见~</p><p>希望本文能对你有所帮助,如果喜欢本文,可以点个赞或关注。</p><blockquote>这里是宇宙之一粟,下一篇文章见!<br>宇宙古今无有穷期,一生不过须臾,当思奋争。</blockquote><p>本文参与了<a href="https://segmentfault.com/a/1190000042741155">思否技术征文</a>,欢迎正在阅读的你也加入。</p>
如何在 Go 代码中运行 C 语言代码
https://segmentfault.com/a/1190000042808076
2022-11-14T17:50:48+08:00
2022-11-14T17:50:48+08:00
宇宙之一粟
https://segmentfault.com/u/yuzhoustayhungry
0
<h2>前言</h2><p>在前面多篇 Go 系列文章中,我们了解到,Go 语言脱胎于 C 语言,这就意味着在某些更底层的细节中,我们可以使用 C 语言实现,然后通过 Go 来调用相关的 C 代码。其实这一特点,在 Java 的 JVM、Python 的解释器也是通过底层是直接调用 C 实现的。</p><p>而本篇文章就来学习一下,如何在 Go 语言中运行 C 程序。</p><h2>直接在 Go 代码中写入 C 程序</h2><p>Go 语言通过 cgo 攻击来识别代码中的 C 语言,我们可以通过命令 <code>go env</code> 来查看是否 cgo 工具是否开启。</p><p><img src="/img/remote/1460000042808079" alt="" title=""></p><p><code>CGO_ENABLED=1</code> 表示 cgo 工具可用,当设置为 0 时,表示工具不可用。</p><p>然后我可以新建一个 <code>CinGo.go</code> 的程序,然后在注释中写入 c 语言的代码。然后导入 Go 提供的 c 包 <code>import "C"</code> ,Go 语言在看到导入这个包之后就知道如何去处理注释中的内容了。</p><p>这里我们在 C 代码中写入要给 <code>callC()</code> 函数,然后在 Go 语言中进行调用:</p><pre><code>package main
// #include <stdio.h>
// void callC() {
// printf("Hello World from C!\n");
// }
import "C"
import "fmt"
func main() {
fmt.Println("让我们学习 Go 语句调用 C 程序")
C.callC()
fmt.Println("调用 C 程序结束")
}</code></pre><p>执行结果:</p><pre><code>$ go run CinGo.go
让我们学习 Go 语句调用 C 程序
Hello World from C!
调用 C 程序结束</code></pre><p>但是,这种方式的 C 代码和 Go 语言代码在同一个文件中,所以大家能明显发现这种方式的代码耦合度太高,仅仅适用于项目简单单一的情形。</p><p>一个更合理的方式应该是 C 代码单独在一个文件。</p><h2>Go 直接调用 C 文件</h2><p>那么,如果已经写好一个封装好的 C 文件代码,Go 语言该如何调用呢?</p><p>此时我们需要直接写好 C 代码,因为 Go 代码是无法对 C 代码文件进行重写或者修改的。</p><p><strong>写好 C 头文件</strong></p><p>我们在本地 Go 项目中,创建一个 <code>hello.h</code> 的头文件,代码如下:</p><pre><code>#ifndef HELLO_H
#define HELLO_H
int sayHello(const char *name, char *out);
void printMessage(char *message);
void cHello();
int add(int a, int b);
#endif</code></pre><p><strong>编写 C 文件</strong></p><p>然后编写 <code>hello.c</code> 文件,如下:</p><pre><code>#include "hello.h"
#include <stdio.h>
int sayHello(const char *name, char *out) {
int n;
n = sprintf(out, "Hello, My name is %s!", name);
return n;
}
void cHello() {
printf("Hello from C!\n");
}
void printMessage(char* message) {
printf("从 Go 语言接收的信息: %s\n", message);
}
int add(int a, int b) {
return a + b;
}</code></pre><p><strong>写好 Go 代码</strong></p><p>最后编写我们的 <code>main.go</code> 语言:</p><ul><li>我们需要在 CFLAGS 参数中填入我们的 GOPath 路径, <code>#cgo CFLAGS: -I /Users/yuzhou_1su/go/src/CinGo</code> 。</li><li>然后在 LSFLAGS 中填入我们的 C 编译后的本地链接文件: <code>#cgo LDFLAGS: /Users/yuzhou_1su/go/src/CinGo/hello.a</code></li></ul><pre><code>package main
// #cgo CFLAGS: -I /Users/yuzhou_1su/go/src/CinGo
// #cgo LDFLAGS: /Users/yuzhou_1su/go/src/CinGo/hello.a
// #include <stdlib.h>
// #include <hello.h>
import "C"
import (
"fmt"
"unsafe"
)
func main() {
C.cHello()
a := C.int(1024)
b := C.int(2022)
result := C.add(a, b)
fmt.Println("Reuslt is:", result)
goMessage := C.CString("This is Go")
defer C.free(unsafe.Pointer(goMessage))
C.printMessage(goMessage)
}</code></pre><p><strong>最后代码结构如下:</strong></p><p><img src="/img/remote/1460000042808080" alt="" title=""></p><p>然后我们首先编译 c 代码:</p><pre><code>$ gcc -c *.c
$ /usr/bin/ar rs hello.a *.o
ar: creating archive hello.a
$ rm hello.o</code></pre><p>然后再执行 Go 代码,结果如下:</p><pre><code>$ go run main.go
Hello from C!
Reuslt is: 3046
从 Go 语言接收的信息: This is Go</code></pre><h2>总结</h2><p>在编写上述的小案例过程你中的,都出现了了很多小问题,比如 C 代码和 <code>import "c"</code> 语句之间不能有空格。经常会出现找不到 C 函数等等问题。</p><p>总得来说,日常 Go 开发还是不需要此类高级用法,也就是说其实我们平常编程过程中不太需要 cgo,大多数情况下还是尽量用 Go 语言自己实现。如果确实需要使用 C 语言,还是得多去了解 <a href="https://link.segmentfault.com/?enc=de%2FhZMpdfvtEyOEQhQ1RGQ%3D%3D.tMOZ7TFgT5BLoptE%2Fwo9XNVeUFU87%2FjA5fX38bj5Juw%3D" rel="nofollow">cgo</a> 的文档,以防出错。</p><p>希望本文能对你有所帮助,如果喜欢本文,可以点个赞或关注。</p><blockquote>这里是宇宙之一粟,下一篇文章见!<br>宇宙古今无有穷期,一生不过须臾,当思奋争。</blockquote><p>灵感来源:</p><blockquote><ul><li><a href="https://link.segmentfault.com/?enc=sAjaz7vZaKsRa2RhGkI6JA%3D%3D.1vqHap9Jv%2FKVw1sX7TGoVzOFeSU2%2F2DZ6zXHpp524ws7aZrHRlx8tDVgjXzkFqGtWMWOyF%2BzRarHYWZRMrAwMw%3D%3D" rel="nofollow">Calling C code from go | Karthik Karanth</a></li><li><a href="https://link.segmentfault.com/?enc=MNehXyimfumz29TSgooSeA%3D%3D.EJx6nxQGt9yen4eFiM5vVGFetyCtoBi%2FnMs2h2BCjyk%3D" rel="nofollow">https://pkg.go.dev/cmd/cgo</a></li><li>《精通 Go 语言》</li></ul></blockquote><p>本文参与了<a href="https://segmentfault.com/a/1190000042741155">思否技术征文</a>,欢迎正在阅读的你也加入。</p>
回声嘹亮 之 Go 的 Echo 框架 —— 上手初体验
https://segmentfault.com/a/1190000042808039
2022-11-14T17:47:13+08:00
2022-11-14T17:47:13+08:00
宇宙之一粟
https://segmentfault.com/u/yuzhoustayhungry
0
<h2>Echo 介绍</h2><p><a href="https://link.segmentfault.com/?enc=9C0IhrQzk29zVsh5Qu4YvQ%3D%3D.7BB%2FCR9v7YBJcFjodqlsPeeOztk%2Fm06lJmyTBNqbnw0%3D" rel="nofollow">Echo</a> 是众多 Go Web 框架的一个,根据官网介绍,它有着高性能、可扩展性、极简的特点。</p><p><img src="/img/remote/1460000042808042" alt="" title=""></p><p>Echo 的特点概述:</p><ul><li>优化的 HTTP 路由,可智能地优先路由</li><li>方便构建强大且可扩展的 RESTful API</li><li>API 组</li><li>可扩展的中间件框架,在多个级别定义中间件(root, group, route)</li><li>为 JSON , XML 进行表单数据负载绑定</li><li>发送各种 HTTP 响应的便捷功能</li><li>集中式 HTTP 错误处理</li><li>使用任何模板引擎进行模板渲染</li><li>定义日志的格式</li><li>高度个性化、可定制</li><li>通过 Let’s Encrypt 实现自动 TLS</li><li>HTTP/2 支持</li></ul><h2>Echo 上手</h2><p>从 4.0.0 版本开始,Echo 可作为 Go 模块使用。因此,需要一个能够理解 /vN 后缀导入的 Go 版本:</p><ul><li>1.9.7+</li><li>1.10.3+</li><li>1.14+</li></ul><p>这些 Go 版本中的任何一个都允许您将 Echo 导入为 <code>github.com/labstack/echo/v4</code>,这是今后使用 Echo 的推荐方式。</p><h3>Echo 安装</h3><p>这里以 Linux 为例,我们先看一下自己本地的 Go 版本:</p><pre><code>$ go version
go version go1.16.2 linux/amd64</code></pre><p>本文使用的是 Go 1.16 版本,所以可以直接上手使用。</p><ol><li>新建一个项目文件夹 <code>HeadFirstEcho</code>,然后进入该文件目录下:</li></ol><pre><code>$ mkdir HeadFirstEcho && cd HeadFirstEcho</code></pre><ol start="2"><li>然后利用 <code>go mod</code> 初始化该项目:</li></ol><pre><code>$ go mod init HeadFirstEcho
go: creating new go.mod: module HeadFirstEcho</code></pre><ol start="3"><li>下载 Echo,命令:</li></ol><pre><code class="go">// go get github.com/labstack/echo/{version}
go get github.com/labstack/echo/v4</code></pre><p>本次下载成功如下:</p><p><img src="/img/remote/1460000042808043" alt="" title=""></p><p>下载成功后,我们来看一下如果利用 Echo 来写一个 <strong>HelloWorld</strong> 项目。</p><h3>Hello World</h3><p>在 Hello World 程序中,我们只需要让用户通过访问服务器地址,得到一个 Hello World 的文本就算成功,所以只需要编写一个服务器。创建一个 <code>helloServer.go</code> 文件:</p><pre><code class="go">package main
import (
"net/http"
"github.com/labstack/echo/v4"
)
func main() {
e := echo.New() // 得到一个 echo.Echo 的实例
// 注册路由
e.GET("/", func(c echo.Context) error {
return c.String(http.StatusOK, "Hello, World!")
})
// 开启 HTTP Server
e.Logger.Fatal(e.Start(":2022"))
}</code></pre><p>启动我们的服务器: <code>$ go run helloServer.go</code> , 接着我们能看到如下的界面,说明 helloServer 服务器启动成功!</p><p><img src="/img/remote/1460000042808044" alt="" title=""></p><p>然后我们打开浏览器,访问:<code>http://localhost:2022/</code>,就能成功的看到我们的 Hello,World! 输出到屏幕上。</p><p><img src="/img/remote/1460000042808045" alt="" title=""></p><p>对比,之前用 Go 系统自带的 http 库实现的 HelloWorld 应用还是有异曲同工之妙的,该篇文章点 <a href="https://link.segmentfault.com/?enc=r3QbEnLH7Dz0rvV1KsVwQw%3D%3D.jh9ZFAGigFbtBWM4zNZ0ZOzqJwQCihlxWmsi6Lp9W7F7ZgtuohNkvnFlwTCY3eWa" rel="nofollow">此处</a></p><h3>带日志功能的 HelloWorld 程序</h3><p>Echo 还带有非常丰富的日志中间件:日志会记录每个有关 HTTP 请求的信息,只需要使用 <code>e.Use(middleware.Logger())</code> 方法就能调用日志记录器,非常方便。当然日志也支持自定义,具体之后的文章再进行研究。</p><p>恢复中间件从链中任何地方的 panic 报错中恢复,并能够打印堆栈跟踪并将控制权处理给集中式 HTTPErrorHandler。用法也很简单:<code>e.Use(middleware.Recover())</code>。</p><p>来看如下的例子:</p><pre><code class="go">package main
import (
"net/http"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
)
// Handler
func hello(c echo.Context) error {
return c.String(http.StatusOK, "你好,世界!")
}
func main() {
// Echo instance
e := echo.New()
// Middleware
e.Use(middleware.Logger())
e.Use(middleware.Recover())
// Routes
e.GET("/", hello)
// Start server
e.Logger.Fatal(e.Start(":1323"))
}</code></pre><p>除了能在前端显示出:“<strong>你好,世界</strong>!” 的界面,还能在后台看到日志打印在控制台上:</p><pre><code class="shell">$ go run hello.go
____ __
/ __/___/ / ___
/ _// __/ _ \/ _ \
/___/\__/_//_/\___/ v4.6.3
High performance, minimalist Go web framework
https://echo.labstack.com
____________________________________O/_______
O\
⇨ http server started on [::]:1323
{"time":"2022-02-25T15:08:10.481569868+08:00","id":"","remote_ip":"127.0.0.1","host":"localhost:1323","method":"GET","uri":"/","user_agent":"Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:97.0) Gecko/20100101 Firefox/97.0","status":200,"error":"","latency":9308,"latency_human":"9.308µs","bytes_in":0,"bytes_out":16}</code></pre><p>能看到的日志信息有:</p><ul><li>time :时间,2022-02-25T15:08:10.481569868+08:00</li><li>id:此处为空</li><li>remote_ip:127.0.0.1</li><li>host:localhost:1323</li><li>method:GET 方法</li><li>uri:/ 根目录</li><li>user_agent 用户头:此处为火狐 Ubuntu 版,Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:97.0) Gecko/20100101 Firefox/97.0</li><li>status:状态码,200</li><li>error:空</li><li>latency: 9308</li><li>latency_human: 9.308µs</li><li>bytes_in: 0</li><li>bytes_out: 16</li></ul><h2>其他中间件</h2><p>从上面两个中间件我们可以看出,Echo 的确不愧为官网自称的极简 Web 框架,所有的函数调用都非常简单,而且支持可定制化。</p><p>在官方文档中,我们能看到所有的基本 Web 组件都已经帮我们实现好,比如:</p><ul><li>Binding:绑定请求数据</li><li>Context:上下文,表示当前 HTTP 请求的上下文。</li><li>Cookies</li><li>Error Handling:错误处理</li><li>Request:请求</li><li>Response:响应</li><li>Static Files:静态文件</li><li>Templates:模板</li><li>Testing:测试</li></ul><p>还有更多的中间件,包括 认证、CORS、CSRF、JWT、Logger、Secure、Session 等等,感兴趣的朋友赶紧上手试一试吧。</p><h2>丰富而简单的案例</h2><p>如果有一点 Web 开发的知识,对上述内容一定不陌生。</p><p>除了针对每一个组件的介绍,官方文档也提供了很多案例来帮助我们更好的学习和上手 <strong>Echo</strong> 这个框架,比如 <a href="https://link.segmentfault.com/?enc=Ei10p%2FLiLeTQDm%2FWRGyJ8w%3D%3D.3qIx8gsI25dJSWldfBHCbzehL1xhF4N4kuEyPKNz8M6%2B1pLtPqtdh04WOgV60J6DtX1RSKyz0hdTFPEeSC8boQ%3D%3D" rel="nofollow">文件下载</a> 、 <a href="https://link.segmentfault.com/?enc=zL%2B%2BVBHjjlBuolhsHpyzTg%3D%3D.y7gC9A8do2ozvFIaEif0OSKhdpv2oU4G4UfQDAtnJet5WL%2F8iPUyjLdotkB4HQSG" rel="nofollow">上传</a> 、还有一个 <a href="https://link.segmentfault.com/?enc=P3HYoj1oabGNhZhm%2FeJJqA%3D%3D.uYU7908m1PyRIWZ0wehh4Q7bux2%2FIg9pdO41EPix5cmsZwmIONysC3aAoEz4Lpyr" rel="nofollow">WebSocket</a> 的案例实现服务器和客户端的聊天。</p><p>跟着文档学习的过程你中,发现每个程序案例都很简单,几乎都是在本地运行代码,就能看到同样的效果,对新人真的很友好!</p><p>如果大家感兴趣的话,就自己去探索吧,刚好笔者也想做一个简单的 Web 项目,刚好可以利用上 Echo ,一边开发一边学习。</p><h2>总结</h2><p>Go 语言的 Web 开发框架其实也有很多: <a href="https://link.segmentfault.com/?enc=6TYBzG0HvUFTVTaUpav1lw%3D%3D.PGZQC0PRrmQzLG5drV8yOFvCDylEhD0hQurz8sgJs4sRc4bDVu71CiXYYPc%2FkLG%2B" rel="nofollow">Gin</a> 、 <a href="https://link.segmentfault.com/?enc=EllyLxnDpgoWNqYUXROSlg%3D%3D.5Vs5kvKdG8aO1DayBOhnaYj7O7HSpjOZpoLtiy1a7%2BW19XnEhO9oU5uqR5tZMGlN" rel="nofollow">Beego</a> 、 <a href="https://link.segmentfault.com/?enc=5nX%2BgBXBh2wMRzkCu7KGdg%3D%3D.%2FacsQouOclGSAS4xSAuqx74FkO6UBqDGV9q9TL5POvc%3D" rel="nofollow">Revel</a> 。</p><p>Echo 作为其中的一款。截止到 2022 年,目前已经在 Github 上收获了 21.7k 的 Star 和 1.9k 的 Fork,可以说是经得起时间检验,得到了大家的认可,正可谓对应上标题中的<strong>回声嘹亮</strong>。</p><p>在著名的 <a href="https://link.segmentfault.com/?enc=PG2gb22ziyopvR3szHch5Q%3D%3D.tzGwp0o3hVgAPoaU1Y9KfXP8LCyHA9nOWeYoeMlwtGWH285X4YLPeXW33zaLcTo%2BgAct3IaASu7Ql3mwgsySvPoLtEXmvTRoeKdHq%2FO7QFJVkohII6tCaqqTo1K4gPfMs57PT5DxPSaGmKyQjU%2BrOw%3D%3D" rel="nofollow">Go 开发者成长路线图</a> 中也推荐了 Go 语言开发者应该要学习 Echo 框架。</p><p><img src="/img/remote/1460000042808046" alt="" title=""></p><p>最后,希望更多人能够和我一样加入对这个框架的学习。之后的文章中,会介绍 Echo 的其他组件在 Web 开发中的运用,下一篇 Echo 文章见!</p><p>参考资料:</p><ol><li><a href="https://link.segmentfault.com/?enc=M4uS2nvx0bYe2UXUZCztLA%3D%3D.pBJ1Q%2FGLvRa%2BpPMJKyWjQZySOM7tYL5etm1WYdocePAmXk1MM8WboPE3p2m4Hmd2" rel="nofollow">Echo 官网</a></li><li><a href="https://link.segmentfault.com/?enc=KMVVBsF2FYV%2FgRv0%2B5ZfAg%3D%3D.l12MKLr3f6rGJ4VIykKDK%2Fu8tdzzEDekaChr%2FtjJsBk14KsBjYZMuh9S0oACo4Cf" rel="nofollow">Echo Github 源码</a></li><li><a href="https://link.segmentfault.com/?enc=ApYe84eAaKdEqIJHG6eOJA%3D%3D.KyD8BI2Ch08FLETwdaMmaxDUAPgI2BcFL9FJQdxqFqrx5O2UWnbkPlEg2Dv6h%2BEv" rel="nofollow">Echo 官方文档</a></li><li><a href="https://link.segmentfault.com/?enc=HdjcAFJRequ7WyVF20nIUg%3D%3D.oqd%2BeP7CvUzNTacXkUa%2FN8hjudjnsfzMwkut6SZPqph0WZd42wZmUt5XNMFJ9zc8EXAY6QXFMYwhbpE%2FetgN9Q%3D%3D" rel="nofollow">Go 语言 Web 框架 Echo 系列教程 | polarisxu (studygolang.com)</a></li><li><a href="https://link.segmentfault.com/?enc=snKsdzBK%2FJHKf%2FSkLO01og%3D%3D.aIeV6zWmdvl8FaF1ZyPQuzIRLov0Zj2XMracwRLL1ALEdAfFmpzVJH4KaUthUo0inTMmRc4K%2FlfrfxwTh06QZA%3D%3D" rel="nofollow">Go echo package</a></li></ol><p>希望本文能对你有所帮助,如果喜欢本文,可以点个赞或关注。</p><blockquote><p>这里是宇宙之一粟,下一篇文章见!</p><p>宇宙古今无有穷期,一生不过须臾,当思奋争。</p></blockquote><p>本文参与了<a href="https://segmentfault.com/a/1190000042741155">思否技术征文</a>,欢迎正在阅读的你也加入。</p>
Go 微服务实战之如何实现加解密操作的微服务开发
https://segmentfault.com/a/1190000042773134
2022-11-08T21:15:31+08:00
2022-11-08T21:15:31+08:00
宇宙之一粟
https://segmentfault.com/u/yuzhoustayhungry
0
<h2>1 前言</h2><p>在上一篇文章——《<a href="https://segmentfault.com/a/1190000042756262">Go 微服务实战之如何使用 go-micro 写微服务应用</a>》中,我们介绍了微服务的相关概念和 go-micro 框架的特点。</p><p>接下来,我们将以循序渐进的方式建立一个简易的提供加解密服务的 Go 微服务项目。首先为了创建微服务,需要前期设计几个实体:</p><ul><li>定义服务的 RPC 方法的 protocol buffer 文件</li><li>具体方法实现的 handler 文件</li><li>一个公开 RPC 方法的服务器 server</li><li>一个可以发出 RPC 请求并获得响应结果的客户端 client</li></ul><p><img src="/img/remote/1460000042773137" alt="img" title="img"></p><h2>2 创建 encryption.proto 文件</h2><p>首先,为了将 protocol buffer 文件编译为 Go 包,需要先安装 <code>protoc</code>,下载点<a href="https://link.segmentfault.com/?enc=EOnJZ58OiG7qVysK3v%2BMvQ%3D%3D.MXkmfKjNJHWtCKHas6z192OZDiJWCZWXl4AunydheXs9eeGPgPl1wpfb8TwqhoMiA4mhGIyAju9hU5rFDze1og%3D%3D" rel="nofollow">此处</a>,选择你对应的系统。</p><p>本文是以 Win 进行的示例开发,下载的是 <code>protoc-21.9-win32.zip</code>,解压完后添加到系统环境变量,如图所示:</p><p><img src="/img/remote/1460000042773138" alt="img" title="img"></p><p>然后安装 <code>proto-gen-micro</code>,使用如下命令:</p><pre><code>go install github.com/go-micro/generator/cmd/protoc-gen-micro@latest</code></pre><p><img src="/img/remote/1460000042773139" alt="img" title="img"></p><p>接下来,创建我们的项目目录 <code>encryptService</code> 文件夹,然后在其中创建一个 <code>proto</code> 目录,新建一个 <code>encryption.proto</code> 文件,写入如下内容:</p><pre><code class="protobuf">syntax = "proto3";
package main;
option go_package="./proto";
service Encrypter {
rpc Encrypt(Request) returns (Response) {}
rpc Decrypt(Request) returns (Response) {}
}
message Request {
string message = 1;
string key = 2;
}
message Response {
string result = 2;
}</code></pre><p>上面的文件命名了一个 <code>Encrypter</code> 的服务,有着 <code>Request</code> 和 <code>Response</code> 两条消息。这两条信息是用来请求加密和解密的。</p><ul><li>首行前置的文件语法是 <code>proto3</code></li><li>请求消息 <code>Request</code> 有两个字段,分别为: <code>message</code> (需要加密的信息)和 <code>key</code>(密钥)。客户端使用这些字段来发送一个 <code>plaintext/ciphertext</code> 消息</li><li>响应消息 <code>Response</code>只有一个字段 <code>result</code>:它是加密/解密过程的结果。加密 <code>Encypter</code> 服务有两个 RPC 方法:<code>Encrypt</code> 和 <code>Decypt</code>,两者都是接收一个请求,然后返回一个响应。</li></ul><p><img src="/img/remote/1460000042773140" alt="img" title="img"></p><p>接着我们可以通过编译 <code>.proto</code> 文件来生成 Go 文件,执行如下命令:</p><pre><code class="shell">protoc --proto_path=. --micro_out=. --go_out=. proto/encryption.proto</code></pre><p>执行成功后会在我们的项目 <code>encryptService/proto</code> 目录下自动生成两个文件:</p><ul><li><strong>encryption.pb.go</strong></li><li><strong>encryption.pb.micro.go</strong></li></ul><p>文件成功生成后如图:</p><p><img src="/img/remote/1460000042773141" alt="img" title="img"></p><p>这些自动生成的文件不需要我们手动进行修改。</p><h2>3 编写 <code>encryptService</code> 微服务端</h2><h3>3.1 新建 <code>utils.go</code> 文件</h3><p>接下来,我们新建一个 <code>utils.go</code> 文件,定义字符串 AES 加解密的方法,如下:</p><pre><code class="go">package main
import (
"crypto/aes"
"crypto/cipher"
"encoding/base64"
)
var initVector = []byte{35, 46, 57, 24, 85, 35, 24, 74, 87, 35, 88, 98, 66, 32, 14, 05}
// 字符串加密函数
func EncryptString(key, text string) string {
block, err := aes.NewCipher([]byte(key))
if err != nil {
panic(err)
}
plaintext := []byte(text)
cfb := cipher.NewCFBEncrypter(block, initVector)
cipertext := make([]byte, len(plaintext))
cfb.XORKeyStream(cipertext, plaintext)
return base64.StdEncoding.EncodeToString(cipertext)
}
// 解密函数
func DecryptString(key, text string) string {
block, err := aes.NewCipher([]byte(key))
if err != nil {
panic(err)
}
cipertext, _ := base64.StdEncoding.DecodeString(text)
cfb := cipher.NewCFBEncrypter(block, initVector)
plaintext := make([]byte, len(cipertext))
cfb.XORKeyStream(plaintext, cipertext)
return string(plaintext)
}</code></pre><h3>3.2 新建 <code>handler.go</code> 文件</h3><p>接着新建一个 <code>handler.go</code> 文件,在这个文件内为我们的服务定义业务逻辑:</p><ol><li>首先定义一个 <code>Encrypt</code> 结构体</li><li>增加两个方法 <code>Encrypt</code> 和 <code>Decrypt</code> 处理 RPC 请求</li></ol><pre><code class="go">package main
import (
"context"
"encryptService/proto"
)
type Encrypter struct{}
// 将消息加密后发送请求
func (g *Encrypter) Encrypt(ctx context.Context, req *proto.Request, rsp *proto.Response) error {
rsp.Result = EncryptString(req.Key, req.Message)
return nil
}
// 将密文解密后返回相应
func (g *Encrypter) Decrypt(ctx context.Context, req *proto.Request, rsp *proto.Response) error {
rsp.Result = DecryptString(req.Key, req.Message)
return nil
}</code></pre><p>如上的代码,在 <code>Encrypter</code> 结构体中的两个方法 <code>Encrypt</code> 和 <code>Decrypt</code> ,</p><pre><code class="go">func (g *Encrypter) Encrypt(ctx context.Context, req *proto.Request, rsp *proto.Response)
func (g *Encrypter) Decrypt(ctx context.Context, req *proto.Request, rsp *proto.Response)</code></pre><p>两个方法都是接收一个 <code>context</code> 对象、一个 RPC 请求对象、和一个 RPC 响应对象。每个方法所做的工作是调用各自的实用函数,并将响应对象返回为一个结果 <code>rsp.Result</code>。</p><p>值得一提的是,<code>Encrypt</code> 加密和 <code>Decrypt</code> 解密会被映射到 protocol buffer 文件中的 RPC 方法中,如下方法:</p><pre><code class="protobuf">rpc Encrypt(Request) returns (Response) {}
rpc Decrypt(Request) returns (Response) {}</code></pre><h3>3.3 新建 <code>main.go</code> 文件</h3><p>紧接着,我们在 <code>encryptService</code> 根目录下,新建一个 <code>main.go</code> 文件,根据上一篇文章中对 go-micro 框架中创建微服务实例的方法,我们写入如下内容:</p><pre><code class="go">package main
import (
"encryptService/proto"
"fmt"
"go-micro.dev/v4"
)
func main() {
// 创建一个新服务
service := micro.NewService(
micro.Name("encrypter"),
)
// 初始化
service.Init()
proto.RegisterEncrypterHandler(service.Server(),
new(Encrypter))
// 启动服务
if err := service.Run(); err != nil {
fmt.Println(err)
}
}</code></pre><ul><li><code>micro.NewService</code> 用于新建一个微服务,然后一个 <code>service</code> 对象</li><li>运行 <code>service.Init()</code> 收集命令行参数</li><li>通过 <code>proto.RegisterEncrypterHandler</code> 方法注册服务,这个方法是由 protocol buffer 编译器动态生成的。</li><li>最后,<code>service.Run</code> 启动服务</li></ul><h3>3.4 运行 <code>encryptService</code> 服务</h3><p>我们来看一下如何正常启动整个微服务实例:</p><ol><li>执行 <code>go mod init encrypService</code></li></ol><p>在执行这一步出现问题,比如遇到如下错误:</p><pre><code>go: encryptClient/proto imports
go-micro.dev/v4/api: go-micro.dev/v4/api@v1.18.0: parsing go.mod:
module declares its path as: github.com/micro/go-micro
but was required as: go-micro.dev/v4/api</code></pre><p>使用如下命令进行解决,得到 v4 版:</p><pre><code>go get go-micro.dev/v4</code></pre><p>成功截图如下:</p><p><img src="/img/remote/1460000042773142" alt="img" title="img"></p><p>再来执行 <code>go mod tidy</code> ,执行成功如下图,最后会自动生成 <code>go.mod</code> 和 <code>go.sum</code> 文件。:</p><p><img src="/img/remote/1460000042773143" alt="img" title="img"></p><p>使用 <code>go build .</code> 编译整个项目,编译成功后在 Win 下会生成一个 <code>.exe</code> 的可执行文件。</p><p>编译完整个项目后的目录结构如下:</p><p><img src="/img/remote/1460000042773144" alt="img" title="img"></p><p>最后,运行我们的 encrypService 服务,通过使用 <code>./encryptService.exe</code> 命令进行启动,成功如下:</p><p><img src="/img/remote/1460000042773145" alt="img" title="img"></p><p>正如你从服务器终端看到的那样,go-micro 利用一个 <code>info Transport</code> 和一个消息代理 <code>info Broker</code> 成功启动了一个微服务。此时,我们可以通过在浏览器中访问 <code>http://127.0.0.1:58184/</code> 查看相关信息:</p><p><img src="/img/remote/1460000042773146" alt="img" title="img"></p><p>现在,客户端可以向这些端口发送请求,但目前这些服务并不那么有用,因为我们还没有编写客户端来消费这些 API,接下来尝试建立一个 <code>encryptClient</code> 客户端,学习如何连接到这个服务器。</p><h2>4 编写 <code>encryptClient</code> 客户端</h2><p>同理,通过 Go Micro 框架构建客户端,通过 RPC 调用上面的服务端,接下来就是按步骤编写客户端的方法。新建一个 <code>encryptClient</code> 目录,然后在这个目录下建立一个 <code>proto</code> 文件夹。客户端项目结构图如下:</p><p><img src="/img/remote/1460000042773147" alt="img" title="img"></p><h3>4.1 编写 proto 文件</h3><p>首先,我们需要知道服务器和客户端应该同意使用相同的 protocol buffers (协议缓冲区)。同样地,Go Micro 希望服务器和客户端使用相同的 <code>.proto</code> 文件,在上面的的例子是 <code>encryption.proto</code> 文件。</p><p>在 <code>encryptClient/proto</code> 下创建一个和服务端相同的 <code>encryption.proto</code> 文件:</p><pre><code class="go">syntax = "proto3";
package main;
option go_package="./proto";
service Encrypter {
rpc Encrypt(Request) returns (Response) {}
rpc Decrypt(Request) returns (Response) {}
}
message Request {
string message = 1;
string key = 2;
}
message Response {
string result = 2;
}</code></pre><p>类似地,使用 <code>protoc -I=. --micro_out=. --go_out=. proto/encryption.proto</code> 命令执行生成 Go 文件,如图:</p><p><img src="/img/remote/1460000042773148" alt="img" title="img"></p><h3>4.2 编写 <code>main.go</code> 文件</h3><pre><code class="go">package main
import (
"context"
"encryptClient/proto"
"fmt"
"go-micro.dev/v4"
)
func main() {
// 创建新服务
service := micro.NewService(micro.Name("encrypter.client"))
// 初始化客户端,解析命令行参数
service.Init()
// 创建新的加密服务实例
encrypter := proto.NewEncrypterService("encrypter", service.Client())
// 调用 encrypter 加密服务
rsp, err := encrypter.Encrypt(context.TODO(), &proto.Request{
Message: "Hello world",
Key: "111023043350789514532147",
})
if err != nil {
fmt.Println(err)
}
// 打印响应
fmt.Println(rsp.Result)
// 调用解密 decrypter 服务
rsp, err = encrypter.Decrypt(context.TODO(), &proto.Request{
Message: rsp.Result,
Key: "111023043350789514532147",
})
if err != nil {
fmt.Println(err)
}
// 打印解密结果
fmt.Println(rsp.Result)
}</code></pre><ul><li><code>service := micro.NewService(micro.Name("encrypter.client")</code> 新建服务实例</li><li>调用加密服务时,传入 <code>"Hello world"</code> 文本和一个密钥 <code>"111023043350789514532147"</code></li><li><code>fmt.Println(rsp.Result)</code>,最后在终端打印 <code>rsp.Result</code></li><li>调用解密服务时,传入加密的结果 <code>rsp.Result</code> 和同一个密钥 <code>"111023043350789514532147"</code></li><li>然后打印解密结果 <code>fmt.Println(rsp.Result)</code></li></ul><p>编写完成后,执行 <code>go mod init encryptClient</code>,如图:</p><p><img src="/img/remote/1460000042773149" alt="img" title="img"></p><p>接着,使用 <code>go mod tidy</code> ,自动生成 <code>go.sum</code> 文件。</p><p>然后执行编译 <code>go build .</code> ,生成 <code>encryptClient.exe</code> 文件。</p><p>最后执行客户端打印,终端输出 Hello world 的 AES 加密文本 <code>8rqECLu6rQTfkCM=</code> 和解密后的明文 <code>Hello world</code>:</p><pre><code>$ ./encryptClient.exe
8rqECLu6rQTfkCM=
Hello world</code></pre><p>执行过程,如图所示:</p><p><img src="/img/remote/1460000042773150" alt="img" title="img"></p><p>这个过程证明了我们的加解密微服务的 RPC 调用时成功的,不过也能看到通过使用 Go Micro 框架,我们能通过几行代码,就创建了微服务和客户端。</p><h2>5 总结</h2><p>本文通过实现加解密操作展示了一个微服务应用的开发过程。通过编写服务端,成功运行了一个微服务实例,该服务能够通过加密请求得到一个加密后的密文,通过解密请求将消息进行解密,并返回明文结果。然后通过编写客户端向服务端进行 RPC 调用,成功将 Hello world 字符串进行加密并打印出密文和明文的结果。</p><p>这个过程充分展示了 Go Micro 框架的便利性,至于 Go Mirco 框架还有更多的知识等着大家学习。希望本文能起到抛砖引玉的效果,让更多看到文章的人加入学习和微服务的开发当中。</p><p>希望本文能对你有所帮助,如果喜欢本文,可以点个赞或关注。</p><blockquote><p>这里是宇宙之一粟,下一篇文章见!</p><p>宇宙古今无有穷期,一生不过须臾,当思奋争。</p></blockquote><p>参考链接:</p><ul><li><a href="https://link.segmentfault.com/?enc=2UaxoSnBYMIZ8oVlheXptw%3D%3D.YHGLhwDBAt4nEwDBExRM%2BkTrAfDznGl2k%2BJhXpPw6Ddg4QmCNwirmz1JcEx2l%2Fx0m9uzkHjvXLyysUt5m4w5Bw%3D%3D" rel="nofollow">Go Micro入门 - Go语言中文文档</a></li><li><a href="https://link.segmentfault.com/?enc=9dV8c%2FtbEWWAGoz6dDuKzQ%3D%3D.RbD5vW1ywraq9kt%2F2HzMf8ApcQ8ffLPYg%2BhGFu3EIzbjLu%2FIHICBqpB8PwCrZJOa%2FNbzd28F7LHEFm%2BAlRMUGQ%3D%3D" rel="nofollow">go-micro 微服务开发中文手册</a></li><li><a href="https://link.segmentfault.com/?enc=V60NO4EGsWAy5YbeQylXrg%3D%3D.SNlG%2FG9cKi2jYZm0UqGoT1jcWU2uW6X3H9WUtYThV%2BTowozKO0ELwlQl05Kkm6mbBF5UOdcVSjtES91aR5ERQXUCTT%2BHMyaIoInZgWSofJk%3D" rel="nofollow">Writing microservices with Go Micro</a></li><li><a href="https://link.segmentfault.com/?enc=Ib%2Bwo%2Frd8LUGcki4WPwL%2FQ%3D%3D.WwAcc9EO6LZDF8r8wuS2l4NlSh6%2Br0imz8FzCvgMycCGQ2i9Z5mgdc3nKj9y7med" rel="nofollow">Introduction to gRPC</a></li><li><a href="https://link.segmentfault.com/?enc=hedZdv0eWudwTWVBDbEf%2Fg%3D%3D.3PldNE4V5211KaKcoy1BWPgY5pUtPkdI9z36WT1KHb3ed%2Fuuyg39TtAj3td4B6n01hlmdfPgkpZd7isfbdTsmHL5SIvXhkghwNnJZstf0vI%3D" rel="nofollow">Hands-On-Restful-Web-services-with-Go</a></li><li><a href="https://link.segmentfault.com/?enc=e466%2Bxw9iF3xEX5j0gZFaQ%3D%3D.bZo8uSdzEb3g5KORdP2msMjzddpBdCO2EEZJAXqn693pxSnObG2Q1Nr%2BfL8n7yxL" rel="nofollow">https://github.com/go-micro/g...</a></li><li><a href="https://link.segmentfault.com/?enc=eytmdIzSqZDWGjogdZySTg%3D%3D.zbRXNshPt%2BZFn49EFwjXXHTiIhpwMmmdl7tMXGURDd615FcA7egsAA6AgHnI9lYO" rel="nofollow">https://github.com/go-micro/g...</a></li></ul><blockquote>本文参与了<a href="https://segmentfault.com/a/1190000042741155">思否技术征文</a>,欢迎正在阅读的你也加入。</blockquote>
Go 微服务实战之如何使用 go-micro 写微服务应用
https://segmentfault.com/a/1190000042756262
2022-11-07T18:26:52+08:00
2022-11-07T18:26:52+08:00
宇宙之一粟
https://segmentfault.com/u/yuzhoustayhungry
2
<h2>什么是微服务?</h2><p>什么是微服务(microservice)?这是企业界正在向计算界提出的问题。一个产品的可持续性取决于它的可修改程度。</p><p>大型产品如果不能正常维护,就需要在某个时间点停机维护。而微服务架构用细化的服务取代了传统的单体服务,这些服务定义了明确的 RPC 或消息驱动的 API 边界。</p><p>微服务架构有别于更为传统的单体式方案,可将应用拆分成多个核心功能。每个功能都被称为一项服务,可以单独构建和部署,这意味着各项服务在工作(和出现故障)时不会相互影响。</p><p>微服务带来了以下好处:</p><ul><li>每个服务都可以由专注于此服务的团队独立开发。小团队可以通过在一组小的功能上工作来进行并行迭代。</li><li>开发人员可以自由选择开发技术,对新的开发人员来说,可扩展性很强。</li><li>微服务架构可以使每个微服务独立部署。对系统的单个组件支持持续集成(CI)和持续交付(CD)。</li><li>微服务架构使得每个服务都可独立扩展。利用松耦合的架构提供更轻松的软件替换。</li><li>微服务架构不与特定的技术相联系。</li></ul><p>在谈论微服务时,编排和服务发现是微服务中非常重要的部分。像 Kubernetes 这样的工具可以用来编排和协调 Docker 容器。一般来说,微服务的最佳实践就是每个微服务有一个 Docker 容器。</p><p><strong>服务发现</strong>是对微服务实例的 IP 地址的自动检测。这种方式消除了硬编码 IP 地址的潜在威胁,硬编码会导致服务之间缺乏联系。</p><p><img src="/img/bVc3yy4" alt="概念模型中的服务发现" title="概念模型中的服务发现"></p><h2>单体架构与微服务架构的区别</h2><p>下图描绘了单体架构和微服务架构的结构图。</p><p>图的左边就是单体架构的示意图,如图所示:单体架构将所有的功能(如 UI、日志、数据层、系统逻辑、数据库等)都集成在一个系统中,像是一个紧耦合的架构。</p><p>相反,微服务是独立的实体,每个功能都是单独的服务,如日志服务、文件服务、系统逻辑服务等,更易于修改和替换,每个服务都可以通过各种远程传输机制进行沟通,如 HTTP、REST 或者 RPC。服务之间的交换的数据格式可以是 JSON 或者 Protocol buffers, 微服务还可以处理各种请求点,如 UI 和 API 客户端。</p><p><img src="/img/bVc3yJN" alt="单体架构与微服务架构" title="单体架构与微服务架构"></p><p>微服务可以被任何语言实现(Java、Go、Python、 Rust、 NodeJS 等),因为其有着松耦合的性质,每个独立的服务还可以今后被任何其他新技术或业务所需要的技术所替换。</p><blockquote>关于微服务的相关知识就简单介绍到这,感兴趣的同学可以看看文末的推荐阅读部分,都是非常好的微服务学习资料。</blockquote><h2>Go Micro 介绍</h2><h3>优秀微服务框架一览</h3><p>Java 社区中有着非常著名的框架用于构建微服务系统。如:</p><ul><li><a href="https://link.segmentfault.com/?enc=5qvy%2FYxrtfK4mX8dhQusjQ%3D%3D.zJz1AGyJQwhY5KYi3zufMxyBu%2FR5OcOMunNl08D6QXc%3D" rel="nofollow">Spring</a>: Spring Boot 是用于编写微服务的流行 Java 框架。</li><li><a href="https://link.segmentfault.com/?enc=GeAVmm0KijLP4hpt%2BxGvaw%3D%3D.uH6fQhW5fej3ww5AeMcjpiaOlxGgFBF%2BPQ2%2BOd5KFQcgcNtBFJw43RJOOTVS3MZ7" rel="nofollow">Spring Cloud</a>:基于 Spring Boot,为微服务体系开发中的架构问题,提供了一整套的解决方案——服务注册与发现,服务消费,服务保护与熔断,网关,分布式调用追踪,分布式配置管理等。</li><li><a href="https://link.segmentfault.com/?enc=9koCC%2FxxDw9BPL3UDnkd9Q%3D%3D.9rVMKgugSALvR1iLzr7%2BIIj8D6P%2BS0fcgNUAtkJ1MeWWqcP7rDvQE%2F%2FvoBNEAsbV" rel="nofollow">Dropwizard</a>:一个开源的 RESTful 快速开发框架,对微服务的开发也极其友好,而且性能很强</li><li><a href="https://link.segmentfault.com/?enc=cezAlHiGW4C4N%2FX7gbdsGQ%3D%3D.33L0J2f62e%2BIJnP0zyqT%2BsANAsy3Jl231kj0ibKgvHw%3D" rel="nofollow">Micronaut</a>:是一个现代的、基于 JVM 的全栈微服务框架,旨在构建模块化、易于测试的微服务应用程序</li><li><a href="https://link.segmentfault.com/?enc=EgQsv%2FxV3nvPauc4Vdro9g%3D%3D.Tfm3qrruuBVIw%2FTWxFfBBZtBEImjMTQJzFit6thw3vs%3D" rel="nofollow">Apache Dubbo</a>:由阿里巴巴开源的分布式服务化治理框架,是一款微服务框架,为大规模微服务实践提供高性能 RPC 通信、流量治理、可观测性等解决方案,涵盖 Java、Golang 等多种语言 SDK 实现。</li></ul><p>以上都是非常有名的微服务框架,在 Go 语言中,也有很多著名的框架(<a href="https://link.segmentfault.com/?enc=93ITWcKNPrjyoeugO19Dpw%3D%3D.kYXZUhCBJRUsTKr2SXZbAC51CwwQ0YY2QAY47hia86k%3D" rel="nofollow">go-kit</a>、<a href="https://link.segmentfault.com/?enc=hygOnMC9FJqUwx6j5tJ%2Fmg%3D%3D.9a%2FCzqIdqqwgpn5%2FXPieWt71O%2BTuQ6mYL30%2BkSKv3%2B4%3D" rel="nofollow">go-kratos</a>、<a href="https://link.segmentfault.com/?enc=LO6ZjPzf12uACirV7STKgQ%3D%3D.slyw6MsLoRuj6zLzobznWgNgqy9MIn3qyGQ5PjTPFVs%3D" rel="nofollow">go-zero</a> 等), Go Micro 也是其中之一,截止发文 Github Star 数量达到了 19.6k。</p><p><img src="/img/bVc3yPg" alt="go-micro" title="go-micro"></p><p><a href="https://link.segmentfault.com/?enc=tiH1pCmINPBtDGLEKGPdog%3D%3D.Jfh%2FOEmmoxWlMTD2EvVuri10Rh0yUG2oygd9BwAjOBqR9y8J32iRbNb2Bd1Rmw2y" rel="nofollow">Go Micro</a> 是一个基于 RPC 的可插拔库,它提供了在 Go 中编写微服务的基本构建块。它使用 consul 实现服务发现,但可以换成 etcd、zookeeper 或任何能够满足该接口的其他实现。通过 http 或使用 proto-rpc 或 json-rpc 进行通信, </p><p>Go Micro 解决了构建可扩展系统的关键要求。它采用微服务架构模式并将其转换为一组充当平台构建块的工具。Micro 处理分布式系统的复杂性,并提供开发人员已经理解的简单抽象。</p><p>Go Micro 提供了 RPC 实现和事件驱动架构(EDAs),可以向其添加任何外部功能。如果你想换掉底层技术,代码重写率为零。</p><h3>Go Micro 特点</h3><p>Go Micro 的主要特点有:</p><ul><li>RPC Client/Server:基于 RPC 的请求/响应,支持双向流。为同步通信提供了一个抽象层,向一个服务提出的请求将被自动处理、负载均衡、拨号和流化。</li><li>服务发现: 自动服务注册和名称解析。服务发现是微服务开发的核心。当服务 A 需要与服务 B 对话时,它需要该服务的位置。默认的发现机制(zeroconf 系统)是多播 DNS(mdns)机制。</li><li>负载均衡:客户端负载均衡建立在服务发现的基础上。一旦我们有了一个服务的任何数量的实例的地址,我们现在需要一种方法来决定哪个节点的路由。我们使用随机散列的负载均衡来提供跨服务的均匀分布,并在出现问题时重试一个不同的节点。</li><li>信息编码:基于内容类型的<strong>动态信息编码</strong>。客户端和服务器将与内容类型一起使用编解码器,为你无缝编码和解码 Go 类型。任何种类的消息都可以被编码并从不同的客户端发送。客户端和服务器默认会处理这个问题。这包括默认的 protobuf 和 json 格式。</li><li>信息同步:发布/订阅(PubSub) 是作为异步通信和事件驱动架构的第一类公民而建立的。事件通知是微服务开发的一个核心模式。默认的消息传递系统是一个 HTTP 事件消息代理。</li><li>事件流: PubSub 对于异步通知来说是很好的,但对于更高级的用例,事件流是首选。提供持久性存储,从网络中的 offset(片偏移量) 和 acking(确认字符) 中进行消费。 Go Micro 包括对NATS Jetstream和 Redis 流的支持。</li><li>同步化:分布式系统通常以最终一致的方式构建。对分布式锁和领导节点的支持是作为 Sync 接口建立的。当使用最终一致的数据库或调度时,使用 Sync 接口。</li><li><p>可插拔接口:Go Micro 对每个分布式系统的抽象都使用了 Go 接口。正因为如此,这些接口是可插拔的,使 Go Micro 与运行时间无关。您可以在底层使用任何可用技术。 例如用于翻译的编解码器,用于存储系统的 brokers。</p><ul><li>插件地址:<a href="https://link.segmentfault.com/?enc=0hcu%2FeITXiK6J78tcBYmlQ%3D%3D.kX22JllQMKlxiyC4MgwXZKIDVr9eML%2BZxvQqj8mKO4cNTLSnh0TZqqW3K%2F0zQpoU" rel="nofollow">https://github.com/go-micro/p...</a></li></ul></li></ul><p>接下来,让我们动手写一个服务。</p><h2>服务接口</h2><p>顶层的服务接口是构建服务的主要组件。它把 Go Micro 的所有底层包都包装成一个方便的接口。</p><pre><code class="go">type Service interface {
Init(...Option)
Options() Options
Client() client.Client
Server() server.Server
Run() error
String() string
}</code></pre><h3>go-micro 安装</h3><p>使用下面的命令安装最新的 go-micro v4.9</p><pre><code>go install go-micro.dev/v4@latest</code></pre><p>本文在 Windows 11 安装成功如下:</p><p><img src="/img/bVc3yZC" alt="image.png" title="image.png"></p><p>使用 <code>micro.NewService</code> 创建一个新服务:</p><pre><code class="go">package main
import (
"go-micro.dev/v4"
)
// 创建新服务
service := micro.NewService(
micro.Name("HelloWorld")
)
// 初始化 flags
service.Init()
// 启动服务
service.Run()</code></pre><p>其他选项可以在创建时传入:</p><pre><code class="go">service := micro.NewService(
micro.Name("greeter"),
micro.Version("latest"),
)</code></pre><p>Options 全部参数如下图:</p><p><img src="/img/bVc3yYD" alt="image.png" title="image.png"></p><h2>总结</h2><p>本文介绍了微服务及其相关概念,重点比较了单体架构和微服务架构的区别,最后介绍了 Go 优秀的微服务框架之一的 Go-micro,并展示该框架的安装和使用,下一篇文章将介绍如何利用这一框架进行微服务应用的开发。敬请期待~</p><p>希望本文能对你有所帮助,如果喜欢本文,可以点个赞或关注。</p><blockquote><p>这里是宇宙之一粟,下一篇文章见!</p><p>宇宙古今无有穷期,一生不过须臾,当思奋争。</p></blockquote><p>推荐阅读:</p><ul><li><a href="https://link.segmentfault.com/?enc=ZW%2FlZOXxvyCwzqs3kZ1aOw%3D%3D.tpvByOJk%2BT%2Bmh13ZSCyCp8CdiCxHDL5KHhwg99UbUc5sBrQdM0DOuyRRH%2FbnQcQHTNdFKzFzhqjtZsnjmZpHWA%3D%3D" rel="nofollow">微服务是什么?阮一峰的网络日志</a></li><li><a href="https://link.segmentfault.com/?enc=T8Y9qKtrWaGDZZpK9EDezQ%3D%3D.nK9IafWcnc%2B6B8o0ECfMb8oDamELu4tf%2FmTz5bp1WnOf264Jt7zwst82mKIi2Ofc7Zziyka7jpXbJRzxueTqMw%3D%3D" rel="nofollow">凤凰架构 | 服务发现</a></li><li><a href="https://link.segmentfault.com/?enc=T66Ie%2BQNY17RjyT8wm7H6Q%3D%3D.kmjIV75ggiTxuDJJz6RPXwy2QcQDciH%2FMUjUkQ3uWrsbl%2FhOuixD%2Brh1vgNIcECU" rel="nofollow">微服务:从设计到部署</a></li><li><a href="https://link.segmentfault.com/?enc=%2FNx64o%2FQT6PLKSeV9KQz6g%3D%3D.Sw4%2B4iS6IlGxUkXhqTxrnhQXJYJ6Io8EW%2BuzsUdw8MtGH%2F3CTV8ao8F%2FC8OclVame9R2HXOIIQrjpnf0M1mEiw%3D%3D" rel="nofollow">微服务架构设计</a></li><li><a href="https://link.segmentfault.com/?enc=%2Bia9%2B%2F%2Fu5OFLxEsZwAGeQQ%3D%3D.aHn61opdbsjbc0KJgBfZ5sWGRN%2F4C%2BXSel%2FZQZd8aH6mgwDTa9CD5OdmHBfb1hI9" rel="nofollow">go-micro 官方地址</a></li><li><a href="https://link.segmentfault.com/?enc=gcu98Z0DVyYrDFRKKrGfAg%3D%3D.Z6xVh8noRvFQ3kUd2QkdvqlQbv48%2BklVJDSXMVgHDm2UjwSd1Cg7A74Eq3raaB8L" rel="nofollow">go-micro 示例</a></li></ul><blockquote>本文参与了<a href="https://segmentfault.com/a/1190000042741155">思否技术征文</a>,欢迎正在阅读的你也加入。</blockquote>
Go Web实战之如何增加应用配置模块
https://segmentfault.com/a/1190000042745420
2022-11-04T15:35:14+08:00
2022-11-04T15:35:14+08:00
宇宙之一粟
https://segmentfault.com/u/yuzhoustayhungry
1
<h2>1 介绍</h2><p>当我们为自己编写程序时,通常会将一些重要的配置项直接写在源代码里,比如:服务器监听的端口、数据库使用的名称和端口号、HTTP请求超时的持续时间...</p><p>但是,如果我们尝试将这个项目开源分享给他人使用,用户使用的数据库的用户名和名称可能与你不相同,甚至你还要为他们的服务器使用另一个端口。</p><p>如果你还设置了数据库的密码的话,为了安全,更不可能在代码中信息泄露出来。因此,本节,将介绍如何增加我们的 <code>sports</code> 应用的配置模块。</p><h2>2 增加配置模块</h2><p>在许多的开源项目中,配置都是通过键值(key-value) 数据结构来处理的。在真实应用中,你经常会发现一个公开配置选项的类(或者是结构体),这个类经常会将文件解析出来,将每个选择赋值。应用程序通常会提出命令行选项以调整配置。</p><h3>2.1 定义 Configuration 接口</h3><p>接下来,我们为应用程序增加配置的能力,这样上面说的很多配置就不用在代码文件中定义。<br>1、创建 <code>sports/config</code> 文件夹,然后新建一个 <code>config.go</code> 文件,写入如下的代码:</p><pre><code class="go">package config
type Configuration interface {
GetString(name string) (configValue string, found bool)
GetInt(name string) (configValue int, found bool)
GetBool(name string) (configValue bool, found bool)
GetFloat(name string) (configValue float64, found bool)
GetStringDefault(name, defVal string) (configValue string)
GetIntDefault(name string, defVal int) (configValue int)
GetBoolDefault(name string, defVal bool) (configValue bool)
GetFloatDefault(name string, defVal float64) (configValue float64)
GetSection(sectionName string) (section Configuration, found bool)
}
</code></pre><p>可以看到,<code>Configuration</code> 接口定义了检索配置设置的方法,支持获取字符串 <code>string</code>、数字 <code>int</code>、浮点型 <code>float64</code>、布尔型 <code>bool</code> 的值:</p><ul><li><code>GetString()</code></li><li><code>GetInt()</code></li><li><code>GetBool()</code></li><li><code>GetFloat()</code></li></ul><p>还有一组方法允许提供一个默认值:</p><ul><li><code>GetStringDefault()</code></li><li><code>GetIntDefault()</code></li><li><code>GetBoolDefault()</code></li><li><code>GetFloatDefault()</code></li></ul><p>配置数据将允许嵌套的配置部分,这个将使用 <code>GetSection()</code> 方法实现。</p><h3>2.2 来看一个基本的 JSON 配置文件</h3><p>配置可以从命令行中获取,当然更好的方式是将配置保存在一个文件中,由应用程序自动解析。</p><p>文件的格式取决于应用程序的需求。如果你需要一个复杂的配置,有级别和层次(以 Windows 注册表的方式)关系的话,那么你可能需要考虑 JSON、YAML 或 XML 等格式。</p><p>让我们看一个 <code>JSON</code> 配置文件的例子:</p><pre><code class="json">{
"server": {
"host": "localhost",
"port": 80
},
"database": {
"host": "localhost",
"username": "myUsername",
"password": "abcdefgh"
}
}</code></pre><p>上面的 JSON 配置文件中定义了服务器 server 和数据库 database 的信息。但在本文中,我们基于上一节介绍的日志功能来看,为了简化操作,只简单配置我们的日志和主函数的信息。</p><p>2、在 <code>sports</code> 目录下,创建一个 <code>config.json</code> 文件,写入如下内容:</p><pre><code class="json">{
"logging": {
"level": "debug"
},
"main": {
"message": "Hello, Let's Go! Hello from the config file"
}
}</code></pre><p>这个配置文件定义了两个配置部分,分别命名为 <code>logging</code> 和 <code>main</code>:</p><ul><li><code>logging</code> 部分包含一个单一的字符串配置设置,名称为 <code>level</code></li><li><code>main</code> 部分包含一个单一的字符串配置设置,名称为 <code>message</code></li></ul><p>这个文件显示了配置文件使用的基本结构,在 JSON 配置文件中,要注意引号和逗号符合 JSON 文件的格式要求,很多人经常搞错。</p><h3>2.3 实现 Configuration 接口</h3><p>为了能够实现 <code>Configuration</code> 接口,我们将在 <code>sports/config</code> 文件夹下创建一个 <code>config_default.go</code> 文件,然后写入如下代码:</p><pre><code class="go">package config
import "strings"
type DefaultConfig struct {
configData map[string]interface{}
}
func (c *DefaultConfig) get(name string) (result interface{}, found bool) {
data := c.configData
for _, key := range strings.Split(name, ":") {
result, found = data[key]
if newSection, ok := result.(map[string]interface{}); ok && found {
data = newSection
} else {
return
}
}
return
}
func (c *DefaultConfig) GetSection(name string) (section Configuration, found bool) {
value, found := c.get(name)
if found {
if sectionData, ok := value.(map[string]interface{}); ok {
section = &DefaultConfig{configData: sectionData}
}
}
return
}
func (c *DefaultConfig) GetString(name string) (result string, found bool) {
value, found := c.get(name)
if found {
result = value.(string)
}
return
}
func (c *DefaultConfig) GetInt(name string) (result int, found bool) {
value, found := c.get(name)
if found {
result = int(value.(float64))
}
return
}
func (c *DefaultConfig) GetBool(name string) (result bool, found bool) {
value, found := c.get(name)
if found {
result = value.(bool)
}
return
}
func (c *DefaultConfig) GetFloat(name string) (result float64, found bool) {
value, found := c.get(name)
if found {
result = value.(float64)
}
return
}
</code></pre><p><code>DefaultConfig</code> 结构体用 map 实现了 <code>Configuration</code> 接口,嵌套配置部分也同样用 maps 表示。即上面的代码中的:</p><pre><code class="go">type DefaultConfig struct {
configData map[string] interface{}
}</code></pre><p>一个单独的配置可以通过将 <code>section</code> 名称和 <code>setting</code> 名称分开,例如:<code>logging:level</code>,或者使用 <code>map</code> 映射来根据键的名称或者值,例如 <code>logging</code> 。</p><h3>2.4 定义接收默认值的方法</h3><p>为了处理来自配置文件的值,我们在 <code>sports/config</code> 文件夹下创建一个 <code>config_default_fallback.go</code> 文件:</p><pre><code class="go">package config
func (c *DefaultConfig) GetStringDefault(name, val string) (result string) {
result, ok := c.GetString(name)
if !ok {
result = val
}
return
}
func (c *DefaultConfig) GetIntDefault(name string, val int) (result int) {
result, ok := c.GetInt(name)
if !ok {
result = val
}
return
}
func (c *DefaultConfig) GetBoolDefault(name string, val bool) (result bool) {
result, ok := c.GetBool(name)
if !ok {
result = val
}
return
}
func (c *DefaultConfig) GetFloatDefault(name string, val float64) (result float64) {
result, ok := c.GetFloat(name)
if !ok {
result = val
}
return
}
</code></pre><h3>2.5 定义从配置文件加载数据的函数</h3><p>在 <code>sports/config</code> 文件夹下新建一个加载 JSON 数据的 <code>config_json.go</code> 文件,写入如下代码:</p><pre><code class="go">package config
import (
"encoding/json"
"os"
"strings"
)
func Load(filename string) (config Configuration, err error) {
var data []byte
data, err = os.ReadFile(filename)
if err == nil {
decoder := json.NewDecoder(strings.NewReader(string(data)))
m := map[string]interface{}{}
err = decoder.Decode(&m)
if err == nil {
config = &DefaultConfig{configData: m}
}
}
return
}
</code></pre><p><code>Load</code> 函数读取一个文件的内容,将其包含的 <code>JSON</code> 文件解析为一个映射,并使用该映射创建一个 <code>DefaultConfig</code> 的值。</p><blockquote>关于 Go 如何处理 JSON 文件,感兴趣可以搜索我之前的文章:《Go 语言入门很简单:Go 语言解析JSON》</blockquote><h3>3 使用 Configuration 配置系统</h3><p>为了从刚刚增加的配置系统中获取日志级别的信息,我们将回到上一节中 logging 文件夹中的 <code>default_create.go</code> 文件中,写入如下代码:</p><pre><code class="go">package logging
import (
"log"
"os"
"strings"
"sports/config"
)
// func NewDefaultLogger(level LogLevel) Logger {
func NewDefaultLogger(cfg config.Configuration) Logger {
// 使用 Configuration
var level LogLevel = Debug
if configLevelString, found := cfg.GetString("logging:level"); found {
level = LogLevelFromString(configLevelString)
}
flags := log.Lmsgprefix | log.Ltime
return &DefaultLogger{
minLevel: level,
loggers: map[LogLevel]*log.Logger{
Trace: log.New(os.Stdout, "TRACE ", flags),
Debug: log.New(os.Stdout, "DEBUG ", flags),
Information: log.New(os.Stdout, "INFO ", flags),
Warning: log.New(os.Stdout, "WARNING ", flags),
Fatal: log.New(os.Stdout, "FATAL ", flags),
},
triggerPanic: true,
}
}
func LogLevelFromString(val string) (level LogLevel) {
switch strings.ToLower(val) {
case "debug":
level = Debug
case "information":
level = Information
case "warning":
level = Warning
case "fatal":
level = Fatal
case "none":
level = None
}
return
}
</code></pre><p>在 JSON 中没有很好的方法来表示 <code>iota</code> 值,所以我们使用一个字符串并定义了 <code>LogLevelFromString()</code> 函数,以此来将配置设置转换为 <code>LogLevel</code> 的值。</p><p>最后,我们更新 <code>main()</code> 函数来加载和应用配置数据,并使用配置系统来读取它所输出的信息,更改 <code>main.go</code> 文件如下。</p><pre><code class="go">package main
import (
// "fmt"
"sports/config"
"sports/logging"
)
// func writeMessage(logger logging.Logger) {
// // fmt.Println("Let's Go")
// logger.Info("Let's Go, logger")
// }
// func main() {
// var logger logging.Logger = logging.NewDefaultLogger(logging.Information)
// writeMessage(logger)
// }
func writeMessage(logger logging.Logger, cfg config.Configuration) {
section, ok := cfg.GetSection("main")
if ok {
message, ok := section.GetString("message")
if ok {
logger.Info(message)
} else {
logger.Panic("Cannot find configuration setting")
}
} else {
logger.Panic("Config section not found")
}
}
func main() {
var cfg config.Configuration
var err error
cfg, err = config.Load("config.json")
if err != nil {
panic(err)
}
var logger logging.Logger = logging.NewDefaultLogger(cfg)
writeMessage(logger, cfg)
}
</code></pre><p>至此,我们的配置是从 <code>config.json</code> 文件中获取,通过 <code>NewDefaultLogger()</code> 函数来传递 Configuration 的实现,最终读取到 log 日志级别设置。</p><p><code>writeMessage()</code> 函数显示了配置部分的使用,提供了组件所需的设置,特别是在需要多个具有不同配置的实例时,每一个设置都可以在自己的部分进行定义。</p><p>最后的项目结构如图:</p><p><img src="/img/bVc3wce" alt="image.png" title="image.png"></p><p>最终,我们在终端中编译并运行我们整个代码:</p><pre><code class="go">$ go run .
17:20:46 INFO Hello, Let's Go! Hello from the config file</code></pre><p>整个代码会输出并打印出配置文件中的信息,如图所示:</p><p><img src="/img/bVc3wcm" alt="image.png" title="image.png"></p><h2>4 总结</h2><p>本文介绍了项目配置文件的由来和重要性,并从零到一编写代码,成功在我们的 Web 项目中增加了应用配置功能。并结合上一节的日志功能进行了测试。</p><p>其实在 Go 开源项目中,有个非常著名的开源配置包:<a href="https://link.segmentfault.com/?enc=PjaJFz9XS3yyOGnnxtgDRA%3D%3D.6x4B6JrCSZiz9abfulaFqDsIDQyi6D5d06%2F%2FkSh6CCo%3D" rel="nofollow">Viper</a> ,提供针对 Go 应用项目的完整配置解决方案,帮助我们快速处理所有类型的配置需求和配置文件格式。目前 GitHub Stars 数量高达 21k,今后将在后续的文章中介绍这个项目。</p><p><img src="/img/bVc3wbR" alt="" title=""></p><p>今天的内容到此结束。更多配置相关的内容等着大家自行探索!</p><p>希望本文能对你有所帮助,如果喜欢本文,可以点个赞或关注。</p><blockquote><p>这里是宇宙之一粟,下一篇文章见!</p><p>宇宙古今无有穷期,一生不过须臾,当思奋争。</p></blockquote><p>参考链接:</p><ul><li><a href="https://link.segmentfault.com/?enc=t6PFqF2IzNACONW7HHuntg%3D%3D.djBbj2c6KmfuE3%2BPpOQqwel0TlkG6jmkcMzhQKDPte5xmlTuJyLkUIQi5GV%2B%2FX19MjKuZsnl4k2ERP7DrDNNyQrvdDMaJ6VSChUZ2x6hj1Q%3D" rel="nofollow">Application Configuration - Practical Go Lessons</a></li></ul>
Go Web 项目实战之如何创建项目及增加日志功能
https://segmentfault.com/a/1190000042745375
2022-11-04T15:30:52+08:00
2022-11-04T15:30:52+08:00
宇宙之一粟
https://segmentfault.com/u/yuzhoustayhungry
0
<p>从本文开始,我们来看一下如何从零搭建一个 Go 项目。</p><h2>回顾一下基础的 Go 项目运行过程</h2><p>首先,新建一个 <code>sports</code> 的文件,然后键入此文件目录下,选择在终端中打开,使用如下命令初始化项目:</p><pre><code class="go">go mod init sports</code></pre><p><img src="/img/remote/1460000042745377" alt="" title=""></p><p>然后,我们创建一个 <code>main.go</code> 的文件,写入如下代码:</p><pre><code class="go">package main
import "fmt"
func writeMessage() {
fmt.Println("Let's Go")
}
func main() {
writeMessage()
}</code></pre><p>回到终端,编译并执行我们的项目:</p><pre><code>go run .</code></pre><p>就像之前第一次写一个 <code>HelloWorld</code> 项目一样,<code>run</code> 命令会成功编译并执行我们的 <code>Println()</code> 内的字符串 Let's Go,输出结果如下:</p><pre><code class="go">$ go run .
Let's Go</code></pre><h2>创建一些基本的项目功能</h2><p>众所周知,Web 应用有一些基础的服务和功能如日志、文件配置等等。所以我们可以为 <code>sports</code> 项目提供一些基本的服务,这些服务将为运行网络应用程序提供基础。</p><h3>创建日志接口 Logger</h3><p>先来实现第一个服务器功能——日志。Go 标准库中的 <code>log</code> 包为创建日志提供了一套非常用户友好的基本功能,但在实际开发过程中,仍需要一些额外的功能来筛选一些信息详情。</p><p>创建 <code>sports/logging</code> 文件夹,然后在这个文件夹下面创建一个 <code>logging.go</code> 文件,然后写入如下代码:</p><pre><code class="go">package logging
type LogLevel int
const (
Trace LogLevel = iota
Debug
Information
Warning
Fatal
None
)
type Logger interface {
Trace(string)
Tracef(string, ...interface{})
Debug(string)
Debugf(string, ...interface{})
Info(string)
Infof(string, ...interface{})
Warn(string)
Warnf(string, ...interface{})
Panic(string)
Panicf(string, ...interface{})
}</code></pre><p>上面的代码定义了 <code>Logger</code> 接口,并具体声明了具体的日志消息的具体方法,分别有着不同的日志级别,<code>LogLevel</code> 从 <code>Trace</code> 到 <code>Fatal</code> 。值得注意的是也有一个无级别的 <code>None</code>, 意味着没有日志输出。</p><p>对于每个级别的重要性,<code>Logger</code> 接口定义两个方法:</p><ul><li>一个用于接受简单字符串的方法,如 <code>Debug</code></li><li>一个用于接收模板字符串和占位符值的方法,如 <code>Debugf</code></li></ul><p>将日志功能定义为接口的好处是:在需要更改或替换功能时,只需要更改实现,我们的调用接口并不需要改变;接口也使得功能作为服务提供给应用程序。</p><h3>Logger 接口的具体实现</h3><p>日志接口定义好了之后,我们来创建具体的 <code>Logger</code> 接口的实现。</p><p>创建一个 <code>logger_default.go</code> 的文件,然后写入如下的代码:</p><pre><code class="go">package logging
import (
"fmt"
"log"
)
type DefaultLogger struct {
minLevel LogLevel
loggers map[LogLevel]*log.Logger
triggerPanic bool
}
func (l *DefaultLogger) MinLogLevel() LogLevel {
return l.minLevel
}
func (l *DefaultLogger) write(level LogLevel, message string) {
if l.minLevel <= level {
l.loggers[level].Output(2, message)
}
}
func (l *DefaultLogger) Trace(msg string) {
l.write(Trace, msg)
}
func (l *DefaultLogger) Tracef(template string, vals ...interface{}) {
l.write(Trace, fmt.Sprintf(template, vals...))
}
func (l *DefaultLogger) Debug(msg string) {
l.write(Debug, msg)
}
func (l *DefaultLogger) Debugf(template string, vals ...interface{}) {
l.write(Debug, fmt.Sprintf(template, vals...))
}
func (l *DefaultLogger) Info(msg string) {
l.write(Information, msg)
}
func (l *DefaultLogger) Infof(template string, vals ...interface{}) {
l.write(Information, fmt.Sprintf(template, vals...))
}
func (l *DefaultLogger) Warn(msg string) {
l.write(Warning, msg)
}
func (l *DefaultLogger) Warnf(template string, vals ...interface{}) {
l.write(Warning, fmt.Sprintf(template, vals...))
}
func (l *DefaultLogger) Panic(msg string) {
l.write(Fatal, msg)
if l.triggerPanic {
panic(msg)
}
}
func (l *DefaultLogger) Panicf(template string, vals ...interface{}) {
formattedMsg := fmt.Sprintf(template, vals...)
l.write(Fatal, formattedMsg)
if l.triggerPanic {
panic(formattedMsg)
}
}</code></pre><p><code>NewDefaultLogger()</code> 函数创建一个具有最小严重程度日志 <code>DefaultLogger</code> 和 <code>log.Loggers</code> 将消息写入标准输出。</p><p>最终我们的目录结构如下:</p><p><img src="/img/remote/1460000042745378" alt="" title=""></p><h3>测试日志功能</h3><p>为了做一个简单的测试,我们可以修改最开始的 <code>main()</code> 函数,以便查看日志功能是否能成功打印出日志消息,回到 <code>main.go</code> ,写入如下代码:</p><pre><code class="go">package main
import (
// "fmt"
"sports/logging"
)
func writeMessage(logger logging.Logger) {
// fmt.Println("Let's Go")
logger.Info("Let's Go, logger")
}
func main() {
var logger logging.Logger = logging.NewDefaultLogger(logging.Information)
writeMessage(logger)
}</code></pre><p>由 <code>NewDefaultLogger</code> 创建的最小日志级别被设置为 <code>Information</code>,这意味着更低的日志级别(<code>Trace</code> 和 <code>Debug</code>)将被忽略,再次编译和执行 <code>sports</code> 项目,就会看到本地的日志输出,可能你按照我的操作只会时间戳不同,如下:</p><pre><code class="go">$ go run .
11:46:55 INFO Let's Go, logger</code></pre><p>终端结果如图所示:</p><p><img src="/img/remote/1460000042745379" alt="" title=""></p><h2>总结</h2><p>到了本文的总结时刻了,为了学习 Go Web 项目,本文先回顾了一下 Go 项目的初始化构建和运行过程,然后往我们的 <code>sports</code> 项目中添加了一个日志功能,逐步完善日志接口和实现,最后在主函数中进行了一个简单的日志输出测试。</p><blockquote><p>希望本文能对你有所帮助,如果喜欢本文,可以点个关注。</p><p>这里是宇宙之一粟,下一篇文章见!</p><p>宇宙古今无有穷期,一生不过须臾,当思奋争。</p></blockquote>
一文带你了解 Java 中的构造器
https://segmentfault.com/a/1190000042508883
2022-09-19T11:12:21+08:00
2022-09-19T11:12:21+08:00
宇宙之一粟
https://segmentfault.com/u/yuzhoustayhungry
0
<p>C ++ 引入了构造器(constructor,也叫构造函数)的概念,它是在创建对象时被自动调用的<strong>特殊方法</strong>。</p><p>Java 也采用了构造器,并且还提供了一个垃圾收集器(garbage collector),当不再使用内存资源的时候,垃圾收集器会自动将其释放。</p><h2>构造器定义</h2><p>在 Java 中,可以通过编写构造器来确保每个对象的初始化。但是这里有两个问题:</p><ol><li>这个构造器使用的任何名字都有可能与类里某个成员相冲突;</li><li>编译器负责调用构造器,所以它必须始终知道应该调用哪个方法。</li></ol><p>C++ 语言采用的方案就是将<strong>构造器和类的名字定义相同</strong>,Java 也采用了这个方案。</p><p>构造器的作用是用来建立一个新的类的实例,当一个对象被创建时,JVM 使用一个构造函数,并为其分配内存空间。</p><h3>语法结构</h3><pre><code>class ClassName {
ClassName() {
}
}</code></pre><p>例如,在下面的示例中,我们创建了一个名为 <code>ReLearnConstructor</code> 的构造函数。在构造函数内部,我们正在初始化 <code>hello</code> 变量的值。:</p><pre><code>
public class ReLearnConstructor {
String hello; // 属性
// 构造器
public ReLearnConstructor() {
hello = "Hello, Constructor!";
}
public static void main(String[] args) {
ReLearnConstructor rc = new ReLearnConstructor();
System.out.println(rc.hello);
}
}</code></pre><p>注意创建 ReLearnConstructor 类的对象的语句:<code>ReLearnConstructor rc = new ReLearnConstructor();</code></p><p>在这里,当创建对象时,调用 <code>ReLearnConstructor</code> 构造函数。并且,<code>hello</code> 变量的值被初始化。</p><p>因此打印的 <code>hello</code> 的值为:</p><p><img src="/img/remote/1460000042508886" alt="" title=""></p><h3>构造器目的</h3><p>构造函数的目的是<strong>初始化对象的状态</strong>,为所有声明的属性赋值。如果我们没有自定义构造函数,JVM 就会为这些属性分配默认值。</p><p><strong>原始类型</strong>的默认值:</p><ul><li>整数类型是 0</li><li>浮点类型是 0.0</li><li>布尔类型是 false</li></ul><p>对于其他 Java 引用类型,默认值是null,这意味着引用类型的属性没有被分配任何值。</p><p><img src="/img/remote/1460000042508887" alt="" title=""></p><p>后面可以用代码查看这些默认值。</p><h2>构造器分类</h2><p><img src="/img/remote/1460000042508888" alt="" title=""></p><p>在 Java 中,有三种类型的构造器:</p><ol><li><strong>无参构造器</strong></li><li><strong>有参构造器</strong></li><li><strong>默认构造器</strong></li></ol><h3>无参构造器</h3><p>与方法类似,Java 构造函数可能有参数,也可能没有任何参数。如果构造函数不接受任何参数,则称为无参数构造器。例如上述代码中 <code>ReLearnConstructor</code> 构造器就是:</p><pre><code>// 无参构造器
public ReLearnConstructor() {
hello = "Hello, Constructor!";
}</code></pre><h3>有参构造器</h3><p>字面理解,具有参数的构造函数称为有参数构造器。那为什么需要使用有参构造器?</p><p>有参构造器可用于为<strong>不同对象提供不同初始化的值。</strong> 例如:</p><pre><code>
public class ReLearnConstructor {
String languages;
// 接受单个参数的构造器
public ReLearnConstructor(String lang) {
languages = lang;
System.out.println("我在学习 " + languages + " 语言!");
}
public static void main(String[] args) {
// 向构造器中传入不同的值
ReLearnConstructor rc1 = new ReLearnConstructor("Java");
ReLearnConstructor rc2 = new ReLearnConstructor("Go");
ReLearnConstructor rc3 = new ReLearnConstructor("Python");
}
}</code></pre><p>运行结果:</p><p><img src="/img/remote/1460000042508889" alt="" title=""></p><p></p><h3>默认构造器</h3><p>如果我们不创建任何构造函数,Java 编译器会在程序执行期间自动创建一个无参数构造函数。这个构造函数称为<strong>默认构造函数</strong>。来看一个例子;</p><pre><code>
public class ReLearnConstructor {
String languages;
int a;
boolean b;
float c;
public static void main(String[] args) {
ReLearnConstructor rc = new ReLearnConstructor();
System.out.println("默认值:");
System.out.println("languages:" + rc.languages);
System.out.println("a:" + rc.a);
System.out.println("b:" + rc.b);
System.out.println("c:" + rc.c);
}
}</code></pre><p>运行结果:</p><pre><code>默认值:
languages:null
a:0
b:false
c:0.0</code></pre><p>可以看到,我们还没有创建任何构造函数。因此,Java 编译器会自动创建默认构造函数。上述表格得以印证。</p><h2>原生方法和构造器的区别</h2><ul><li>构造函数必须与在 Java 中定义的类具有相同的名称</li><li>当方法没有返回任何值时,构造函数不会返回任何类型,而方法则具有返回类型或 void</li><li>在对象创建时,仅调用构造函数一次,而方法可以被调用任何次数</li></ul><p>如果我们不用构造器来给属性赋值的话,可以先使用 new 运算符获取类的实例,并使用类的 setter 方法设置值,如下:</p><pre><code>import java.util.Arrays;
class Person
{
private String name;
private int age;
@Override
public String toString() {
return Arrays.asList(name, String.valueOf(age)).toString();
}
public void setName(String name) {
this.name = name;
}
public void setAge(int age) {
this.age = age;
}
// getters
}
// Initialize an object in Java
class Main
{
public static void main(String[] args)
{
Person person = new Person();
person.setName("Yuzhou1su");
person.setAge(22);
System.out.println(person);
}
}</code></pre><p>通过构造器进行初始化就可以省去我们的 setter 方法。</p><p>如下的例子:</p><pre><code>import java.util.Arrays;
class Person {
private String name;
private int age;
// 构造器
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String toString() {
return Arrays.asList(name, String.valueOf(age)).toString();
}
}
class SimpleConstructor {
public static void main(String[] args) {
Person person = new Person("Yuzhou1su", 22);
System.out.println(person);
}
}</code></pre><p>运行结果:</p><pre><code>[Yuzhou1su, 22]</code></pre><h2>构造器重载</h2><p>与 Java 方法重载类似,我们也可以创建两个或多个具有不同参数的构造函数。这称为构造函数重载。</p><pre><code>
public class ReLearnConstructor {
String language;
public ReLearnConstructor() {
this.language = "Java";
}
// 构造器
public ReLearnConstructor(String language) {
this.language = language;
}
public void getName() {
System.out.println("编程语言:" + this.language);
}
public static void main(String[] args) {
ReLearnConstructor rc1 = new ReLearnConstructor();
ReLearnConstructor rc2 = new ReLearnConstructor("Python");
rc1.getName();
rc2.getName();
}
}</code></pre><p>在上面的例子中,我们有两个构造函数:<code>ReLearnConstructor()</code> 和 <code>ReLearnConstructor(String language)</code>。在这里,两个构造函数都用不同的值初始化变量语言的值。根据创建对象时传递的参数,调用不同的构造函数,分配不同的值。</p><p>运行结果:</p><p><img src="/img/remote/1460000042508890" alt="" title=""></p><pre><code>编程语言:Java
编程语言:Python</code></pre><h2>拷贝构造器</h2><p>Java 中的拷贝构造方法是一种使用该类的一个对象构造另外一个对象的构造方法。</p><p>复制构造函数是一种特殊构造函数,用于将新对象创建为现有对象的副本。它只需要一个参数,它将是同一类的另一个实例。我们可以使用 <code>this()</code> 语句从复制构造函数中显式调用另一个构造函数:</p><pre><code>
public class ReLearnConstructor {
private String language;
// 构造器
public ReLearnConstructor(String language) {
this.language = language;
}
// 拷贝构造器
public ReLearnConstructor(ReLearnConstructor rc) {
this.language = rc.language;
}
public void getName() {
System.out.println("编程语言:" + this.language);
}
public static void main(String[] args) {
ReLearnConstructor rc = new ReLearnConstructor("Python");
ReLearnConstructor copyOfrc = new ReLearnConstructor(rc);
rc.getName();
copyOfrc.getName();
}
}</code></pre><p>运行结果:</p><pre><code>编程语言:Python
编程语言:Python</code></pre><p>当需要拷贝一个带有多个成员变量的复杂对象或者想构造已存在对象的深拷贝对象时非常有用。</p><h2>匿名内部类</h2><p>除了上文介绍的使用构造器的方法,另一种初始化对象的方法是使用“双大括号初始化”。这将创建一个<strong>匿名内部类</strong>,其中只有一个实例初始化程序。建议不要使用这种方法。</p><pre><code>import java.util.Arrays;
class Person
{
private String name;
private int age;
@Override
public String toString() {
return Arrays.asList(name, String.valueOf(age)).toString();
}
public void setName(String name) {
this.name = name;
}
public void setAge(int age) {
this.age = age;
}
// getters
}
// Initialize an object in Java
class Main
{
public static void main(String[] args)
{
// Anonymous class
Person person = new Person() {{
// Initializer block
setName("Yuzhou1su");
setAge(22);
}};
System.out.println(person);
}
}</code></pre><h2>总结</h2><ul><li>实例化对象时会隐式调用构造函数。</li><li>创建构造函数的两条规则是:构造函数的名称应与类相同。Java 构造函数不能有返回类型。</li><li>如果一个类没有构造函数,Java 编译器会在运行时自动创建一个默认构造函数。默认构造函数使用默认值初始化实例变量。例如 int 变量将被初始化为 0</li><li>构造函数类型:</li><li>无参构造器 - 不接受任何参数的构造函数参数化构造函数</li><li>接受参数的构造器 - 接受参数的构造函数</li><li>默认构造器 - 如果没有明确定义,Java 编译器会自动创建一个构造函数。</li><li>构造函数不能被 <code>abstract</code>、<code>static</code> 或 <code>final</code> 修饰</li></ul><p>编译器会报如下错误:</p><pre><code>Illegal modifier for the constructor in type ReLearnConstructor; only public, protected & private are permitted</code></pre><ul><li>构造函数可以重载但不能被覆盖</li></ul><blockquote>本文参与了<a href="https://segmentfault.com/a/1190000042453662">思否技术征文</a>,欢迎正在阅读的你也加入。</blockquote>
手把手带你入门 API 开发
https://segmentfault.com/a/1190000042057687
2022-07-01T14:14:53+08:00
2022-07-01T14:14:53+08:00
宇宙之一粟
https://segmentfault.com/u/yuzhoustayhungry
0
<h2>引言</h2><p><img src="/img/remote/1460000042057689" alt="" title=""></p><p>在本文中,您将学习如何使用<a href="https://link.segmentfault.com/?enc=eLIpfx784z3sEqdSyx0Ggw%3D%3D.LUpZ17wNHlgX6Ra2oI2y6qJp8N71HA4BUEg%2FrpUGVRTMJzCNWfkrdTpG4wa8%2FUMo" rel="nofollow"> Flask</a>、<a href="https://link.segmentfault.com/?enc=XrNsfqRcwCR9kc4ENE2asg%3D%3D.fbybKRblhVSS1xx6teNjEVST8ixUdXEq54ihq0oJBIDAC1a3dlv8H%2BfaMNzbjfAH" rel="nofollow">SQLite 3</a>(轻易数据库)和 JSON 创建用于数据通信的 REST API。</p><p>本文使用 4 个最常用的 HTTP 动词:GET、POST、PUT 和 DELETE,对应数据库的 CRUD 操作。</p><p>比如管理的是一个游戏数据库 <code>games.db</code>,其中包含名称(name)、价格(price) 和等级(rate)。</p><p>我们还将通过使用 Flask 创建的 API 公开几个操作:</p><ul><li>获取所有游戏</li><li>创建一个新游戏</li><li>更新游戏</li><li>删除游戏</li><li>通过 ID 获取游戏</li></ul><p>首先,我们将使用 Python 创建与数据库相关的 CRUD,然后我们将在 API 中使用 Flask 公开所有这些函数,编码格式为 JSON。</p><h2>安装 SQLite</h2><ol><li>点击<a href="https://link.segmentfault.com/?enc=YvMMfimKYjPu3K%2Fpc0Zxbw%3D%3D.pfUcr7Lr4rZ4rX8s%2BUYQ0XtKsDA3UPnhXob0YIfTSkcjTx9m5KDXWql06%2FtEELG3" rel="nofollow">此处</a>,下载你的系统对应的 SQLite 版本,本文以 Windows 为例:</li></ol><p><img src="/img/remote/1460000042057690" alt="" title=""></p><ol><li>下载后将这两个压缩包内的文件 解压到 C 盘的某个目录下:</li></ol><p><img src="/img/remote/1460000042057691" alt="" title=""></p><ol><li>并将该目录添加至环境变量:</li></ol><p><img src="/img/remote/1460000042057692" alt="" title=""></p><ol><li>查看 SQLite3 版本:</li></ol><pre><code class="bash">λ sqlite3
SQLite version 3.38.5 2022-05-06 15:25:27
Enter ".help" for usage hints.
Connected to a transient in-memory database.
Use ".open FILENAME" to reopen on a persistent database.
sqlite></code></pre><h2>创建数据库</h2><p>使用 <code>sqlite3 databaseName.db</code> 命令来创建一个 SQLite 数据库,本文创建一个 <code>games.db</code> 数据库:</p><pre><code class="shell">λ sqlite3 games.db
SQLite version 3.38.5 2022-05-06 15:25:27
Enter ".help" for usage hints.
sqlite> .databases
main: C:\Program Files\cmder\games.db r/w</code></pre><h2>创建表</h2><pre><code class="sql">CREATE TABLE IF NOT EXISTS games(
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
price REAL NOT NULL,
rate INTEGER NOT NULL
)</code></pre><h2>创建 db.py</h2><p>我们上个步骤中看到数据库将被称为 <code>games.db</code> 。新建 Python 的 SQLite3 连接文件 <code>db.py</code>:</p><pre><code class="python">import sqlite3
DATABASE_NAME = "games.db"
# 获取数据库连接
def get_db():
conn = sqlite3.connect(DATABASE_NAME)
return conn
# 创建数据库表
def create_tables():
tables = [
"""
CREATE TABLE IF NOT EXISTS games (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
price REAL NOT NULL,
rate INTEGER NOT NULL
)
"""
]
db = get_db()
cursor = db.cursor()
for table in tables:
cursor.execute(table)</code></pre><p>中,此外,我们有两个功能:</p><ul><li>其中之一是 <code>get_db()</code>:用于获得数据库连接</li><li>另一个功能 <code>create_tables()</code>:是当 <code>games</code> 数据库表不存在的情况下创建数据库表。</li></ul><p>现在我们已经定义了数据库,让我们看看使用 SQLite3 数据库的游戏的 CRUD 操作。</p><h2>创建 game_controller.py</h2><p>在 API 中公开数据库之前,我们将创建一个游戏控制器,它将负责保存、更新、删除和获取游戏数据的所有操作。</p><p>所有这些函数都在一个名为 <code>game_controller.py</code> 的文件中,它看起来像这样:</p><pre><code class="python">from db import get_db
def insert_game(name, price, rate):
db = get_db()
cursor = db.cursor()
statement = "INSERT INTO games(name, price, rate) VALUES (?, ?, ?)"
cursor.execute(statement, [name, price, rate])
db.commit()
return True
def update_game(id, name, price, rate):
db = get_db()
cursor = db.cursor()
statement = "UPDATE games SET name = ?, price = ?, rate = ? WHERE id = ?"
cursor.execute(statement, [name, price, rate, id])
db.commit()
return True
def delete_game(id):
db = get_db()
cursor = db.cursor()
statement = "DELETE FROM games WHERE id = ?"
cursor.execute(statement, [id])
db.commit()
return True
def get_by_id(id):
db = get_db()
cursor = db.cursor()
statement = "SELECT id, name, price, rate FROM games WHERE id = ?"
cursor.execute(statement, [id])
return cursor.fetchone()
def get_games():
db = get_db()
cursor = db.cursor()
query = "SELECT id, name, price, rate FROM games"
cursor.execute(query)
return cursor.fetchall()</code></pre><p>在文件中,我们看到了几个函数。 <code>insert_game</code> 函数接收游戏数据并将其插入数据库(INSERT);所有这些都使用准备好的语句来避免我们使用 Python 和 Flask 创建的这个 API 中的 SQL 注入。</p><p>我们还看到其他方法,例如 <code>update_game</code> 执行 UPDATE 操作以更新游戏,<code>delete_game</code> 从其 id 删除游戏 (DELETE),<code>get_by_id</code> 从其 id 返回游戏(使用 SELECT 操作)。</p><p>最后我们看看返回所有现有游戏的 <code>get_games</code> 函数。</p><p>请注意,所有函数都使用数据库和游标来执行所有操作。 现在我们有了数据库操作的 CRUD,是时候用 Flask 公开 API 中的所有内容了</p><h2>创建 main.py</h2><p><img src="/img/remote/1460000042057693" alt="main.py" title="main.py"></p><h3>安装 flask</h3><p><img src="/img/remote/1460000042057694" alt="" title=""></p><p>可以查看官方<a href="https://link.segmentfault.com/?enc=ezEEykOU3599gX8Fr8TbRg%3D%3D.lFq5Ssm1NT%2BLeC7z99vugJYkaZ8Huvp68aC3Ne5fSfjfNlUkkyCW6yZUPSYuQhqKtuPQz2zxl0alH8Cz%2BszShg%3D%3D" rel="nofollow">安装帮助</a>:</p><pre><code>$ pip install Flask</code></pre><p>我们在 API 中做的第一件事是创建 Flask 应用程序并导入游戏控制器。我们还从数据库中导入了一个函数,因为我们需要在启动应用程序时创建表:</p><pre><code class="python">from flask import Flask, jsonify, request
import game_controller
from db import create_tables
app = Flask(__name__)</code></pre><p>然后使用 GET、PUT、POST 和 DELETE http 动词定义路由:</p><pre><code class="python">@app.route('/games', methods=["GET"])
def get_games():
games = game_controller.get_games()
return jsonify(games)
@app.route("/game", methods=["POST"])
def insert_game():
game_details = request.get_json()
name = game_details["name"]
price = game_details["price"]
rate = game_details["rate"]
result = game_controller.insert_game(name, price, rate)
return jsonify(result)
@app.route("/game", methods=["PUT"])
def update_game():
game_details = request.get_json()
id = game_details["id"]
name = game_details["name"]
price = game_details["price"]
rate = game_details["rate"]
result = game_controller.update_game(id, name, price, rate)
return jsonify(result)
@app.route("/game/<id>", methods=["DELETE"])
def delete_game(id):
result = game_controller.delete_game(id)
return jsonify(result)
@app.route("/game/<id>", methods=["GET"])
def get_game_by_id(id):
game = game_controller.get_by_id(id)
return jsonify(game)</code></pre><p>每条路由都对应了我们之前创建的游戏控制器功能,从而与 SQLite3 数据库进行交互。</p><ul><li>在更新和插入游戏时,我们使用 <code>get_json</code> 阅读了请求的 JSON ,然后访问字典:</li></ul><pre><code class="python"> game_details = request.get_json()</code></pre><ul><li>在删除或通过 ID 获取的情况下,我们从路由中读取 id 变量作为 <variable> 并在方法中接收它:</li></ul><pre><code class="python">game = game_controller.get_by_id(id)</code></pre><ul><li>此 Python API 通过 JSON 进行通信,因此所有响应都是根据 jsonify 函数返回的内容进行的:</li></ul><pre><code class="python">return jsonify(result)</code></pre><ul><li>最后,我们创建 Flask 应用程序来启动服务器并监听请求:</li></ul><pre><code class="python">if __name__ == "__main__":
create_tables()
"""
Here you can change debug and port
Remember that, in order to make this API functional, you must set debug in False
"""
app.run(host='0.0.0.0', port=8000, debug=False)</code></pre><p>至此,我们完整的 <code>main.py</code> 函数如下:</p><pre><code class="python">from flask import Flask, jsonify, request, json
import game_controller
from db import create_tables
app = Flask(__name__)
@app.route('/games', methods=["GET"])
def get_games():
games = game_controller.get_games()
return jsonify(games)
@app.route("/game", methods=["POST"])
def insert_game():
game_details = json.loads(request.get_data())
name = game_details["name"]
price = game_details["price"]
rate = game_details["rate"]
result = game_controller.insert_game(name, price, rate)
return jsonify(result)
@app.route("/game", methods=["PUT"])
def update_game():
game_details = request.get_json()
id = game_details["id"]
name = game_details["name"]
price = game_details["price"]
rate = game_details["rate"]
result = game_controller.update_game(id, name, price, rate)
return jsonify(result)
@app.route("/game/<id>", methods=["DELETE"])
def delete_game(id):
result = game_controller.delete_game(id)
return jsonify(result)
@app.route("/game/<id>", methods=["GET"])
def get_game_by_id(id):
game = game_controller.get_by_id(id)
return jsonify(game)
"""
Enable CORS. Disable it if you don't need CORS
"""
@app.after_request
def after_request(response):
response.headers["Access-Control-Allow-Origin"] = "*" # <- You can change "*" for a domain for example "http://localhost"
response.headers["Access-Control-Allow-Credentials"] = "true"
response.headers["Access-Control-Allow-Methods"] = "POST, GET, OPTIONS, PUT, DELETE"
response.headers["Access-Control-Allow-Headers"] = "Accept, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization"
return response
if __name__ == "__main__":
create_tables()
"""
Here you can change debug and port
Remember that, in order to make this API functional, you must set debug in False
"""
app.run(host='0.0.0.0', port=8000, debug=False)</code></pre><p>值得注意的是,这里我们添加 CORS,如果您要从与 API 本身不同的域使用此 API,则需要启用 CORS。只需将以下代码片段添加到 API(在存储库中,您会发现已添加的代码,您可以根据需要将其删除):</p><pre><code class="python">"""
Enable CORS. Disable it if you don't need CORS
"""
@app.after_request
def after_request(response):
response.headers["Access-Control-Allow-Origin"] = "*" # <- You can change "*" for a domain for example "http://localhost"
response.headers["Access-Control-Allow-Credentials"] = "true"
response.headers["Access-Control-Allow-Methods"] = "POST, GET, OPTIONS, PUT, DELETE"
response.headers["Access-Control-Allow-Headers"] = "Accept, Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization"
return response</code></pre><h2>启动程序并测试</h2><p>使用 <code>python3 main.py</code>,运行我们的服务器和 API :</p><pre><code class="python"> * Serving Flask app 'main' (lazy loading)
* Environment: production
WARNING: This is a development server. Do not use it in a production deployment.
Use a production WSGI server instead.
* Debug mode: off
* Running on all addresses (0.0.0.0)
WARNING: This is a development server. Do not use it in a production deployment.
* Running on http://127.0.0.1:8000
* Running on http://10.26.4.188:8000 (Press CTRL+C to quit)</code></pre><p>为了方便本文创建的所有 API 的测试,使用到 <a href="https://link.segmentfault.com/?enc=bZqjjPHaSc%2Fm700BsSTktA%3D%3D.rOwKkseT4Z2LweJ%2B3MWVPdkOJqWqLKYVQDX0hCkSLJI%3D" rel="nofollow">Apifox</a> 作为接口测试工具:</p><p><img src="/img/remote/1460000042057695" alt="img" title="img"></p><h3>GET 获取所有游戏测试</h3><p>访问 <code>http://127.0.0.1:8000/games</code>。返回 200 OK,但是目前数据库中没有游戏,所以返回空 JSON 字符串:</p><p><img src="/img/remote/1460000042057696" alt="img" title="img"></p><h3>POST 新增一个游戏测试</h3><p>因为游戏 id 是自增的,所以只需要往外面的服务器传入 name, price 和 rate:</p><p><img src="/img/remote/1460000042057697" alt="img" title="img"></p><p>服务器后台显示如下:</p><pre><code>127.0.0.1 - - [16/Jun/2022 14:51:05] "POST /game HTTP/1.1" 200 -</code></pre><h3>PUT 修改游戏内容测试</h3><p>查看我们当前的所有的游戏内容:</p><p><img src="/img/remote/1460000042057698" alt="img" title="img"></p><p>然后将 LOL 的名字改为 <strong>英雄联盟,</strong>此时我们需要传入四个参数:id,name, price, rate,因为要根据 id 来找到我们想要更改的那个游戏:</p><pre><code>{
"id": 2,
"name": "英雄联盟",
"price": 1,
"rate": 99
}</code></pre><p>测试成功如下:</p><p><img src="/img/remote/1460000042057699" alt="img" title="img"></p><h3>GET 获取单个游戏内容测试</h3><p>为了检查刚刚针对 id 为 2 的游戏是否更名成功,我们可以传入<code>http://127.0.0.1:8000/game/2</code> 路径:</p><p><img src="/img/remote/1460000042057700" alt="img" title="img"></p><p>可以看到,PUT 和当前的 GET 请求都是 OK 的。</p><h3>DELETE 删除游戏测试</h3><p>最后,就来到我们的删除环节,把 id 为 3 的游戏内容删除:</p><p><img src="/img/remote/1460000042057701" alt="img" title="img"></p><p>可以看到删除成功,我们的 API 测试是完全 OK 的</p><p><img src="/img/remote/1460000042057702" alt="img" title="img"></p><h2>总结</h2><p>最后,又到了本文需要做总结的时候了。</p><p>本篇教程利用 Python、SQLite3、Flask 实现了一个简单的<strong>游戏 Rest API</strong> 功能,通过在 Flask 中处理传入的 HTTP 的四大请求:GET、POST、PUT、DELETE,实现了最基本的增删改查功能。</p><p>最后利用 Apifox 作为我们的接口测试工具,完整的体验了一个简易 API 开发的流程。</p><p>如果文章对你有用的话,点个赞再离开吧,下一篇文章再见。</p>
Go 语言入门很简单:net/http 包
https://segmentfault.com/a/1190000041777013
2022-04-29T14:00:34+08:00
2022-04-29T14:00:34+08:00
宇宙之一粟
https://segmentfault.com/u/yuzhoustayhungry
0
<h2>引言</h2><p>之前的<a href="https://link.segmentfault.com/?enc=3jgG8ip02kZywnh1S6L1dg%3D%3D.ApsAglZT9fTP8s29LT7J2mk%2FNhTAxj0jKI3aN7t9iBMAfAdBrP%2BwzdAaZDLEJXAQ" rel="nofollow">文章学</a>过把模板和视图分离,建立一个 Web 服务器来展现 HTML 模板。我们将学习如何使用 Go 的模板包创建动态 HTML 和文本文件。</p><h2>建立 Web 服务器</h2><p>到目前为止,我们一直在向终端输出模板,但是当我们开始深入研究更多 HTML 时,这开始变得不那么有意义了。相反,我们希望可视化在 Web 浏览器中生成的 HTML。为此,我们首先需要设置一个 Web 服务器来呈现我们的 HTML 模板。</p><pre><code class="go">package main
import (
"html/template"
"net/http"
)
var testTemplate *template.Template
type ViewData struct {
Name string
}
func main() {
var err error
testTemplate, err = template.ParseFiles("hello.gohtml")
if err != nil {
panic(err)
}
http.HandleFunc("/", handler)
http.ListenAndServe(":8000", nil)
}
func handler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html")
vd := ViewData{"Kyrie Jobs"}
err := testTemplate.Execute(w, vd)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}</code></pre><p>创建一个名为 <code>hello.gohtml</code> 的文件并将以下内容添加到其中:</p><pre><code><h1>Hello, {{.Name}}!</h1></code></pre><p>现在通过在终端中输入 <code>go run main.go</code> 来启动服务器。该程序应保持运行并在端口 8000 上侦听 Web 请求,因此您可以在 <code>localhost:8000</code> 查看呈现的 HTML。</p><p><img src="/img/remote/1460000041777015" alt="" title=""></p><h2>if...else 块</h2><p>我们当前的模板很无聊,因为它只打印出一个人的名字。但是如果没有提供名字会发生什么?</p><p>让我们试试看。打开你的 <code>main.go</code> 文件并删除你的 <code>handler()</code> 函数中创建 <code>ViewData</code> 实例的代码,而是向 <code>testTemplate.Execute</code> 方法提供 nil 。完成后,您的 <code>handler()</code> 函数应如下所示:</p><pre><code class="go">func handler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html")
err := testTemplate.Execute(w, nil)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}</code></pre><p>现在重新启动您的服务器(或让新服务器重新启动)并在浏览器中访问该页面 - <code>localhost:8000</code>。您应该看到一个看起来像这样的页面。</p><p><img src="/img/remote/1460000041777016" alt="" title=""></p><p>当我们不提供名称时,模板将使用空字符串代替值呈现。相反,我们希望我们的页面显示一个更通用的字符串,例如“Hello, there!”。让我们继续更新模板以使用我们的第一个操作,即 <code>if/else</code> 块。像这样更新 <code>hello.gohtml</code>:</p><pre><code><h1>Hello, {{if .Name}} {{.Name}} {{else}} there {{end}}!</h1></code></pre><p>如果您在浏览器中查看该页面,您应该会看到这更新了您的模板以显示“Hello, there !”就像我们想要的那样,但不幸的是,这在“there”这个词和感叹号之间增加了一个额外的空格。大多数时候这并不重要,但是在处理文本时,这有时会很烦人。在下一节中,我们将看看两个选项来稍微清理一下。</p><p>为了摆脱额外的空白,我们有几个选择:</p><ol><li>从我们的模板中删除它们。</li><li>使用减号 (<code>-</code>) 告诉模板包修剪多余的空白。</li></ol><p>第一个选项非常简单。我们只需将 <code>hello.gohtml</code> 文件更新为并删除多余的空格。</p><pre><code class="html"><h1>Hello, {{if .Name}}{{.Name}}{{else}}there{{end}}!</h1></code></pre><p>在这个例子中,这很好用,因为它是一段非常短的文本,但是想象一下我们正在生成 python 代码,其中间距很重要 - 这很快就会变得非常烦人。幸运的是,模板包还提供了一种使用减号来修剪不需要的空白的方法。</p><pre><code class="html"><h1>
Hello,
{{if .Name}}
{{.Name}}
{{- else}}
there
{{- end}}!
</h1></code></pre><p>在此代码片段中,我们通过将减号字符放在 else 关键字的前面来告诉模板包,我们不希望 Name 变量及其后面的任何内容之间的所有空格,并且我们也对 end 执行相同操作倒数第二行的关键字。重新加载您的页面,您应该会看到该空格不再存在。</p><p>对于本教程的其余部分,我将选择使用此处的第一个示例作为我的 <code>hello.html</code> 文件。</p><h2>范围块</h2><p>现在让我们假设您想在您的网站上显示所有小部件以及它们的价格。这是动态 Web 应用程序的任务类型,因为没有人愿意为您销售的每件商品手动创建 HTML 并维护它。相反,我们希望对每个项目使用相同的 HTML。在 Go 中,您可以使用模板内的范围块来实现此目的。</p><pre><code class="html">{{range .Widgets}}
<div class="widget">
<h3 class="name">{{.Name}}</h3>
<span class="price">${{.Price}}</span>
</div>
{{end}}</code></pre><p>如果你重启你的服务器(或者让新的)并在 <code>localhost:8000</code> 重新加载页面,你现在应该会看到一个 HTML 页面,其中显示了三个小部件,每个小部件都有一个标题和一个价格。如果我们在数组中添加更多小部件,我们会在这里看到更多,如果我们将其保留为空数组,我们将不会在此处看到任何小部件。</p><p>与 <code>range</code> 操作混淆的最常见来源是我们正在访问小部件的各个属性,而无需在 <code>.Widgets</code> 值内使用索引或任何其他访问器。这是因为范围操作会将集合中每个对象的值设置为范围块内的点 (<code>.</code>)。例如,如果您要在范围块内渲染 <code>{{.}}</code>,您将看到与在 Widget 对象上使用 <code>fmt.Println()</code> 相同的输出。</p><h2>嵌套模板</h2><p>随着您的模板开始增长,您会很快发现您需要在不同的地方重用组件。这就是嵌套模板来拯救这一天的地方。使用 Go 的模板包,您可以声明多个唯一命名的模板,然后当您需要在代码中使用另一个模板时,您只需使用 template 关键字引用它。例如,假设您想为您的网站声明一个页脚,您可以将其包含在多个页面和多个布局中。将以下页脚模板添加到 <code>hello.html</code> 文件中。你把它放在哪里并不重要,但我更喜欢把它放在文件的顶部。</p><pre><code class="html">{{define "footer"}}
<footer>
<p>
Copyright 2016 Calhoun.io
</p>
<p>
Contact information: <a href="mailto:jon@calhoun.io">jon@calhoun.io</a>.
</p>
</footer>
{{end}}</code></pre><p>然后在小部件的范围块之后插入以下行。</p><pre><code>{{template "footer"}}</code></pre><p>您的 <code>hello.gohtml</code> 文件应如下所示:</p><pre><code class="html">{{define "footer"}}
<footer>
<p>
Copyright 2016 Calhoun.io
</p>
<p>
Contact information: <a href="mailto:jon@calhoun.io">jon@calhoun.io</a>.
</p>
</footer>
{{end}}
{{range .Widgets}}
<div class="widget">
<h3 class="name">{{.Name}}</h3>
<span class="price">${{.Price}}</span>
</div>
{{end}}
{{template "footer"}}</code></pre><p>现在,如果您查看 <code>localhost:8000</code>,您将看到该页面正在使用您定义的页脚模板。当您定义一个模板时,您可以在任何其他模板中使用它,甚至可以多次使用它。尝试包含页脚模板两次以了解我的意思。</p><h2>模板变量</h2><p>我们的上一个示例很棒,但是当您需要在嵌套模板中包含一些数据时会发生什么?幸运的是,模板操作允许您传入第二个参数,该参数将分配给模板内的点 (<code>.</code>) 参数。例如,假设我们想为小部件的名称标题部分编写模板,我们可以使用以下代码来实现。</p><pre><code class="html">{{define "widget-header"}}
<h3 class="name">{{.}}</h3>
{{end}}
{{range .Widgets}}
<div class="widget">
{{template "widget-header" .Name}}
<span class="price">${{.Price}}</span>
</div>
{{end}}</code></pre><p>在这种情况下,<code>.Name</code> 属性被分配给 widget-header 模板内的点 (<code>.</code>) 属性。</p><p>带有模板变量的嵌套模板甚至允许您深入多层,这意味着可以从模板内部调用模板。</p><pre><code class="html">{{define "widget"}}
<div class="widget">
{{template "widget-header" .Name}}
<span class="price">${{.Price}}</span>
</div>
{{end}}
{{define "widget-header"}}
<h3 class="name">{{.}}</h3>
{{end}}
{{range .Widgets}}
{{template "widget" .}}
{{end}}</code></pre><p>这段代码的最终结果是相同的,但是现在我们有了一个小部件模板,我们可以轻松地在 Web 应用程序的其他页面上重用它,而无需重写代码。</p><h2>接下来</h2><p>凭借您新学到的模板技能,您应该可以创建可重用的动态模板。在下一篇文章中,我们将介绍如何使用内置的模板函数,如 <code>and</code>、<code>eq</code>、<code>index</code>,然后我们将看看如何添加我们自己的自定义函数。我原本打算包括那些听到的,但这篇文章有很多要介绍的行动,我不想卖空任何一个。</p><p>在关于函数的帖子之后,我们将介绍如何使用模板来创建 Web 应用程序的视图层。这将包括创建共享布局、定义可以被覆盖的默认模板,以及在不同页面中包含相同的模板,而无需将所有代码放入单个文件中。</p><p>如果您感到雄心勃勃或好奇,您还可以查看 <code>text/template</code> 和 <code>html/template</code> 的模板文档。继续自己探索其他一些知识~</p>
一文了解 Python 中的生成器
https://segmentfault.com/a/1190000041642166
2022-04-01T12:07:05+08:00
2022-04-01T12:07:05+08:00
宇宙之一粟
https://segmentfault.com/u/yuzhoustayhungry
0
<h2>前言</h2><p>生成器很容易实现,但却不容易理解。生成器也可用于创建迭代器,但生成器可以用于一次返回一个可迭代的集合中一个元素。现在来看一个例子:</p><pre><code class="python">def yrange(n):
i = 0
while i < n:
yield i
i += 1</code></pre><p>每次执行 yield 语句时,函数都会生成一个新值。</p><p><img src="/img/bVcYTbU" alt="" title=""></p><p>“生成器”这个词被混淆地用来表示生成的函数和它生成的内容。</p><p>当调用生成器函数时,它甚至没有开始执行该函数就返回一个生成器对象。 当第一次调用 <code>next()</code> 方法时,函数开始执行直到它到达 yield 语句。 产生的值由下一次调用返回。</p><p>以下示例演示了 yield 和对生成器对象上的 <strong>next</strong> 方法的调用之间的相互作用。</p><pre><code class="python">>>> def foo():
... print("begin")
... for i in range(3):
... print("before yield", i)
... yield i
... print("after yield", i)
... print("end")
...
>>> f = foo()
>>> next(f)
begin
before yield 0
0
>>> next(f)
after yield 0
before yield 1
1
>>> next(f)
after yield 1
before yield 2
2
>>> next(f)
after yield 2
end
Traceback (most recent call last):
File "<pyshell#13>", line 1, in <module>
next(f)
StopIteration
>>></code></pre><h2>生成器也是迭代器</h2><p>生成器也是迭代器,支持使用 for 循环。当使用 <code>for</code> 语句开始对一组项目进行迭代时,即运行生成器。一旦生成器的函数代码到达 <code>yield</code> 语句,生成器就会将其执行交还给 <code>for</code> 循环,从集合中返回一个新值。生成器函数可以根据需要生成任意数量的值(可能是无限的),依次生成每个值。</p><pre><code class="python">f_2 = foo()
for i in f_2: print(i)
begin
before yield 0
0
after yield 0
end
before yield 1
1
after yield 1
end
before yield 2
2
after yield 2
end</code></pre><p><img src="/img/bVcYTbV" alt="" title=""></p><p>当一个函数包含 <code>yield</code> 时,Python 会自动实现一个迭代器,为我们应用所有需要的方法,比如 <code>__iter__()</code> 和 <code>__next__()</code>,所以生成器也能和迭代器有相同的功能,如下所示:</p><pre><code class="python">def yrange():
i = 1
while True:
yield i
i = i + 1
def squares():
for i in yrange():
yield i * i
def take(n, seq):
seq = iter(seq)
result = []
try:
for i in range(n):
result.append(next(seq))
except StopIteration:
pass
return result
print(take(5, squares()))
# [1, 4, 9, 16, 25]</code></pre><p>接下来看一下如何使用生成器计算斐波那契数列:</p><pre><code class="python">def fib(n):
if n <= 1:
return 1
a, b = 0, 1
for _ in range(n):
a, b = b, a + b
yield a
for i in fib(10):
print(i, end=' ')
# Result:1 1 2 3 5 8 13 21 34 55 </code></pre><h2>生成器推导式</h2><p>生成器表达式是列表推导式的生成器版本。它们看起来像列表推导式,但返回的是一个生成器,而不是一个列表。生成器推导式的本质:</p><ul><li>使用 yield 会产生一个生成器对象</li><li>用 return 将返回当前的第一个值。</li></ul><pre><code class="python">generator_expressions = (x for x in range(10))
generator_expressions
<generator object <genexpr> at 0x0000023F8BC51AF0>
sum(generator_expressions)
45</code></pre><h2>无限生成器</h2><p>生成器的另一个常见场景是无限序列生成。在 Python 中,当您使用有限序列时,您可以简单地调用 <code>range()</code> 并在列表中对其进行计数,例如:</p><pre><code class="python">a = range(5)
print(list(a))
[0, 1, 2, 3, 4]</code></pre><p>也可以这样做,使用如下生成器生成无限序列:</p><pre><code class="python">def infinite_sequence():
num = 0
while True:
yield num
num += 1</code></pre><p>运行此代码时,可以看到其运行非常快,可以通过 <code>CTRL+C</code> 来使得程序结束,如下:</p><p><img src="/img/bVcYTbW" alt="" title=""></p><h2>生成器实际用法</h2><p><strong>1. 读取文件行</strong></p><p>生成器的一个常见用法是处理大型文件或数据流,例如 CSV 文件。假设我们需要计算文本文件中有多少行,我们的代码可能如下所示:</p><pre><code class="python">def csv_reader(file_name):
file = open(file_name)
result = file.read().split("\n")
return result
csv_gen = csv_reader("some_file.csv")
row_count = 0
for row in csv_gen:
row_count += 1
print(f"Row count is {row_count}")</code></pre><p>我们的 <code>csv_reader</code> 函数将简单地将文件打开到内存中并读取所有行,然后它将行拆分并与文件数据形成一个数组。如果文件包含几千行,可能就会导致速度变慢,设置是内存被占满。</p><p>这里就可以通过生成器重构的 <code>csv_reader</code> 函数。</p><pre><code class="python">def csv_reader(file_name):
for row in open(file_name, "r"):
yield row</code></pre><ol start="2"><li><strong>读取文件内容</strong></li></ol><pre><code class="python">def readfiles(filenames):
for f in filenames:
for line in open(f):
yield line
def grep(pattern, lines):
return (line for line in lines if pattern in line)
def printlines(lines):
for line in lines:
print(line, end="")
def main(pattern, filenames):
lines = readfiles(filenames)
lines = grep(pattern, lines)
printlines(lines)</code></pre><h2>高级生成器用法</h2><p>到目前为止,我们已经介绍了生成器最常见的用途和构造,但还有更多内容需要介绍。随着时间的推移,Python 为生成器添加了一些额外的方法:</p><ul><li><code>send()</code> 函数</li><li><code>throw()</code> 函数</li><li><code>close()</code> 函数</li></ul><p>接下来,我们来看一下如何使用这三个函数。</p><ol><li>首先,新建一个生成器将生成素数,其实现如下:</li></ol><pre><code class="python">def isPrime(n):
if n < 2 or n % 1 > 0:
return False
elif n == 2 or n == 3:
return True
for x in range(2, int(n**0.5) + 1):
if n % x == 0:
return False
return True
def getPrimes():
value = 0
while True:
if isPrime(value):
i = yield value
if i is not None:
value = i
value += 1</code></pre><ol start="2"><li>然后我们调用 <code>send()</code> 函数,这个函数会向生成器 <code>prime_gen</code> 传入一个值,然后从这个值开始计算下一个素数的值:</li></ol><pre><code class="python">prime_gen = getPrimes()
print(next(prime_gen))
print(prime_gen.send(1000))
print(next(prime_gen))</code></pre><p>可以看到如下结果:</p><p><img src="/img/bVcYTbX" alt="" title=""></p><ol start="2"><li><code>throw()</code> 允许您使用生成器抛出异常。例如,这对于以某个值结束迭代很有用。比如我们想得到小于 20 的素数就可以使用如下方法:</li></ol><pre><code class="python">prime_gen = getPrimes()
for x in prime_gen:
if x > 20:
prime_gen.throw(ValueError, "I think it was enough!")
print(x)</code></pre><p>运行该代码,得到结果如下:</p><p><img src="/img/bVcYTbY" alt="" title=""></p><ol start="3"><li>在前面的示例中,我们通过引发异常来停止迭代,但这并不是用户想看到的,谁想看到报错呢。因此,结束迭代的更好方法是使用 <code>close()</code>:</li></ol><pre><code class="python">prime_gen = getPrimes()
for x in prime_gen:
if x > 20:
prime_gen.close()
print(x)</code></pre><p>运行结果如下图:</p><p><img src="/img/bVcYTbZ" alt="" title=""></p><p>可以看到,生成器在运行到停止了,没有引发任何异常。</p><h2>总结</h2><p>生成器简化了迭代器的创建。 <strong>生成器是产生一系列结果而不是单个值的函数</strong>。</p><p>生成器可以用于优化 Python 应用程序的性能,尤其是在使用大型数据集或文件时的场景中。</p><p>生成器还通过避免复杂的迭代器实现或通过其他方式处理数据来提供清晰的代码。</p><p>参考链接:</p><ul><li><a href="https://link.segmentfault.com/?enc=gcitwm1OSmsgLHnZfw8E%2Bw%3D%3D.FEBbVWSGInb6RvRff9n%2BdXrQtuT%2B%2FUbHGvWuL%2F8e4Gf85f5BBbnRp67UGB5UvjZDF%2Bwprgyuk446vuFhubEn2219phKWPPMHQWcVkQSlTtE%3D" rel="nofollow">How to Use Generator and yield in Python</a></li><li><a href="https://link.segmentfault.com/?enc=omRJC0mXfbfcN7GaLEGLjA%3D%3D.%2F%2FzZdADwMf%2BvadL8Rujjir%2Fe4J%2FuRyhpwoG5RUTZUT70v3RiuEQUJSdsLqvhL6VGRk0knYRqDIzXh6EWGYNaaA%3D%3D" rel="nofollow">https://realpython.com/introd...</a></li><li><a href="https://link.segmentfault.com/?enc=MC6R4qDTQciDYIzeMcDq1w%3D%3D.4v5JOmcsZhItbho48EiDcxezHuJ2p%2BM%2FRZV%2BDqcKmOAXdqn0eciI1eQyKItaJ0WZ5t5QThPEfdU1LB2krxbpgA%3D%3D" rel="nofollow">https://anandology.com/python...</a></li></ul>
2022招聘季|如何才能让校招项目准备的高大上一点
https://segmentfault.com/a/1190000041579365
2022-03-21T10:18:20+08:00
2022-03-21T10:18:20+08:00
宇宙之一粟
https://segmentfault.com/u/yuzhoustayhungry
2
<h2>实习很重要,项目的第一来源</h2><p>作为老学长,我的建议是 3、4 月的时候最好是要有知名公司的实习,这样在正式秋招为自己增加很多筹码。可能其他公司一看你的实习经历,可能就愿意给你机会让你进面试了,甚至直接免笔试,毕竟大厂 buffer 加持,人家就很想了解你在大公司的实习情况。</p><p>如果你也像我一样小公司实习,甚至说你没找实习。那也不意味着你没有了机会,我认为暑假这段时间一定要好好把握,做两个不错的项目,然后吃透一个!这里强调一下是吃透 1 个项目,为什么呢?</p><p>下文再解答...</p><h2>无论我们做什么项目,在面试官眼里都可能是 Low 的?</h2><p>先说一个可能的残酷现实:无论我们做什么项目,在面试官眼里都可能是 Low 的。</p><p>你可能会反驳我,如果想。我承认你对...</p><p>比如秒杀系统,这几年都强调高并发的经验,所以大家都做,那些相关技术栈都快成八股文了,网上相关文章也一堆,你会觉得高大上,但是面试官如果问:</p><ul><li>你项目上线了吗?真实情况有人用吗?没人用你做成这样干嘛?</li><li>为什么选 Redis 不选 Memcache?</li><li>为什么消息队列选 RabbitMQ,不是其他?</li><li>等等一堆你可能想不到的问题</li></ul><p>其他项目就更别说了:博客系统、在线聊天工具、XX 爬虫系统、XX 管理系统...</p><p>有些读者可能会问了:我作为一个学生,那我要是能做出微信、头条、淘宝这样的项目,我还用得着来这公司上班?下一个扎克伯格怕不就是我了?</p><p>问的好,其实很多企业级项目归根结底,本质也是这些项目。那为什么我们做不太行呢?</p><p>总而言之,我说的 "Low" 不是说项目真的不好,而是想表达项目难做。什么叫难做?</p><h2>做项目不是跟着开源代码/视频敲一遍</h2><p>作为学生,本来大家就不可能做的是企业级项目,所以没办法。大家都会选择一些常见的容易上手的项目来做,这些项目不是不能做,但绝对不会是你跟着开源代码或者看视频跟着敲了一遍,就说你做了这个项目。</p><p>好好思考一下。你是真的懂背后的原理吗?比如:</p><ul><li>你的项目架构是啥,了解吗?(我记得面试的时候好多面试官喜欢问,还有线下让我画架构图)</li><li>为什么要做这个项目?问对业务逻辑的认识?</li><li>项目用的什么数据库?问 ORM、MyBatis,或者继续深挖数据库的知识点</li><li>上 Spring 的,问 AOP、IOC;问对 servlet 的理解?</li><li>上前后端分离的,问如何解决跨域请求?问 session?</li><li>上高并发的,问如何优化使得并发量有提升?</li><li>上微服务的,问你微服务怎样设计?</li><li>项目出现什么,就看你会被问什么?</li></ul><p>有时候最怕顾此失彼,为了追最新框架、最新技术,往往忘记了对核心的技术原理的理解。</p><p>所以项目难做就在于此,你用旧技术他会觉得 low,你用新技术他会觉得你只是单纯的套用,又不是真正的懂。</p><p>所以针对这样的情况,我的看法:虽然事实是我的项目其实做的是有点 low,但是小项目也要展现出它的高大上。</p><p>那么,如何展现出高大上?</p><h2>让项目的来的高大上一些</h2><p>项目的来源无非几个:学校;企业(有实习的同学);网上(开源 or 付费)</p><h3>学校或企业做的项目</h3><p>珍惜每一次锻炼的机会,用心做好每一次的项目:</p><ul><li>课程设计/毕业设计中:虽然我是小 demo ,但是我拿到优秀的项目哦(展现拿到优秀的点)</li><li>比赛项目:ACM、互联网+、挑战杯、蓝桥杯(我这个项目获奖了,专家评审认可的;没获奖也可以高大上,比没有好对吧)</li><li>实习项目:如果这是 BAT 的实习项目呢?(大厂实习虽然可能只是简单 CRUD,但至少来源就高大上了。)</li></ul><blockquote>在 读本科那会,有门课需要做 Java 课程设计,我们大家都做的管理系统,全班只有一个同学做了坦克大战,然后还可以让老师体验玩一下。</blockquote><p>试想一下,如果是你是那门课的老师,你喜欢哪个项目?</p><h3>网上学来的、或开源项目达到高大上的效果</h3><p>如果你觉得学校的课程设计不够好,免费的项目 GitHub 和 Gitee 上的优秀开源项目很多;付费的牛客和慕课上的针对性项目也挺高大上的。推荐大家学习:</p><p>你学了后如何体现你的项目高大上呢?</p><ul><li>比如你做的项目开源,得到了2k 以上 star ,高大上吧</li><li>你对某个大牛项目有自己的贡献,发现了啥 bug 也挺高大上</li><li>你的项目跟着 mou 前阿里/字节架构师/工程师学的(这个 title 的付费项目一大堆,跟着学比自己乱写的的确要高大上吧)</li></ul><p>或者大家都是本地项目,你的项目部署上线了,然后面试官可以直接访问,看到你做的项目;比如你做了小程序的项目,打开微信就能体验,哪个项目高大上?</p><h3>对项目的思考要多一点,也很高大上</h3><p>优秀又好做的项目好多人推荐,那么就有可能大家都做同一个项目(比如秒杀),但是你对项目有自己的思考,就很不一样的。</p><p>如何展示给面试官你的与众不同?</p><ul><li>大家都用 Java 做高并发,你来一个 Go 语言版本的。大家都上框架做 Web 开发,你自己写了一个 MVC 框架, RPC 框架等等。</li><li>之前还看到一本书中的一个观点,做一些有工具也很有亮点。比如我们每天在 Linux 中都在用的 <code>cat</code> 命令,你有没有研究过这个命令的实现算法,你来做会怎么做?</li><li>代码重构,项目优化也是很多人不会想的点,极少人做那就是高大上</li><li>其他可以思考的点,大家多去网上找找。</li></ul><p>最后,做“高大上”只是我的一个技巧而已,而且只是提供一个思路,希望大家都要好好准备一个项目,让自己吃透,这一点就足够高大上了。</p><h2>为什么我建议你做两个项目比较好</h2><p>以我的经历来说,之前,简历上只做了 Python 的项目:一个在线教育平台和一个上线的个人博客。</p><p>所以只能投 Python 的岗位,根本不敢去找其他如 Java 的工作,而且投了也没有什么回馈。为什么呢?</p><p>首先,Python 不是一门企业级应用首选的语言,据说是会有一些坑和性能瓶颈,导致用 Python 做后端开发的公司较少。以前用 Python 的公司也都转 Go 语言了,貌似知乎、字节都是如此。</p><p>其次,Python 更多是作为机器学习和数据分析的首先的语言,读研期间会使用到的框架基本都和 Python 联系密切,所以找算法和数据岗可能更好一点,所以我拿着 Python 在后端开发方向( Java 和 Go )的简历中没有一点优势可言。</p><p>最后,Python 作为一门简单的解释性语言,入门时大多人都会选 Python。随着学习的深入,个人体会却是 Python 易学难精,但作为第二语言真的不错。</p><p>所以在 7 月和 8 月份的时候,每天就是恶补 Java 项目。因为感觉再做秒杀系统,我玩不出新花样了。我选择了校园微商城项目,而且小程序在那会也不算过时,毕竟时至今日,小程序开发还是很有市场的。</p><p>人真的是被逼出来的,如果不逼自己一把,就永远不知道自己什么时候可以做好。</p><h2>后记:Java 和 Python 双项目真的让我受益颇多</h2><p>正因为有 Java + Python,我可以投互联网公司,可以投银行,可以投国企。</p><p>我还记得有面试官问我对这两门语言的看法?</p><p>(我在想这不是正中下怀吗?就等你问了)大概是这样答得:</p><ul><li>因为这两门语言都挺火的,<a href="https://link.segmentfault.com/?enc=HX1B5uPcoVb7V%2FpRqU0Myw%3D%3D.1GeuL4LeEgT%2FOh8rnzHR%2FBUcoRdl4xMr559IFwmRUQk6tDNsr56ojvZ%2FUo31GtXL" rel="nofollow">Tiobe</a> 排行都是前几名,所以我想都学习一下。</li><li>再者,就像好的工程师知道用更适合的工具拧相应的螺丝。开发也是如此,编程语言也是工具,有优有劣,用不同的语言做更适合它的项目罢了。</li><li>我本科学的就是 Java 做 Web 开发,Python 是在读研期间学会的。因为机器学习的课程使用,而且好用的机器学习库都离不开 Python,这时候更适合我的工具就是 Python 了。后面在这个基础上接触了 Python 的 Web 开发框架 Django,由此做了我简历上的这个项目。</li></ul><p>大概就是这样,吹水还是要会的。</p><p>最后我在秋招中,也不限定只投某个具体的编程语言。凭着 Java + Python 的双项目拿到了 Java 开发工程师、Python 开发、C++ 游戏开发、Go 后端开发工程师的 offer...</p><p>C++ 开发那个我是说自己本科学过,但是不咋会;Golang 提的也是我正在学;离谱的是也进入了 Erlang 开发的面试,面试官说如果拿到 offer 可以培养的。</p><p>所以,这也是为什么校招中很多公司要求至少一门编程语言但不限于 C/C++、Java、Python、C#、javascript... 的原因吧。</p><h2>推荐项目</h2><p>关于如何做项目的几个点终于说完了,也不知道对大家有没有一点帮助。最后推荐几个我觉得还不错的开源项目吧:</p><h3>开发类</h3><p>Java 的项目推荐:</p><ol><li><a href="https://link.segmentfault.com/?enc=gEjUPSpsaXHnjKoramhQ3A%3D%3D.4L8HziNsSNbixymF0KFUzOQ6k386OAfZl64Vh%2FeY0MJXvnZwaz9Q99VAGc0Kl7TT" rel="nofollow">秒杀系统设计与实现</a></li><li><a href="https://link.segmentfault.com/?enc=xRX1tlLSuss53Z83OQ19Qw%3D%3D.2r4IsGWRI8qWEijcp8RBayo89iBWCsyks3GU3DhY0Tuyjqq7cdniKFx%2B9fRYxnAK" rel="nofollow">若依</a></li><li><a href="https://link.segmentfault.com/?enc=qQIw0TxnM%2BTImzBrSwTrIA%3D%3D.3hhHpnY3WX1j9q5e%2BpeD4cjLGYV66fNzI1uyUDQ4vtUFAEshgz8AH7Q%2B3UPwul8C" rel="nofollow">mall 项目(40k+star)</a></li></ol><p>Go 的项目推荐:</p><ol start="4"><li><a href="https://link.segmentfault.com/?enc=k18s3RQfmwhRMcFNHbuNiQ%3D%3D.b96ZWuXg%2BgKwnzbvKgvDNCmro0hkq%2BBeZ0a4iTRWEXWhXy3tx63Lq14w1RBX9yQX" rel="nofollow">基于gin+vue搭建的后台管理系统框架</a></li><li><a href="https://link.segmentfault.com/?enc=zc%2FRlqUF5xB9SkYxk%2Bo1Kg%3D%3D.7%2FAbZeFDqJvd6m%2Brvd58V3svcx4A96Z9UDfnXvT4C0QtXc1FIs4zAcJZzcVFqC7U" rel="nofollow">7 天用 Go 从零实现系列</a></li></ol><h3>知识复习类</h3><ol start="6"><li><a href="https://link.segmentfault.com/?enc=Qw%2B8Bg00aURLRKoZdCouXQ%3D%3D.bs0HfoLjGN0keZ79A2702lManPRMpF5lH1xVvaQ1jsX9rj2QMpdjS2f9X78rHA6bKA4Url7kmAX4KsPDrfylOw%3D%3D" rel="nofollow">后端架构师技术图谱</a></li><li><a href="https://link.segmentfault.com/?enc=DoNAVfbvIQsoJ%2Bup0bnTzw%3D%3D.JOdM1vnoMdGhcr8pVvCU85%2Fyo1%2BJPyDHuzdvR%2FJjFCXyJ9XxJ8t8SlZxQLWAj2go" rel="nofollow">CS-Notes</a></li><li><a href="https://link.segmentfault.com/?enc=SlVgnDbKa78B%2B4ib%2FGrWhA%3D%3D.KxqqhuH7KKqpH6j9kPQGqabNLixxAmmRor117IaadKuCjXfhtTOmlxmX194zIx9g" rel="nofollow">JavaGuide</a></li><li><a href="https://link.segmentfault.com/?enc=MWyWZA2vNrToGzgcJ5H%2BsQ%3D%3D.umIPCaIrfwlgE6q8wTQebJ%2F1v31T4wLI0Iq9e3yRyFdOZ7Q%2FMC9ICQiFIOKuueqp" rel="nofollow">JavaFamily</a></li><li><a href="https://link.segmentfault.com/?enc=gAylOmG4B6WI4PW%2BaJT6Sw%3D%3D.VTn%2FTPe34%2BPzCBzvOp792F9e%2FV6TBgc0OEf1NUHARf4%2FhII3424EUSHGvSIbcquVGryM2xetrfCVKQXE%2FfDJjA%3D%3D" rel="nofollow">Awesome-interview</a></li></ol><p>如果你还是不知道该做什么项目,请参考如下项目,去找一个你喜欢和你语言相关的项目吧:</p><ol start="11"><li><a href="https://link.segmentfault.com/?enc=CJ5RXOLpJusT8AZNPz3yqw%3D%3D.cMXyZQLZDELIZeIPAZ830TkQQF%2Bd5BZyeTXvr%2F2Ycxyd%2FRZYdgGPXePl4fCL%2BIATl55pS2eKACDa1cFiODGkvw%3D%3D" rel="nofollow">GitHub 中文排行榜</a></li><li><a href="https://link.segmentfault.com/?enc=PzISuUIea7KAUEXD91F6RA%3D%3D.k%2Bf1ckDQqSClz7G%2BM0GIgj3KJt2jGxLXihwECSP6xdk%3D" rel="nofollow">Hello Github</a></li></ol><p>下一篇讲讲如何准备算法吧,算法不是我的强项。其实我感觉我对面试的体会还是更多一些,后续也写文章讲讲《如何准备面试》。</p><blockquote>本文参与了 SegmentFault 思否征文<a href="https://segmentfault.com/a/1190000041558580">「如何“反杀”面试官?」</a>,欢迎正在阅读的你也加入。</blockquote>
代码之外:人生最大的幸运就是努力没有白费 -- 我的2021年度总结
https://segmentfault.com/a/1190000041173853
2021-12-24T14:06:05+08:00
2021-12-24T14:06:05+08:00
宇宙之一粟
https://segmentfault.com/u/yuzhoustayhungry
2
<blockquote>本文参与了 <a href="https://segmentfault.com/a/1190000041089764">SegmentFault 思否征文「2021 总结」</a>,欢迎正在阅读的你也加入。</blockquote><h2>代码之外——2021 年度总结</h2><blockquote>题记:看到那么多优秀的前辈们发的总结,本人实在惭愧,既没有高深的技术分享也没有大厂的职业经历, 只能发发代码之外的东西。</blockquote><p>每次一到年尾做总结的环节,说明追赶时间的一年结束了。</p><p>在这科技发达的日子里,找回忆是如此的简单——随手翻翻自己手机里的照片,在哪些特别的日子里都发了什么样的朋友圈,拍的怎样一个个令自己尴尬与搞笑的视频。</p><p>细细回想,往事如在眼前,记忆一幕幕袭来,2021 年我都干了些什么?我这一年的几个关键词可以总结如下:</p><ul><li>工作</li><li>离别</li><li>毕业</li><li>宠物</li><li>生活</li></ul><h2>秋招结束,与导师的聊天</h2><p>2021 年初结束半年秋招岁月之后,在学校忙着写论文、做设计、写软著。因为研究生要毕业了,所以格外珍惜这毕业前夕的岁月。不像往昔的校园生活里不是为了课程、发论文,做课题,就是为了找工作而奔波,只有这一段时光是那校园日子里最悠闲舒适的时光。</p><p>早上早起可以看看闲书,走到路上仿佛校园的花都是向自己自己祝贺而盛开。</p><p>庆幸自己没有辜负这一年为了求职而做的那些努力,得到的这些不足于外人道但却值得自己骄傲的认可。</p><p>有一天,导师来到实验室在我旁边问我:“大半年没看到你,工作找的怎么样了?” </p><p>“还行,去了一家给了 SP 的外企。想去不一样的公司体验体验”</p><p>“不错,那有高的肯定去更高的啦...”</p><p>在这里真的谢谢我导,提供了那么多机会考研锻炼,虽然说读研的成果不算优异,但是做过的事也还算丰富:科创、基金、专利、杂志约稿、做网站、打比赛、写论文、做汇报、帮老师上课、写公众号...</p><p>读研期间,不时有看到一些研究生压力大选择轻生的新闻。</p><p>导师一直跟我们强调的是:“如果你们有什么压力记得跟我聊,我看来<strong>人总不能对自己要求太高</strong>,<strong>科研绝不是唯一的路</strong>。</p><p>读研顶多在这三年让你们明白科研是个怎么个流程,所以我给你们的任务不算繁重,一直想把你们训练成为会自己发现问题解决问题、有一定动手能力的人、会展示自己的人等等。</p><p>如果你觉得你适合科研,你就可以申请读博,我也会全力支持,不要求你一定得读我的博;如果你觉得不适合,就去工作,任何岗位单位我都支持,找工作有难处我也会帮忙推荐。再不济,研究生文凭不要了,你还有本科,你还应该去生活。千万别想不开。”</p><p>我自认为自己不是一个做学术的人才,所以想早早选择找实习、找工作。 我的选择不一定是最优解,但总比没有的好。</p><p>人生总会面临众多选择(考研、读研、工作),去寻找到自己的路吧。选择好了你就只管努力,<strong>没必要在乎别人那条路怎么样,剩下的交给时间</strong>。</p><h2>家人闲坐,灯火可亲</h2><p>然后就是回家过年的那段日子,从小到大都很喜欢热闹,可能是独生子所以常常害怕孤独吧。</p><p>这个年一家人都是开开心心的。</p><p>家人知道我在找工作中有了这么多机会,我即将去的是他们心中的大城市,他们替我高兴。</p><p>我知道原来我一直是家里的榜样,能让家人放心是我最大的自豪,我当时心想一定要更加努力去报答家人。</p><p>最喜欢家里冬日里的柴火—— 既是生活的方式,也是没有暖气的南方为数不多的取暖方式,最关键的是能吃到香喷喷的柴火饭。更代表了这是家人的烟火气与温暖。</p><p><img src="/img/remote/1460000041173855" alt="IMG_3093.JPG" title="IMG_3093.JPG"></p><p>图片来源:家里的灶火</p><h2>公司实习--身份改变,思维转变</h2><p>终于踏上了去实习的路,不需要担心租房、不用纠结在哪落脚,到指定的地点等待 HR 安排带领我们入住公司租的公寓,真正的无忧拎包入住。</p><p>看到住进的两室一厅的房间售价 8万/1平 的时候,当时内心只得感叹:这也是我能住的地方么?这就是让我望而却步的房价吗?</p><blockquote>其实内心一直都是现实的,由奢入俭难,进入的第一天我就在问:我在这里打拼多少年才有机会买这样的一套房子?关键是明年我该租在哪?</blockquote><p>无论如何,总算是有机会慢慢接触和了解这座城市了。</p><p><img src="/img/remote/1460000041173856" alt="IMG_8686.JPG" title="IMG_8686.JPG"></p><p>新人总要先接受公司的培训,熟悉公司的部门与业务。认识了来自不同地方的同事,原来大家都是这么优秀的人。不脱离自己的舒适去,我就永远是笼中之鸟,不知鸿鹄之志。</p><p>实习来了就发现公司的业务真的是复杂的,对于一个刚进入职场的人来说,关于公司的概念简直为 0 。</p><p>经过大概两周的培训就开始写第一个小模块了,陌生的系统、新的语言、新的开发环境,当时总在担心自己不可能能这么快上手吧,写代码的过程中总担心有很多问题。</p><p>还好计算机编程的底层逻辑总是类似,所以误打误撞也算进入了填坑模式。</p><p>当提交完第一个代码后,内心无比不放心。而当测试在跟我说说通过后,我都不敢相信这是真的,我一直都觉得不可能,请他再再检查检查吧。</p><p>毕竟是新人,无比超级担心传到正式系统会各种 bug,害怕挨批,还好这一切并没有发生,实习期间也得到了企业导师的认可。</p><p>后续又是参加了部门的团建、公司的欢迎晚宴(高级西餐厅,好像人均套餐 688),真的是贫穷限制了我吃饭的环境。</p><p>当然最开心的事莫过于,成功领到了第一笔实习工资。</p><h3>实习体会</h3><p>实习过程中最大的思维改变就是:公司是事情来了希望你能<strong>快速完成任务然后再学习</strong>,学校是教你学会了再去找点事情做。</p><p>职场更多是团队模式,不是你埋头写代码就行了。业务怎么来的,需求怎么提的,需求怎么转变为代码、写完代码给谁测试。全是跟人接触,代码往往是最不被看到的那一面。</p><p>还有一个点就是,公司人来人往太常见了。大家一定要习以为常,我在实习过程中看到了招我进来的 3 个 HR 也离职了,部门内部刚认识的同组同事也说马上要走了,但马上又有新人换旧人(工作交接)。</p><p>这不是说公司不好了,感觉更是行业规则吧:不然又怎么普遍都说“金3银4”,大家都懂背后的原因 —— 为了更好的涨薪,为了不一样的环境,为了更舒服的体验。</p><p>渐渐明白了<strong>大家各自奔前程,没有谁会真的一直陪着你</strong>。</p><p>最后,非常建议各位还在学校的师弟师妹们,多多尝试找实习。<strong>人无论如何总是要工作的(收租除外)</strong>,还可以在学校的时候提前收获一份经历。其次,实习还是一种长见识的机会,更是一种无代价的容错。千万别放弃,早做准备,总有一个坑位是等着你的。</p><h2>人终离别,不愧余生</h2><blockquote>对我影响最大的爷爷于今年离开了我</blockquote><p>在我来深圳实习后不久,爷爷就住院了。在我读研的三年里,爷爷一年身体不如一年,几乎每年都会住院。</p><p>实习期间几乎每个周末都会给家里打电话,爸妈去医院看望爷爷我也让他们给我打电话。</p><p>会跟他讲实习的工作,讲深圳的物价,讲公司的各种好玩的事。</p><p>所以这次,我也以为是会住一段时间就能出院。家人也一直不告诉我病情的严重性,父母永远都是这样,总觉得在外是需要好好学习、好好工作的,不应该被分心,不想对我造成困扰。</p><p>有一次跟爷爷的通话我永远也不会忘记:“孙,爷爷看你现在也终于工作了,我也放心了,你从小到大一直都很听话,不用经常打电话担心爷爷了,在外面好好工作,生活上把自己照顾好,顾好自己的小家就可以了。家里的事不用你操心...”</p><p>一直到放 5.1 假,家里都说回来太远让我不用回,然后我也没把这个当回事。直到5.9号那天,我爸给我微信发了一句:“<strong>你想回来见爷爷最后一面吗</strong>?”</p><p>内心再也没法平静,连夜请假买了凌晨的高铁票,赶回家和自己的爷爷告别,无论怎么快,却还是没来得及看他最后一眼。</p><p>爷爷当了几十年的老师,一辈子热衷于自己的教育事业。在我们那小农村,教出来 90 年代的好几个大学生,从为人到做事,所有人都在夸爷爷的好。</p><p>我一直铭记爷爷的事迹和他对我的教诲:大概这是爷爷留给我最宝贵的财产。</p><p><strong>最大的痛苦是生离死别</strong>,最大的成长也莫过于此。</p><p>或许平日里就该珍惜好身边的人,想见的人赶紧去见见,多一面可能是最后一面,多一句可能是最后一句。</p><p>离别终有时,但求不愧余生。</p><h2>今此一别,归来少年</h2><blockquote>我毕业了</blockquote><p>在爷爷丧事期间,研究生答辩也不方便回去,导师同一届学生里唯一一个申请线上参加的人。最后顺利通过,答辩组的提问基本也能回答上。</p><p>只是没有和答辩组留下合影,算是一个研究生小小的遗憾。</p><p>回到学校,我就该毕业了。知道我近况的老师和同学见面都给我安慰和鼓励,我的回答都是:此事让我明白了很多,现在已经完全好了,谢谢。</p><p>生活怎么样都还在继续,该完成的还需要完成:</p><ul><li>拍毕业照、谢师宴</li><li>提交论文</li><li>离校手续</li><li>搬离学校<br>额外多做了两件毕业之外的事:</li><li>给本科学弟学妹们上职业生涯规划(关于考研、考公、求职)</li><li>帮助实验室成立本科团队</li></ul><p>最终在 7 月终于离开了这座象牙塔,没有本科期间的轰轰烈烈,没有那么多伤别,默默完成手续拎着行李回到深圳,真正成为了社会中的打工人。回首这三年,多么谢谢当初那个鼓励自己做读研这个决定的人 <em>(:з」∠)</em></p><p>也愿各位此去,归来仍是少年。</p><h2>读万卷书与行万里路</h2><h3>万卷书读不了 -- 放着</h3><p>离校什么都舍不得仍,书📚 更加舍得。分了好几次才把书全搬过来。</p><p>今年成为了一个 CRUD boy,没有多看技术书。仔细一想,毕竟技术才是手艺,这万一手艺差了就难吃好饭了,争取明年补上。(技术书没看什么就没敢上镜,读的书也不多,只是爱买,担心被喷,狗头🐶 保平安)。</p><p><img src="/img/remote/1460000041173857" alt="03538bff5f5734c14cf9c4d6b5e261a.jpg" title="03538bff5f5734c14cf9c4d6b5e261a.jpg"></p><p>爱书可能也是我天生的,就像女生爱口红那样吧。今年对我影响最大的莫过于《金刚经》了,真正陪我度过了最艰难的 5 月,读过的人可能才知道这本书的神奇之处了。</p><h3>万里路走不了 —— 躺着</h3><p>回到公司,也没有刚来实习的那个工作状态了,又一次组内聚餐,聚餐完走在路上,我追上我的企业导师,跟他简单谈了谈。</p><p>“最近工作状态有点差,想跟你谈一下。我来实习也有 4 个月了,这中间家里也出现了变故,我现在手上的任务差不多完成了。想去毕业旅行,调整一下。” </p><p>“好的,可以理解,那你要请多久。”</p><p>我说:“半个月”</p><p>“。。。”</p><p>导师也是先震惊了一下(可能没想到有人刚转正就要求请假半个月的吧),然后笑着说可以。你确定你手上现在没有多余的工作就可以。我说当前的任务都完成,然后跟组内的同事说明情况,我就溜了。(欠下的债终究要还的,回来发现直接有100个需求等着我们开发组。。。)</p><p>这一段请假的经历有点像今年读过📚《士兵突击》里的袁朗让许三多休息几个月的桥段,许三多一下子找不到当兵的意义了。去当兵是为了父亲,当好兵是为了班长,进老 A 或许为了钢七连,他一直在各种意义下活着。最喜欢的是书中的一段许三多的座右铭:</p><blockquote>好好活就是做有意义的事,有意义的事就是好好活。</blockquote><p>6 月广东疫情,所以也没有想出去。也是没有想到居然在 7 月,刚拿着毕业证签完转正后的入职合同,然后就在 7 月底的两周请了 unpaid leave。 </p><p>先回了一趟长沙见了见朋友,逛了逛校园。四年可以很长,四年改变的也很多。</p><p>毕业四年没有见过的朋友一见面也还是熟悉的样子,最后我们竟然在他们租房的楼顶喝着酒,熬到东方之既白。(但年纪大了表示真的熬不动了)</p><p>然后从长沙坐飞机去了海拉尔,见到了真正的内蒙古大草原。</p><p><img src="/img/remote/1460000041173858" alt="IMG_6310.JPG" title="IMG_6310.JPG"></p><p>第 1 眼看到草原是惊艳,这仿佛是自由,发条朋友圈朋友发来的都是满屏的羡慕;</p><p>过几天发现第 N 眼,看到的还是草的时候,你就觉得原来看草原也是会腻的。</p><p>出去走走的本质其实是<strong>在一个你活腻了的地方去看别人活腻了是什么样子</strong>。</p><p>不出去走走,怎么知道别人活腻了是什么样?怎么知道在家躺着原来这么舒服?</p><h2>不止代码,还有俩猫</h2><h3>你好,三er</h3><p>这是一只半夜出现在阳台的猫,朋友的阳台。没曾想到这只流浪猫,一点不怕人,朋友让我跟着她把猫带着送到宠物医院,医院不要,发朋友圈也没人看上。</p><p>大概每座城市最不缺的就是流浪猫。既然我爱猫,那就贡献自己的微薄力量吧,我选择了领养回家。</p><p>哦。是只三花猫,就叫 “三er”吧。(北方人友好名)</p><p><img src="/img/remote/1460000041173859" alt="51693fbb8429e919b4ad8f5346ee2ff.jpg" title="51693fbb8429e919b4ad8f5346ee2ff.jpg"></p><p>匆匆忙忙带回家,还是只可爱的小猫咪,先买了吃喝拉撒必备东西,后续缺啥再说吧。</p><p>现在打完三针疫苗的三er已经变成一只调皮捣蛋鬼了,身形也肉眼可见的胖了,还学会了偷吃冻干,在家上蹿下跳,坏事干尽。</p><p>我早知今日,何必当初呢。</p><h3>你好,柚子</h3><p>后面的某一天又在宠物医院的朋友圈看到了领养信息,大概我不喜欢独生子的生活,所以不想看“独生”猫的生活。(显然我没有问过三er的意愿)。</p><p>本来约好的是去看一下猫,不一定能让我领养,然后救助人程姐告诉完我猫的经历后,跟我说从 10 多个想要领养猫的人里面挑中了我(这难道就是天生丽质,有猫缘么)。</p><p>既然领养的是只银渐层,那就叫 “银子” 吧,又感觉太市侩,比如我说“银子过来”,它不过来咋办,岂不是说明银子不来我这。</p><p>刚好那天在吃柚子,那就叫 “柚子” 吧,还谐音。(起名鬼才就是我)</p><p><img src="/img/remote/1460000041173860" alt="IMG_8655.JPG" title="IMG_8655.JPG"></p><p>如果你问现在两只猫如何了,小打小闹不会少,但也是和谐的一家人。</p><p>代码之外,从此有程序员鼓励喵🐱了。</p><p><img src="/img/remote/1460000041173861" alt="IMG_8685.JPG" title="IMG_8685.JPG"></p><pre><code> 三er祝大家圣诞快乐
</code></pre><h2>生活 -- 你好生活,生活你好</h2><p>说完了学习与工作,也该聊聊生活了。这些真的都是生命中的一部分,如何平衡这些也决定了生活的质量。</p><p>朋友说:“你是一个到哪都会很快乐的人。”</p><p>其实,我只是知道自己最在乎的是什么而已。自己在乎的都快乐了,我就能快乐了。</p><p>2021年,与过去的7年一样,有一个默默在背后支持我的女朋友,唯一的不同是我们今年异地了,而且会在某个时间点争吵,但是生活又怎能一直风平浪静呢。今年算是给她送了她最满意的一次生日礼物了,希望往后的每一年我们都能越过越好,自己有能力让我们进入下一个阶段。</p><p>2021 还有很多温暖的细节,也很感恩:</p><ul><li>捡到的一只流浪小博美顺利找到主人</li><li>打乒乓球认识的身体倍好的大爷约我东湖公园再战</li><li>在同一座城市努力拼搏的同学一起爬山、徒步</li><li>公司组织的去长隆海洋王国看海狮、白鲸、海豚。还有回味的过山车🎢与各种游玩设施🏄。</li><li>各大平台送来的书、周边、礼物。。。</li></ul><p>其实,在这个城市真的不该只有苦逼的奋斗,真的还应该有生活。</p><p>2021 年,努力跟生活说你好。希望这些 2021 的温暖能常伴吾身。</p><h2>2022 ,不负将来</h2><p>当北上广深这些大城市快速运转起来的时候,无论白天还是夜晚:总少不了步履匆匆的行路人,也有与实践赛跑的外卖小哥,有喧嚣的摊贩与商铺,更少不了在这里拼搏的你我。</p><p>每个人都选择了来到某座城市,虽然不知道明天的生活会是怎样,但都为了各自的理想与目标而努力,我也不例外。</p><p>明年争取多看书、写文章、日更挑战;多学点程序员该学的手艺活(写代码);多加运动,健身;保持和家人和朋友的联系。</p><p>代码之外,让生活更像生活。</p><p>人生最大的幸运就是努力没有白费。</p><p>2022年,我来了。</p>
学习Python一年,这次终于弄懂了浅拷贝和深拷贝
https://segmentfault.com/a/1190000022277283
2020-04-06T14:30:50+08:00
2020-04-06T14:30:50+08:00
宇宙之一粟
https://segmentfault.com/u/yuzhoustayhungry
2
<p>官方文档:<a href="https://link.segmentfault.com/?enc=OHZXF0sGKqfoYdNZkJwZkg%3D%3D.2PETEAXJ49ZVVIrBjDonU4AW2ORMGnOFlGYTRjQCveQq93jXdzt5tKTfjZHygfhApmqxQT9uxdQ9HUooEcJxYQ%3D%3D" rel="nofollow">copy主题</a></p>
<p><strong>源代码:</strong> <a href="https://link.segmentfault.com/?enc=FSUNlciAdiyDpBACx5nfng%3D%3D.AXhtKf6eUPafyH3HanIUVAmuBwU%2FD5msZ4YR8hTT4K%2FoI0FCQ6alZRYL85GoZdEY7P2AuapMRSxyYaV2uSeM6g%3D%3D" rel="nofollow">Lib/copy.py</a></p>
<p>话说,网上已经有很多关于Python浅拷贝和深拷贝的文章了,不过好多文章看起来还是决定似懂非懂,所以决定用自己的理解来写出这样一篇文章。</p>
<p>当别人一提起Python中的复制操作,你会不会立马站起来说:“我会”,于是就有了如下操作:</p>
<pre><code class="python">import copy
x = copy.copy(y) # 浅拷贝我会了
x = copy.deepcopy(y) # 深拷贝我来了</code></pre>
<p>那浅拷贝和深拷贝有什么区别呢,你能给我讲讲吗?</p>
<p><img src="/img/remote/1460000022277288" alt="在这里插入图片描述" title="在这里插入图片描述"></p>
<h2>从引用vs.拷贝说起</h2>
<p>首先,我们要弄清楚什么是对象引用与对象拷贝(复制)。</p>
<h3>对象引用</h3>
<p>Python中对象的赋值其实就是对象的引用。当创建一个对象,把它赋值给另一个变量的时候,Python并没有拷贝这个对象,只是拷贝了这个对象的引用而已。</p>
<pre><code class="python">>>> a = 1
>>> b = a
>>> id(a) == id(b)
True
>>> x = [1, 2, 3]
>>> y = [x, 4]
>>> x
[1, 2, 3]
>>> y
[[1, 2, 3], 4]
>>>
>>>> id(x) == id(y)
False
>>> id(x) == id(y[0])
True</code></pre>
<p>如果这个过程不理解,可以看看下图:<br><img src="/img/remote/1460000022277287" alt="在这里插入图片描述" title="在这里插入图片描述"></p>
<p>当我们对x列表进行操作时,会发现y中也发生了意料之外的事情:</p>
<pre><code class="python">>>> x[1] = 2020
>>> y
[[1, 2020, 3], 4]</code></pre>
<p>由于列表是可变的,修改x这个列表对象的时候,也会改变对象y中对x的引用。</p>
<p>所以当我们在原处修改<strong>可变对象</strong>时 可能会影响程序中其他地方对相同对象的其他引用,这一点很重要。如果你不想这样做,就需要明确地告诉Python复制该对象。</p>
<h3>对象拷贝</h3>
<p>如果你需要拷贝,可以进行如下操作:</p>
<ul>
<li>没有限制条件的分片表达式(<code>L[:]</code>)</li>
<li>工厂函数(如list/dir/set)</li>
<li>字典copy方法(<code>X.copy()</code>)</li>
<li>copy标准库模块(<code>import copy</code>)</li>
</ul>
<p>举个例子,假设有一个列表L和一个字典D:</p>
<pre><code class="python">>>> L = [2019, 2020, 2021]
>>> D = {'1':2019, '2':2020, '3':2021}
>>>
>>> A = L[:] # 区分 A=L 或 A = List(L)
>>> B = D.copy() # 区分 B=D
>>> A
[2019, 2020, 2021]
>>> B
{'1': 2019, '2': 2020, '3': 2021}</code></pre>
<p><img src="/img/remote/1460000022277291" alt="在这里插入图片描述" title="在这里插入图片描述"></p>
<p>这样定义之后,当你修改A和B时,会发现并不会对原来的L跟D产生影响,因为,这就是对象的拷贝。</p>
<pre><code class="python">>>> A[1] = 'happy'
>>> B[3] = 'today'
>>> L, D
([2019, 2020, 2021], {'1': 2019, '2': 2020, '3': 2021})
>>> A, B
([2019, 'happy', 2021], {'1': 2019, '2': 2020, '3': 2021, 3: 'today'})</code></pre>
<p>上述对列表和字典的拷贝操作默认都为浅拷贝:</p>
<ul>
<li>制作字典的浅层复制可以使用 dict.copy() 方法</li>
<li>而制作列表的浅层复制可以通过赋值整个列表的切片完成,例如,copied_list = original_list[:]。</li>
</ul>
<p>说到这里,疑问就产生了?什么是浅拷贝?浅拷贝的对应深拷贝又该作何解释?</p>
<h2>谈谈浅拷贝和深拷贝</h2>
<p>官方文档定义:</p>
<blockquote>
<p>浅层复制和深层复制之间的区别仅与复合对象 (即包含其他对象的对象,如列表或类的实例) 相关:</p>
<ul>
<li>一个 <strong>浅层复制</strong> 会构造一个新的复合对象,然后(在可能的范围内)将原对象中找到的 <strong>引用</strong> 插入其中。</li>
<li>一个 <strong>深层复制</strong> 会构造一个新的复合对象,然后递归地将原始对象中所找到的对象的 <strong>副本</strong> 插入。</li>
</ul>
</blockquote>
<h3>浅拷贝</h3>
<p><strong>浅拷贝</strong>:拷贝了最外围的对象本身,内部的元素都只是拷贝了一个引用而已。也就是,把对象复制一遍,但是该对象中引用的其他对象我不复制。</p>
<p>用通俗的话理解就是:你的橱柜(对象)里装着一?(篮子)?(鸡蛋),然后浅拷贝一下的意思。我只拷贝了最外面的这个橱柜,至于里面的内部元素(?和?)我并不拷贝。</p>
<p>当我们遇到简单的对象时,用上面的解释好像很好理解;如果遇到复合对象,就比如下列代码:</p>
<pre><code class="python">l1 = [3, [66, 55, 44], (3, 7, 21)]
l2 = list(l1)
l1.append(100)
print('l1:', l1)
print('l2:', l2)
l1[1].remove(55)
l2[1] += [33, 22]
l2[2] += (9, 9, 81)
print('l1:', l1)
print('l2:', l2)</code></pre>
<p>代码解释:</p>
<blockquote><ul>
<li>
<code>l2</code>是<code>l1</code>的浅拷贝</li>
<li>
<p>把100追加到<code>l1</code>,对<code>l2</code>没有影响</p>
<ul>
<li>
<code>l1</code>内部列表<code>l1[1</code>中的55删除,对<code>l2</code>也产生影响,因为<code>l1[1]</code>和<code>l2[1]</code>绑定的是同一个列表</li>
<li>对可变对象来说,<code>l2[1</code>引用的列表进行+=就地修改列表。这次修改导致<code>l1[1]</code>也发生了改变</li>
</ul>
</li>
<li>对元组来说,+= 运算符创建一个新元组,然后重新绑定给变量 l2[2]。这等同于 <code>l2[2] = l2[2] + (10, 11)</code>。现在,<code>l1</code> 和 <code>l2</code> 中最 后位置上的元组不是同一个对象</li>
</ul></blockquote>
<p>把这段代码可视化出来如下:<br><img src="/img/remote/1460000022277289" alt="在这里插入图片描述" title="在这里插入图片描述"></p>
<p>动手试一试,可以点<a href="https://link.segmentfault.com/?enc=ZTzFJbtHo0S1UyKlw%2FX7mA%3D%3D.P5tvcPapACZMDvXgW%2B41BYVojFSI7aPaPmao%2BDA%2BLe%2FCTH4QtOKyt%2B6228uMlMz6LMondWDPcLdxFJrXa4%2BFKBeoZMPsqs73if8Janb2Xfa4cW%2Fr84Txq4pySGVgKaPnTUeUElwO7eCC66YGhKj4slmBP8C4%2B18o7NbrQE6NLdWs912n8SHkj2dC3%2FWWQRgrpljXZNYEVhmbMZBODCj4Yw4kCGnYTSevhhIMY4QADwRaN6VwFrSdwNJ07sFAzeJMfzzF%2FuSfxXo95ZOnTADTV3aEyH7wi%2FMpJA672fyr2e979YJU6qaTqPHU%2BDKr76JKc7g3EixeeQj7GMalChc%2FuU1h5yLh2iLKgbSvmbxx2n4OvwefG5qh6a4cqkeEvNcRaER4zLjkJbnybh3OKFMNRTE5SG8aMwuoW1uKeiIGNBG5gEqp7ewsspEA%2FwvOe%2Bh2bd5SYHoQrCmqyM%2BQt%2FUSTxasoF1hrRa%2BXrsOzO64lVXkTQc29JKRkqXc4SLN7%2BWhlJM48xoDbPaU4Q5IxRlkEey0Mb04%2B3zataDa6GHKvEOeaNlmkOkvfiRCR2xozKgQwvSLM08WE%2FxKi3yNNn0USkKuFB3C2NJ82Uu%2BbyX0%2Fy2SLbyzb6ZeMdb1p%2FRM7u%2Fu85bv%2FcGbmlY%2FnypqF%2BLuHek7TdotWxIZemSdlSCXtkuRLtlCuPAkJ8TDEEgwAJ7UZqiOIo13VXMEZ7Ps6WcHSQ%3D%3D" rel="nofollow">此处</a></p>
<h3>深拷贝</h3>
<p><strong>深拷贝</strong>:外围和内部元素都进行了拷贝对象本身,而不是引用。也就是,把对象复制一遍,并且该对象中引用的其他对象我也复制。</p>
<p>对比上面的篮子和鸡蛋:你的橱柜(对象)里装着一?(篮子)?(鸡蛋),然后深拷贝一下的意思。把最外面的这个橱柜和里面的内部元素(?和?)全部拷贝过来。<br><img src="/img/remote/1460000022277292" alt="在这里插入图片描述" title="在这里插入图片描述"></p>
<pre><code class="python">from copy import deepcopy
l1 = [3, [66, 55, 44], (3, 7, 21)]
l2 = deepcopy(l1)
l1.append(100)
print('l1:', l1)
print('l2:', l2)
l1[1].remove(55)
l2[1] += [33, 22]
l2[2] += (9, 9, 81)
print('l1:', l1)
print('l2:', l2)</code></pre>
<p>输出结果:</p>
<p><img src="/img/remote/1460000022277290" alt="在这里插入图片描述" title="在这里插入图片描述"></p>
<h3>拷贝的特点</h3>
<ul><li>不可变类型的对象</li></ul>
<p>(如数字、字符串、和其他'原子'类型的对象)对于深浅拷贝毫无影响,最终的地址值和值都是相等的。也就是,"obj is copy.copy(obj)" 、"obj is copy.deepcopy(obj)"</p>
<ul><li>可变类型的对象</li></ul>
<p>=浅拷贝: 值相等,地址相等<br>copy浅拷贝:值相等,地址不相等<br>deepcopy深拷贝:值相等,地址不相等</p>
<ul><li>循环引用的对象</li></ul>
<p>如果对象有循环引用,那么这个朴素的算法会进入无限循环。deepcopy 函数会记住已经复制的对象,因此能优雅地处理循环引用。</p>
<p>循环引用:b 引用 a,然后追加到 a 中;<br>deepcopy 会想办法复制 a,而copy会进入无限循环。如下面代码:</p>
<pre><code class="python">from copy import deepcopy, copy
a = [80, 90]
b = [a, 100]
a.append(b)
print("a:", a)
print("b:", b)
c = deepcopy(a)
print("c:", c)
d = copy(b)
print("d:", d)</code></pre>
<p>输出结果:</p>
<pre><code class="python">a: [80, 90, [[...], 100]]
b: [[80, 90, [...]], 100]
c: [80, 90, [[...], 100]]
d: [[80, 90, [[...], 100]], 100]</code></pre>
<h3>深浅拷贝的作用</h3>
<ol>
<li>减少内存的使用</li>
<li>以后在做数据的清洗、修改或者入库的时候,对原数据进行复制一份,以防数据修改之后,找不到原数据。</li>
<li>可以定制复制行为,通过实现<code>__copy()</code>和<code>__deep__()</code>方法来控制。</li>
</ol>
<h2>总结</h2>
<p>看完这篇文章后,转身就跟你同桌说:<br>“x同学,听说你最近在学Python,你知道浅拷贝和深拷贝吗?”<br>“不知道,学得有点晕”<br>“没事,我来给你讲讲:”</p>
<p><strong>拷贝</strong>其实在开始学好几个操作语句中,我们就已经使用过却可能不知道的(前3个),而且浅拷贝是Python的默认拷贝方式。拷贝的方法如下:</p>
<ol>
<li>可变类型的切片操作:<code>[:]</code>
</li>
<li>工厂函数(如list/dir/set)</li>
<li>字典copy方法(<code>X.copy()</code>)</li>
<li>然后就是Python有专门的copy标准库模块:包含两个方法<code>copy()</code>和<code>deepcopy()</code>
</li>
</ol>
<p>浅拷贝就像是我只拷贝最外围的对象,对象中引用的其他对象我不复制。深拷贝就是完整的把对象和对象里的内容都拷贝过来。拷贝的目的:</p>
<ol>
<li>为了节省内存</li>
<li>防止数据丢失。</li>
</ol>
<p><strong>后记</strong>:深浅拷贝的坑及难以理解的点也只在复合对象上,简单对象就是我们平常理解的复制。而针对非容器类型(如数字、字符串、和其他'原子'类型的对象)没有被拷贝一说。</p>
<p>要是你的同桌还是不懂,你就把这篇文章甩给他,让他好好看看。如果你觉得这篇文章还不错,请点个赞或者收个藏,点个关注更好啦。</p>
<blockquote>
<p>本文内容有参考:</p>
<ol>
<li>中文网址:<a href="https://link.segmentfault.com/?enc=snpIH%2F8%2FDQePcwdQTfw4BQ%3D%3D.HXORoQ99HIr3V8O2Gl6EyQiFlpnTdMJpTqKIkb7x8TAdqekVx5XjoqwVQbEluWBw" rel="nofollow">将你的代码运行可视化</a>
</li>
<li>《流畅的Python》-- 第 8 章 对象引用、可变性和垃圾回收</li>
<li>《Python3标准库》-- <a href="https://link.segmentfault.com/?enc=%2Blm2pD9bbfM26zG2TlRjlw%3D%3D.pLxx1KCMFJApjxLIHdUqGdRHGuENSrtAQEjlW8gg%2BZo%3D" rel="nofollow">2.9 copy — Duplicate Objects</a>
</li>
</ol>
</blockquote>
为什么校招面试中“线程与进程的区别”老是被问到?我该如何回答?
https://segmentfault.com/a/1190000022270862
2020-04-05T17:37:32+08:00
2020-04-05T17:37:32+08:00
宇宙之一粟
https://segmentfault.com/u/yuzhoustayhungry
0
<h2>进程与线程?(Process vs. Thread?)</h2>
<p><strong>面试官</strong>(正襟危坐中):给我说说“线程”与“进程”吧。</p>
<p><strong>我</strong>(总是不太聪明的样子):“限乘?”、“进什么城(程)?”</p>
<p><strong>面试官</strong>:“操作系统中的进程与线程,你回去了解一下。门在左边,记得关门。”<br><img src="/img/remote/1460000022270867" alt="在这里插入图片描述" title="在这里插入图片描述"><br>当翻译过来后,这两个概念都带了个“程”字,但进程的英文:Process,而线程的英文:Thread,好像并没有什么联系。<br>大多数初学者一开始都会被这两个概念弄的晕头转向,包括我本人。<br><img src="/img/remote/1460000022270866" alt="在这里插入图片描述" title="在这里插入图片描述"><br>当你看完这篇文章,可能你就有了新的理解。</p>
<p>不信,你接着往下看看(不过在这之前,点个赞或关注好不好?)。</p>
<h3>进程和线程基础(理论概念)</h3>
<h4>1. 定义</h4>
<p>看了下面的定义,可能会有点晕,但我还是要把他写下来(为了严谨)。</p>
<p><strong>进程</strong>是资源(CPU、内存等)分配的基本单位,具有一定<strong>独立</strong>功能的程序关于某个数据集合上的一次运行活动,进程是系统进行<strong>资源分配和调度</strong>的一个独立单位。</p>
<p><strong>线程</strong>是进程的一个实体,是<strong>独立运行和独立调度</strong>的基本单位(CPU上真正运行的是线程)。线程自己基本上不拥有系统资源,只拥有一点在运行中必不可少的资源(如程序计数器,一组寄存器和栈),但是它可与同属一个进程的其他的<strong>线程共享</strong>进程所拥有的全部资源。</p>
<h4>2.区别</h4>
<ol>
<li>进程是资源分配的基本单位;线程是程序执行的基本单位。</li>
<li>进程拥有自己的资源空间,没启动一个进程,系统就会为它分配地址空间;而线程与CPU资源分配无关,多个线程共享同一进程内的资源,使用相同的地址空间。</li>
<li>一个进程可以包含若干个线程。</li>
</ol>
<h4>3. 优劣</h4>
<p>正是因为这二者有区别,所以带来的各自的优劣</p>
<ol>
<li>线程之间的通信更方便,同一进程下的线程共享全局变量、静态变量等数据,而进程之间的通信需要以通信的方式(Inter Process Communication,IPC)进行。不过如何处理好同步与互斥是编写多线程程序的难点。</li>
<li>线程的调度与切换比进程快很多,同时创建一个线程的开销也比进程要小很多。</li>
<li>但是多进程程序更健壮,多线程程序只要有一个线程死掉,整个进程也死掉了,而一个进程死掉并不会对另外一个进程造成影响,因为进程有自己独立的地址空间。</li>
</ol>
<blockquote>除此之外,推荐看一下阮一峰的一篇博客:<a href="https://link.segmentfault.com/?enc=nQsk4XG6Wu%2FbM1fRp3R46w%3D%3D.nhWqRfTVdv1K%2BMmLC4DINUtFRqP8aYnT8NylA34S8UimD2vBO0LJlH%2F9zKIGlDjYl58ElT8ZsybpkcUBJYT%2FjP4aXp7QQXLluyWuDD3ynfE%3D" rel="nofollow">进程与线程的一个简单解释</a>,用图解释十分生动形象。</blockquote>
<h3>为什么这个问题是面试高频?</h3>
<p>既然这个问题是面试当中会被经常问到的,所以我去网上找一个答案,背出来不就好了。</p>
<p><img src="/img/remote/1460000022270868" alt="在这里插入图片描述" title="在这里插入图片描述"></p>
<p>但是,真的背答案就可以了吗?</p>
<p>我们来分析一下为什么众多面试官老是问这个问题,他应该并不是想听到一个对书本上概念的重复。</p>
<p>那么,<strong>他究竟想考什么?</strong></p>
<ol>
<li>侧重点一:面试官想要了解面试者对这一<strong>知识点的理解程度</strong>(因为这是操作系统中不得不提的一个概念)。如果这个概念回答不上来,意味着面试者对操作系统的学习并不深。</li>
<li>侧重点二:面试官可以对你的回答作进一步展开,通过你的回答某个侧重点方向来<strong>进一步提问</strong>你对你自己回答的理解。(这个高频问题的价值所在)。</li>
</ol>
<p>比如:</p>
<ul>
<li>当你回答到:进程与线程的内存结构不同。进程与进程之间不能共享内存,而线程可以。那么面试官就可以就内存这一点深入提问——内存如何寻址?</li>
<li>当你回答:线程之间通信很方便,进程与进程通信不方便。那么问题就又来了,你给我说一下进程之间怎么通信?进程之间通信方法有哪些?不同通信方法有哪些优劣点?</li>
</ul>
<h3>一个更满意的答案?</h3>
<p>如何作答,才能展示一个让面试官更满意的答案?</p>
<p>这里就不得不用张三丰教给张无忌的太极拳的那一招——忘掉。。。<br>对就是把上面的概念全都忘掉。只留一个目的:“把敌人打败”。<br>最后用自己的一招一式(理解)来回答。</p>
<h4>再谈“进程”与“线程”(口语表述)</h4>
<p>进程的<strong>本质:</strong>:正在执行的一个程序,可以进程比作一个容器或者工厂<br><img src="/img/remote/1460000022270869" alt="在这里插入图片描述" title="在这里插入图片描述"><br>通过上图,方便我们了解并记忆:</p>
<ol>
<li>进程与进程之间相对独立</li>
<li>进程可以包括几个或者上百个线程在运行。</li>
<li>内存(逻辑内存)包括在进程里面,每个进程的内存都是互相独立的,但从一个更高的层次上看,不同的进程也共享着一个巨大的空间,这个空间就是整个计算机。</li>
<li>进程共有文件/网络句柄(handle),这样可以打开同一个文件,抢同一个网络端口。</li>
</ol>
<p>从不同的视角来看进程:<br><img src="/img/remote/1460000022270870" alt="进程模型的3个视角" title="进程模型的3个视角"></p>
<p>线程的<strong>本质</strong>:真正运行的是一个一个的线程<br><img src="/img/remote/1460000022270871" alt="在这里插入图片描述" title="在这里插入图片描述"><br>同理,上图我们知道线程包含:</p>
<ol>
<li>栈(堆栈):主线程的main函数、进行函数调用的参数和返回地址、局部变量等内容都会被压入栈内</li>
<li>PC(Program Couner):程序计数器,PC的指针指向代码所在的内存地址。</li>
<li>TLS(Thread local storage):分配内存,存放变量</li>
</ol>
<p>当有了上面的问题做引子后,面试官就可以借此引出更多话题:</p>
<h4>1. 如何通信(沟通)的内容</h4>
<p>通信是人的基本需求,进程与进程之间是相互独立的,也有通信需求。根据这一问题就可以展开内容提问:</p>
<ul><li>进程/线程如何通信</li></ul>
<p>答:进程可以通过管道、套接字、信号交互、共享内存、消息队列等等进行通信;而线程本身就会共享内存,指针指向同一个内容,交互很容易。</p>
<ul><li>通信方式的差异,比如进程间共享内存和消息队列有何异同?</li></ul>
<h4>2. 如何同步(协调)的内容</h4>
<p>一旦有了通信,人与人之间就会产生矛盾,进程也一样。这些矛盾就会体现在如何同步上。</p>
<ul>
<li>在单个CPU下,实际上在任何时刻只能有一个进程处于执行状态。而其他进程则处于非执行状态。我们是如何确定在任意时刻到底由哪个进程执行,哪些不执行呢?(如何进行进程调度?)</li>
<li>线程之间的关系是合作关系。既然是合作,那就得有某种约定的规则,否则合作就会出问题。(如何进行线程同步?)</li>
</ul>
<h4>3. 内存问题?</h4>
<p>进程要分配内存,所以开销很大,进程只需要分配栈,分配一个PC就好,内存开销小。</p>
<p>这一块就可以问到了操作系统中的内存原理相关的内容。</p>
<h2>总结</h2>
<p>总之,如果上述内容你都了解,那肯定是不怕被问到(大佬,请收下我的膝盖);如果看了此篇文章之后,你能答出个大概,我相信面试官也会放过你,毕竟,我们也真的不是背书机器。</p>
<blockquote>如果你能看到这,能否给我点个关注,点个赞让我也收到鼓励。如果觉得我写的内容有误,也欢迎评论指出。</blockquote>
<p>注意,要敲黑板啦。<br><img src="/img/remote/1460000022270872" alt="在这里插入图片描述" title="在这里插入图片描述"></p>
<ul>
<li>进程是什么?它指的是一个运动中的程序。从名字上看,进程表示的就是进展中的程序。一个程序一旦在计算机里运行起来,它就成为一个进程。进程与进程之间可以通信、同步、竞争,并在一定情况下可能形成死锁。</li>
<li>那么线程是什么?我们知道,进程是运转的程序,是为了在CPU上实现多道编程而发明的一个概念。但是进程在一个时间只能干一件事情。如果想同时干两件事,办法就是线程。线程是进程里面的一个执行上下文或者执行序列。</li>
</ul>
<p>最后,祝大家答的愉快!面试过!过!过!</p>
<blockquote>
<p>参考资料:</p>
<ol>
<li>《现代操作系统(第3版)》</li>
<li>《操作系统之哲学原理(第2版)》</li>
</ol>
</blockquote>