这是我参与更文挑战的第11天,活动详情查看: 更文挑战

星星之火,可以燎原。

在掘金看到有推活动:“掘金线下沙龙——拥抱云原生:探索云原生下的框架与存储”,看到其中有位大佬的主题是“Java拥抱云原生——Quarkus介绍”,工作三年开发的表示一脸懵逼,云原生?Quarkus?没听过~~学!!

Quarkus介绍

Quarkus据说是RedHat开源的一个微服务框架,所以可以把它与SpringCloud/SpringBoot类比?

暂且不表,先看官方文档!比较不能成为尤大嘴里的“有些人”~此处应该有个表情包!

Quarkus官网

什么是Quarkus?

Quarkus是专为OpenJDK Hotspot 和 GraalVM而生的一个全栈,Kubernetes-native的Java应用框架,相比Spring,低内存消耗,快速启动,允许将命令式代码和非阻塞响应式风格结合起来。

什么是Quarkus中说:Quarkus 构建的应用其内存消耗只有传统 Java 的 1/10,而且启动时间更快(快了 300 倍)

Quarkus关键字

  1. 为开发人员设计:实时编码,统一配置,原生可执行文件生成。

  2. 容器优先:支持GraalVM/SubstrateVM,构建时元数据处理,减少反射使用,本机映像预启动。

  3. 命令式和响应式代码

Talk is Cheap,Show me the Code!

Quarkus实战

开启Quarkus的Hello World之旅~

官方教程

环境需求:

  1. IDE: eclipse、idea、vscode,vim,emacs等,我们使用的环境是Eclipse
  2. Java JDK 8 or 11+,笔者电脑安装的Jdk 8
  3. Apache Maven 3.6.2+ 或 Gradle,笔者电脑安装的是maven 3.6.0,先不升级试试

首先尝试直接git clone官方的github-demo

1
2
3
git clone https://github.com/quarkusio/quarkus-quickstarts.git
# 上述链接下载不了可以尝试以下链接:
# git clone git@github.com:quarkusio/quarkus-quickstarts.git

发现项目下载内容超多

image.png

暂不用官方给的样例,我们仿照 Bootstrapping the project 的流程执行:

powershell中执行命令:

1
mvn io.quarkus:quarkus-maven-plugin:1.13.7.Final:create -DprojectGroupId=org.acme -DprojectArtifactId=getting-started -DclassName="org.acme.getting.started.GreetingResource" -Dpath="/hello"

输出结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
[INFO] ------------------------------------------------------------------------
[INFO] BUILD FAILURE
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 01:00 min
[INFO] Finished at: 2021-06-11T18:00:18+08:00
[INFO] ------------------------------------------------------------------------
[ERROR] Failed to execute goal io.quarkus:quarkus-maven-plugin:1.13.7.Final:create (default-cli) on project standalone-pom: Detected Maven Version (3.6.0) is not supported, it must be in [3.6.2,). -> [Help 1]
[ERROR]
[ERROR] To see the full stack trace of the errors, re-run Maven with the -e switch.
[ERROR] Re-run Maven using the -X switch to enable full debug logging.
[ERROR]
[ERROR] For more information about the errors and possible solutions, please read the following articles:
[ERROR] [Help 1] http://cwiki.apache.org/confluence/display/MAVEN/MojoExecutionException

报了几个错误,怀疑是我们的maven版本问题,所以我们升级maven后重新运行以上命令。

升级maven 3.8.0后,执行命令,输出结果为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
applying codestarts...
>> java
>> maven
>> quarkus
>> config-properties
>> dockerfiles
>> maven-wrapper
>> resteasy-example

-----------
[SUCCESS] quarkus project has been successfully generated in:
--> D:\XXXXX\learn\Quarkus\getting-started
-----------
[INFO]
[INFO] ========================================================================================
[INFO] Your new application has been created in D:\XXXXX\learn\Quarkus\getting-started
[INFO] Navigate into this directory and launch your application with mvn quarkus:dev
[INFO] Your application will be accessible on http://localhost:8080
[INFO] ========================================================================================
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 01:36 min
[INFO] Finished at: 2021-06-11T20:31:23+08:00
[INFO] ------------------------------------------------------------------------

可以看到,有一个“greeting-started”的maven项目,我们将其导入到eclipse中,其项目目录如下图:

image.png

我们尝试运行一下项目:

1
2
cd .\getting-started\
./mvnw compile quarkus:dev

powershell中输出结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
[INFO] Scanning for projects...
Downloading from central: https://repo.maven.apache.org/maven2/io/quarkus/quarkus-universe-bom/1.13.7.Final/quarkus-universe-bom-1.13.7.Final.pom
Downloaded from central: https://repo.maven.apache.org/maven2/io/quarkus/quarkus-universe-bom/1.13.7.Final/quarkus-universe-bom-1.13.7.Final.pom (614 kB at 256 kB/s)
[INFO]
[INFO] ----------------------< org.acme:getting-started >----------------------
[INFO] Building getting-started 1.0.0-SNAPSHOT
[INFO] --------------------------------[ jar ]---------------------------------
Downloading from central: https://repo.maven.apache.org/maven2/io/quarkus/quarkus-arc/1.13.7.Final/quarkus-arc-1.13.7.Final.pom
.......省略
[INFO]
[INFO] --- maven-resources-plugin:2.6:resources (default-resources) @ getting-started ---
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] Copying 2 resources
[INFO]
[INFO] --- maven-compiler-plugin:3.8.1:compile (default-compile) @ getting-started ---
[INFO] Changes detected - recompiling the module!
[INFO] Compiling 1 source file to D:\QiWeiwei\learn\Quarkus\getting-started\target\classes
[INFO]
[INFO] --- quarkus-maven-plugin:1.13.7.Final:dev (default-cli) @ getting-started ---
Listening for transport dt_socket at address: 5005
2021-06-11 20:50:51,951 WARN [io.qua.dep.QuarkusAugmentor] (main) Using Java versions older than 11 to build Quarkus applications is deprecated and will be disallowed in a future release!
__ ____ __ _____ ___ __ ____ ______
--/ __ \/ / / / _ | / _ \/ //_/ / / / __/
-/ /_/ / /_/ / __ |/ , _/ ,< / /_/ /\ \
--\___\_\____/_/ |_/_/|_/_/|_|\____/___/
2021-06-11 20:50:54,529 INFO [io.quarkus] (Quarkus Main Thread) getting-started 1.0.0-SNAPSHOT on JVM (powered by Quarkus 1.13.7.Final) started in 2.757s. Listening on: http://localhost:8080
2021-06-11 20:50:54,530 INFO [io.quarkus] (Quarkus Main Thread) Profile dev activated. Live Coding activated.
2021-06-11 20:50:54,531 INFO [io.quarkus] (Quarkus Main Thread) Installed features: [cdi, resteasy]

浏览器中请求:http://localhost:8080/hello结果如下

image.png

这是我参与更文挑战的第10天,活动详情查看: 更文挑战

是以圣人居无为之事,行不言之教,万物作而弗始也,为而弗志也,成功而弗居也。夫唯弗居,是以弗去。

经常有需求说需要爬取某某网站的某些数据,因为python的包最多的,首先尝试使用python爬便有了下文

有了python爬虫,爬图这项技能,不光能爬数据,爬图~

建议大家在法律范围内做爬虫,毕竟命令是领导下的,锅却要我们来背~

python基本配置

安装pip

通过pip我们可以很方便的通过包名安装其他的python包。在Python 2 >=2.7.9 or Python 3 >=3.4 中已经内置了pip。可以使用如下命令查看是否已安装pip。

1
2
python -m pip --version
# output: pip 18.0 from C:\Users\lenovo\AppData\Local\Programs\Python\Python36\lib\site-packages\pip (python 3.6)

如果没有,可以通过下载get-pip.py,并运行如下命令安装:

1
python get-pip.py

我们可以使用pip安装其他包,如下文需要使用的BeautifulSoup需要我们安装bs4

1
pip3 install bs4

爬虫常用包

requests

requests是一个处理URL资源很方便的包。

1
2
3
4
5
6
import requests

r = requests.get('https://juejin.cn')
print(r)
print(r.status_code)
print(r.text)

输出结果:

1
2
3
4
5
6
<Response [200]>
200
<!doctype html>
<html data-n-head-ssr lang="zh" data-n-head="%7B%22lang%22:%7B%22ssr%22:%22zh%22%7D%7D">
<head >
<title>掘金 - 代码不止,掘金不停</title><meta data-n-head="ssr" charset="utf-8"><meta data-n-head="ssr" name="viewport" content="width=device-width, initial-scale=1, user-scalable=no, viewport-fit=cover"><meta data-n-head="ssr" name="apple-itu......

requests我们可以在请求中添加头部,以及需要验证网站的cookie信息。详细文档可查看:Requests: HTTP for Humans

几个常用的样例:

1
2
3
4
5
r = requests.get('https://xxx', auth=('user', 'pass'))
r = requests.post('https://xxxx', data = {'key':'value'})
payload = {'key1': 'value1', 'key2': 'value2'}
r = requests.get('https://httpbin.org/get', params=payload)
print(r.text)

BeautifulSoup

使用Beautiful Soup可以很方便的从html中提取数据。

官方中文文档地址:https://beautifulsoup.readthedocs.io/zh_CN/v4.4.0/

简单的使用样例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
from bs4 import BeautifulSoup
soup = BeautifulSoup(open("index.html"))
soup = BeautifulSoup("<html>data</html>")

# 浏览数据的方式
soup.title
# <title>The Dormouse's story</title>
soup.title.name
# u'title'
soup.title.string
# u'The Dormouse's story'
soup.title.parent.name
# u'head'
soup.p
# <p class="title"><b>The Dormouse's story</b></p>
soup.p['class']
# u'title'
soup.a
# <a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>
soup.find_all('a')
# [<a class="sister" href="http://example.com/elsie" id="link1">Elsie</a>,
# <a class="sister" href="http://example.com/lacie" id="link2">Lacie</a>,
# <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>]
soup.find(id="link3")
# <a class="sister" href="http://example.com/tillie" id="link3">Tillie</a>

open函数文件下载

open是python的内置函数,用于打开一个文件,并返回文件对象。常用参数为filemode,完整参数如下:

1
2
3
4
5
6
7
8
9
10
11
12
open(file, mode='r', buffering=-1, encoding=None, errors=None, newline=None, closefd=True, opener=None)
"""
参数说明:
file: 必需,文件路径(相对或者绝对路径)。
mode: 可选,文件打开模式:
buffering: 设置缓冲
encoding: 一般使用utf8
errors: 报错级别
newline: 区分换行符
closefd: 传入的file参数类型
opener:
"""

开启爬图之路

爬图链接:https://www.easyapi.com/xxx

这个链接的特点是:

  1. 简单,只有一张图片
  2. 链接不变,但刷新后图片变化

网页HTML代码与页面展示如图:

image.png

image.png

我们取其中的重要源码查看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<head>
...
</head>
<body id="404">
<div class="mheight wp">
<div class="con_nofound">
<div>
<!-- 重点在于如何获取这个img标签以及src内容 -->
<p><img src="https://qiniu.easyapi.com/photo/girl106.jpg" title="欣赏美女" width="600"></p>
</div>
</div>
</div>
....
</body>

刷新页面,会发现imgsrc路径在编号,但title不变。

因此,我们可以通过<img title="欣赏美女" .../>来获取这个标签以及src

获取html内容

使用requests获取html内容:

1
2
headers = {'referer': 'https://www.easyapi.com/highmall/service', 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64; rv:47.0) Gecko/20100101 Firefox/47.0'}
htmltxt = requests.get(res_url, headers=headers).text

查找html中的图片链接

1
2
3
4
html = BeautifulSoup(htmltxt)
for link in html.find_all('img', {'title': '欣赏美女'}):
# print(link.get('src'))
srcLink = link.get('src')

下载图片

1
2
3
# 'wb'表示以二进制格式打开一个文件只用于写入。如果该文件已存在则打开文件,并从开头开始编辑,即原有内容会被删除。如果该文件不存在,创建新文件。一般用于非文本文件如图片等。
with open('./pic/' + os.path.basename(srcLink), 'wb') as file:
file.write(requests.get(srcLink).content)

完整代码

网页图片是随机的,因此我们循环请求1000次,获取并下载图片。完整代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import requests
from bs4 import BeautifulSoup
import os

index = 0
headers = {'referer': 'https://www.easyapi.com/xxx/service', 'user-agent': 'Mozilla/5.0 (Windows NT 10.0; WOW64; rv:47.0) Gecko/20100101 Firefox/47.0'}

# 保存图片
def save_jpg(res_url):
global index
html = BeautifulSoup(requests.get(res_url, headers=headers).text)
for link in html.find_all('img', {'title': '欣赏美女'}):
print('./pic/' + os.path.basename(link.get('src')))
with open('./pic/' + os.path.basename(link.get('src')), 'wb') as jpg:
jpg.write(requests.get(link.get('src')).content)
print("正在抓取第"+str(index)+"条数据")
index += 1

if __name__ == '__main__':
url = 'https://www.easyapi.com/xxx/service'
# 其实不需要循环到1000,通过打印链接可以发现,图片名称地址为 xxx/girl(number).jpg,优化方向可以舍弃获取html再获取图片链接
for i in range(0, 1000):
save_jpg(url)

运行效果:

image.png

image.png

有了这项技能,你不光能爬图片~~

若不吝,请点个赞!

这是我参与更文挑战的第9天,活动详情查看: 更文挑战

开心,T恤到手~

策之不以其道,食之不能尽其材,鸣之而不能通其意,执策而临之,曰:“天下无马!”呜呼!其真无马邪?其真不知马也!

运营人员反馈某商品列表页面,每页1000条时,页面卡顿严重,已经影响使用,经实际测试,1000个条记录时,页面加载长达2分钟+,并且会出现卡死情况。遂开始优化之旅~~

页面元素

页面主体部分截图如下,每个元素包括一张主图,一些基本的信息字段。运营反馈当设置分页数量为1000时,会出现卡死,经过实际测试,可以重现。

image.png

主体前端代码分为两个部分,一个是包裹这些元素的父元素,一个是元素组件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<!-- 父元素关键代码 --> 
<div v-loading="productsLoading" class="choosingproduct-products">
<!-- product-card为子组件,使用v-model绑定列表元素 -->
<product-card
v-for="(item, index) in result.list"
:key="index"
v-model="result.list[index]"
:showpoint="false"
/>
<div style="width: 100%;margin-top: 15px;margin-bottom: 85px" class="center">
<pagination
:total="result.totalRow"
:page.sync="query.pageNum"
:limit.sync="query.numPerPage"
@pagination="getSkus"
/>
</div>
</div>

子组件ProductCard的关键代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
<template>
<div class="productcard flex-col-start-start" @click="changeChoosed">
<div class="flex-row-center-spacebetween" style="width: 100%">
<!-- 关键点1:状态切换使用了visibility -->
<el-checkbox v-model="value.choosed" class="productcard-checkbox " :class="{'productcard-delete' : !value.choosed}" @change="changeChoosed" />
</div>
<div style="width: 100%" class="center">
<!-- 关键点2: 图片过多 -->
<el-image :src="value.picurl" style="height: 114px;width: 114px" />
</div>
<div .... />
</div>
</template>

<script>
export default {
name: 'ProductCard',
props: {
value: {
type: Object,
default() {
return {}
}
},
},
data() {
return {
product: {
choosed: false
}
}
},
watch: {
// 关键点3: 也是最重要的一点,watch监听不当,导致性能与卡顿
product: {
deep: true,
handler: function(newValue, oldValue) {
this.$emit('input', this.product)
}
},
value: {
deep: true,
handler: function() {
this.product = this.value
}
}
},
created() {
this.product = this.value
},
methods: {
changeChoosed() {
// 改变选中状态
this.product.choosed = !this.product.choosed
}
}
}
</script>

<style scoped lang="scss">
.productcard {
.productcard-checkbox {
visibility: hidden;
}

.productcard-delete {
visibility: hidden;
}
}

.productcard:hover {
.productcard-checkbox {
visibility: visible;
}
.productcard-delete {
visibility: visible;
}
}
</style>

后端接口

后端根据前端选择的条件,执行查询SQL,返回数据,使用的数据库连接池Druid+ActiveRecord模式。(对这一点感兴趣的可以检索jfinal相关)

1
2
3
4
5
6
7
8
public Page<Record> getData() {
......
Page<Record> result = Db.paginate(pageNum, numPerPage, select, exceptSelect);
for(Record r : result.getList()) {
display(r);
}
return result;
}

分析步骤

因为后端的性能更好分析一些,本人也更熟悉后端,所以先从后端入手:

后端

经过测试,接口返回1000条数据的时间大约是17秒。

  1. 这个时间相当长,有优化空间
  2. 前台卡顿时间2分钟不符合,不是前端卡死的问题关键

前端

使用Chrome-Performance监听加载1000条数据的性能情况,我们得到如下几张图:

image.png

image.png

从而确定了卡顿问题主要在前端,而且从renderList我们猜测,卡顿主要在于ProductCard这个组件的渲染上。

猜测

定位到了ProductCard组件后,我们首先根据Network查看到图片的加载时间较长,猜测可能是图片过多导致加载过长。

第二个,我们注意到选中与不选中,我们在CSS中是通过变换组件的visibility属性来实现的。是否是visibility切换会导致组件渲染卡顿呢?我们检索资料发现visibility:hidden的性能实际比display:none要好。

第三个(终于找到你!),我们在组件中使用了watch,同时监听组件内部元素productprops中的value,当product变动时,会通过this.$emit('input', this.product)传递给外部的value,而value变动,又会触发this.product = this.value,进而又导致product变动,形成近乎死循环。造成严重性能问题。

我们得到以下结论:

  1. 图片过多导致加载过长
  2. 使用visibility
  3. 慎用watch

优化

后端

我们使用parallelStream优化后端性能,优化后代码如下。接口响应时间从17秒左右下降到5秒。

1
2
3
4
5
6
7
8
public Page<Record> getData() {
......
Page<Record> result = Db.paginate(pageNum, numPerPage, select, exceptSelect);
result.getList().parallelStream().forEach(r -> {
display(r);
});
return result;
}

前端

针对分析中的三点,我们分别使用图片懒加载和修复watch后,页面打卡耗时大约为10秒,且不在出现卡死现象。

优化后的watch如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
.......
watch: {
value: {
deep: true,
handler: function() {
this.product = this.value
}
}
},
created() {
this.product = this.value
},
methods: {
changeChoosed() {
this.product.choosed = !this.product.choosed
// 向上层发送数据
this.$emit('input', this.product)
}
......
}

对于前端没有优化经验,欢迎大佬指正!

若不吝可点个赞!

这是我参与更文挑战的第8天,活动详情查看: 更文挑战

不积跬步,无以至千里;不积小流,无以成江海。骐骥一跃,不能十步,驽马十驾,功在不舍。锲而舍之,朽木不折;锲而不舍,金石可镂。

上篇分析singleflight的相关资料中,看到有文章说“doCall方法巧妙的使用两个defer来区分调用函数异常与系统异常”。今天查找资料,学习一下Go的异常处理机制,目前的大概印象,只知道一个关键字panic

异常与错误

在Go语言中,错误被认为是一种可以预期的结果;而异常则是一种非预期的结果,发生异常可能表示程序中存在BUG或发生了其它不可控的问题。

Go 中主要通过 error 和 panic 分别表示错误和异常[2]

例如,从一个map查询一个结果时,可以通过额外的布尔值判断是否成功,属于一种预期的结果。

1
2
3
if v, ok := m["key"]; ok {
return v
}

错误

Go中的错误类型:error

1
2
3
type error interface {
Error() string
}

内置的 error 接口使得开发人员可以为错误添加任何所需的信息,error 可以是实现 Error() 方法的任何类型,具体例子可参考[2]。

Go中errors包提供了几个常用的函数,包括errors.New, errors.Is, errors.As, errors.Unwrap ,以及使用fmt.Errorf

erros.Is判断两个error是否相等,error.As判断error是否为特定类型。

使用实例

函数通常可在最后一个返回值中返回错误信息,一个简单的应用实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package main

import (
"errors"
"fmt"
)

func myF(f float64) (float64, error) {
if f < 0 {
return 0, errors.New("Not legal input ")
}
// 实现
return 0.0, nil
}

func main() {
var m map[string]string
m = make(map[string]string)
m["a"] = "2"
_, ok := m["a"]
_, ok2 := m["b"]
fmt.Println(ok) // true
fmt.Println(ok2) // false

_, e := myF(-1)
_, e2 := myF(2)
fmt.Println(e) // Not legal input
fmt.Println(e2) // <nil>
}

异常

defer,panic recover搭配可以处理异常。

defer

当程序出现异常,如数组访问越界这类“意料之外”的错误时,它能够导致程序运行崩溃,此时就需要开发人员捕获异常并恢复程序的正常运行流程。捕获异常不是最终的目的。如果异常不可预测,直接输出异常信息是最好的处理方式[1]。

defer是Go提供的一种延迟执行机制,每次执行 defer,都会将对应的函数压入栈中。在函数返回或者 panic 异常结束时,Go 会依次从栈中取出延迟函数执行。

panic

panic用于主动抛出程序执行的异常,会终止其后将要执行的代码,并依次逆序执行 panic 所在函数可能存在的 defer 函数列表。

recover

recover 关键字主要用于捕获异常,将程序状态从严重的错误中恢复到正常状态。 必须在 defer 函数中才能生效。

下面是一个defer+panic+recover的代码样例,可以看到,在手动panic后,执行了defer中的输出,并且,a的值为0,所以如果函数中有panic语句,name函数应该需要返回一个error

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package main

import "fmt"

func main() {
for i := 0; i < 10; i++ {
a := my(i)
fmt.Println(a)
}
}

func my(i int) int {
defer func() {
if err := recover(); err != nil {
fmt.Println("发生了异常", err)
}
}()
if i != 5 {
return i
} else {
panic("panic")
}
return -1
}

代码输出结果:

1
2
3
4
5
6
0
1
2
发生了异常 panic
0
4

一个处理极端

超级健壮的代码,每个函数开始的地方都添加如下代码:

1
2
3
4
5
6
7
8
func myfunc() {
defer func() {
if err := recover(); err != nil {
fmt.Println(err)
}
}()
// 函数实现....
}

当然,不要总这么做~~~

[1] 错误和异常

[2] 没有 try-catch,该如何处理 Go 错误异常

[3] Go语言中defer的一些坑

[4] Go 语言踩坑记——panic 与 recover

[5] [译] Part 31: golang 中的自定义 error

这是我参与更文挑战的第7天,活动详情查看: 更文挑战

缓存击穿及解决方案中简单介绍了Go使用singleflight解决缓存击穿,查到了singleflight的源码,阅读感受只能说“让我感觉自己不太适合干程序员这项工作~”,今天下决心把singleflight源码搞懂,尤其是前几天一直困惑的singleflight何时存储,何时删除其内部缓存的问题。

而且,其中涉及到了很多Go的并发编程知识。很有必要学习一番。

Go并发的一些知识

WaitGroup 等待线程组

WaitGroup线程同步,指等待一组协程goroutine执行完成后才会继续向下执行。如下为简单的测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package main

import (
"fmt"
"sync"
)

func main() {
var group sync.WaitGroup
group.Add(2)
for i := 0; i < 2; i++ {
go func() {
fmt.Println("other routine finish ")
group.Done()
}()
fmt.Println("i = ", i)
}
group.Wait()
// 将等待两个协程执行完毕后才执行下面语句
fmt.Println("all group routine finish")
}

运行结果如下:

1
2
3
4
5
i =  0
other routine finish
i = 1
other routine finish
all group routine finish

sync.Mutex 互斥锁

Mutex是一个互斥锁,可以创建为其他结构体的字段;零值为解锁状态。Mutex类型的锁和线程无关,可以由不同的线程加锁和解锁[2]。

注意以下几点:

  1. 在一个 goroutine 获得 Mutex 后,其他 goroutine 只能等到这个 goroutine 释放该 Mutex[3]
  2. 已加锁后只能解锁后再加锁
  3. 解锁未加锁的会导致异常
  4. 适用于读写不确定,并且只有一个读或者写的场景[3]

测试代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package main

import (
"fmt"
"sync"
"time"
)

func main() {
wait := sync.WaitGroup{}
var m sync.Mutex
fmt.Println("Main Routine Locked")
m.Lock()

for i := 0; i <=2 ; i++ {
wait.Add(1)
go func(i int) {
fmt.Println(i, " not get lock, waiting...")
m.Lock()
fmt.Println(i, " get lock, doing...")
time.Sleep(time.Second)
fmt.Println(i, " Unlocked")
m.Unlock()
defer wait.Done()
}(i)
}

time.Sleep(time.Second)
fmt.Println("Main Routine Unlocked")
m.Unlock()

wait.Wait()
}

运行结果如下:

1
2
3
4
5
6
7
8
9
10
11
Main Routine Locked
2 not get lock, waiting...
0 not get lock, waiting...
1 not get lock, waiting...
Main Routine Unlocked
2 get lock, doing...
2 Unlocked
0 get lock, doing...
0 Unlocked
1 get lock, doing...
1 Unlocked

singleflight的两个结构体

cal

call保存当前调用对应的信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// call is an in-flight or completed singleflight.Do call
type call struct {
wg sync.WaitGroup

// These fields are written once before the WaitGroup is done
// and are only read after the WaitGroup is done.
// 函数返回值,在wg.Done前只会写入一次,在wg.Done后是只读的。
val interface{}
err error

// forgotten indicates whether Forget was called with this call's key
// while the call was still in flight.
// 标识Forget方法是否被调用
forgotten bool

// These fields are read and written with the singleflight
// mutex held before the WaitGroup is done, and are read but
// not written after the WaitGroup is done.
// 统计调用次数
dups int
// 返回的 channel
chans []chan<- Result
}

Group

1
2
3
4
5
6
7
8
// Group represents a class of work and forms a namespace in
// which units of work can be executed with duplicate suppression.
type Group struct {
// 互斥锁
mu sync.Mutex // protects m
// 映射表,调用key->调用,懒加载,
m map[string]*call // lazily initialized
}

Do方法

通过group.mugroup.m 确保某个时间点只有一个方法进入实际的执行。

通过call.wg确保实际执行的方法执行完毕后后,其他同样的方法可以从call.val获取到同样的数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// Do executes and returns the results of the given function, making
// sure that only one execution is in-flight for a given key at a
// time. If a duplicate comes in, the duplicate caller waits for the
// original to complete and receives the same results.
// The return value shared indicates whether v was given to multiple callers.
// Do执行和返回给定函数的值,确保某一个时间只有一个方法被执行。如果一个重复的请求进入,则重复的请求会等待前一个执行完毕并获取相同的数据,返回值shared标识返回值v是否是传递给重复的调用的
func (g *Group) Do(key string, fn func() (interface{}, error)) (v interface{}, err error, shared bool) {
g.mu.Lock()
if g.m == nil {
// 懒加载,初始化
g.m = make(map[string]*call)
}

// 检查指定key是否已存在请求
if c, ok := g.m[key]; ok {
// 已存在则解锁,调用次数+1,
c.dups++
g.mu.Unlock()

// 然后等待 call.wg(WaitGroup) 执行完毕,只要一执行完,所有的 wait 都会被唤醒
c.wg.Wait()

// 我的Go知识还没学到异常,暂且不表:
// 这里区分 panic 错误和 runtime 的错误,避免出现死锁,后面可以看到为什么这么做[4]
if e, ok := c.err.(*panicError); ok {
panic(e)
} else if c.err == errGoexit {
runtime.Goexit()
}
return c.val, c.err, true
}
// 如果我们没有找到这个 key 就 new call
c := new(call)

// 然后调用 waitgroup 这里只有第一次调用会 add 1,其他的都会调用 wait 阻塞掉
// 所以只要这次调用返回,所有阻塞的调用都会被唤醒
c.wg.Add(1)
g.m[key] = c
g.mu.Unlock()
// 实际执行fn
g.doCall(c, key, fn)
return c.val, c.err, c.dups > 0
}

doCall

由于本人Go的知识面还没有覆盖到Go的异常部分,其对异常的处理暂且不表,借用文章与代码中的注释的说法:使用了两个 defer 巧妙的将 runtime 的错误和我们传入 function 的 panic 区别开来避免了由于传入的 function panic 导致的死锁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
// doCall handles the single call for a key.
func (g *Group) doCall(c *call, key string, fn func() (interface{}, error)) {
// 表示方法是否正常返回
normalReturn := false
recovered := false

// use double-defer to distinguish panic from runtime.Goexit,
// more details see https://golang.org/cl/134395
defer func() {
// the given function invoked runtime.Goexit
// 如果既没有正常执行完毕,又没有 recover 那就说明需要直接退出了
if !normalReturn && !recovered {
c.err = errGoexit
}

c.wg.Done()
g.mu.Lock()
defer g.mu.Unlock()
// 如果已经 forgot 过了,就不要重复删除这个 key 了
if !c.forgotten {
delete(g.m, key)
}

// 下面应该主要是异常处理的diamante
if e, ok := c.err.(*panicError); ok {
// In order to prevent the waiting channels from being blocked forever,
// needs to ensure that this panic cannot be recovered.
if len(c.chans) > 0 {
go panic(e)
select {} // Keep this goroutine around so that it will appear in the crash dump.
} else {
panic(e)
}
} else if c.err == errGoexit {
// Already in the process of goexit, no need to call again
} else {
// Normal return
for _, ch := range c.chans {
ch <- Result{c.val, c.err, c.dups > 0}
}
}
}()

func() {
// 使用一个匿名函数来执行实际的fn
defer func() {
if !normalReturn {
// Ideally, we would wait to take a stack trace until we've determined
// whether this is a panic or a runtime.Goexit.
//
// Unfortunately, the only way we can distinguish the two is to see
// whether the recover stopped the goroutine from terminating, and by
// the time we know that, the part of the stack trace relevant to the
// panic has been discarded.
if r := recover(); r != nil {
c.err = newPanicError(r)
}
}
}()

// 方法实际执行,将值存在c.val中
c.val, c.err = fn()
normalReturn = true
}()

if !normalReturn {
recovered = true
}
}

流程概述图

singleflight4.jpg

参考资料:

[1] 一篇带给你Go并发编程Singleflight https://developer.51cto.com/art/202103/652064.htm

[2] package sync.Mutex

[3] Go 标准库 —— sync.Mutex 互斥锁

[4] Go并发编程(十二) Singleflight

这是我参与更文挑战的第6天,活动详情查看: 更文挑战

问题起源与某微信群大佬说自己原本支持singleflight的代码被改了~

本着程序员能有不掌握的技术,不能有不了解的名词,去搜索singleflight~进而搜索到“缓存击穿”这个名词,相关名词还有缓存雪崩

概念

先来了解一下名词的概念:

缓存击穿:指热点key在某个时间点过期的时候,而恰好在这个时间点对这个Key有大量的并发请求过来,从而大量的请求打到db[1]。此时缓存起不到作用,就像被“击穿”了。缓存击穿会引起数据库瞬间压力增大。

缓存雪崩:指缓存中数据大批量到过期时间,而查询数据量巨大,引起数据库压力过大甚至down机。

解决方案

互斥锁

当缓存失效时,给从数据库取数据添加一个锁,只能有一个线程(线程A)可以进去读取数据库,其他线程等待;当线程A读取数据库数据成功后,将数据更新到缓存中,其他等待线程直接去读取缓存获取数据。Go的singleflight用的就是这种思想。我们首先贴一段Java-redis的代码:

其中的redis.setnx方法为加锁。

setnx, SET if Not eXists 的缩写。只在键 key 不存在的情况下, 将键 key 的值设置为 value 。若键 key 已经存在, 则 SETNX 命令不做任何动作。命令在设置成功时返回 1 , 设置失败时返回 0

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
String get(String key) {  
String value = redis.get(key);
if (value == null) {
if (redis.setnx(key_mutex, "1") == 1) {
redis.expire(key_mutex, 3 * 60)
value = db.get(key);
redis.set(key, value);
redis.delete(key_mutex);
} else {
//其他线程休息50毫秒后重试
Thread.sleep(50);
get(key);
}
}
}

Go的singleflight采用的也是这种互斥锁的方案,实现上singleflight使用一个临时的map存储第一个协程读取数据库的数据,后续方法直接从map中获取,连缓存都不用读。singleflight全部代码并不多,其github地址。

其源代码可在Go安装路径,如C:\Program Files\Go\src\internal\singleflight查看。

异步构建缓存

简单来说,就是获取始终从缓存获取;缓存中存储数据与过期时间,每次过期后启动另外的线程获取数据更新缓存。无法保证数据的一致性,但大多数情况下可以满足需求。

布隆过滤器

布隆过滤器的作用是能够迅速判断一个元素是否在一个集合

应用布隆过滤器到缓存击穿中,就是维护一个数据库key的集合,每次请求,首先取缓存中获取数据,缓存中没有则通过布隆过滤器判断查询的key是否可能在数据库中,若不在则不请求数据库[3]。

[1]缓存击穿-百度百科,https://baike.baidu.com/item/%E7%BC%93%E5%AD%98%E5%87%BB%E7%A9%BF

[2]https://silenceper.com/blog/202003/singleflight/

[3]https://www.cnblogs.com/wangwust/p/9467720.html

这是我参与更文挑战的第5天,活动详情查看: 更文挑战

生产场景下,因为已知或未知的因素,经常需要我们在生产服务器上查找与定位Bug,日志必不可少。如何方便快捷的在本地下载服务器文件呢。

  1. 使用工具软件,如XFTP,拖动文件下载。下载灵活,但操作繁琐,每次都需要跳转到指定路径去找文件。
  2. 使用本地脚本下载文件:

下载文件

第三方工具包的选择:

paramiko是一个python库,实现了SSH协议,官方文档地址:http://docs.paramiko.org/en/stable/。

paramiko有两个核心组件,SSHClient 与 SFTPClient,一个用于远程执行命令,一个用于文件传输。

如下面代码是典型的采用SFTPClient进行文件下载的样例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 下载文件
def downloadLog(thread_name, host, port, username, password, remote_filepath, local_filepath):
# open a transport
tranport = paramiko.Transport((host, port))
# auth
tranport.connect(None, username, password)
# go!
sftp = paramiko.SFTPClient.from_transport(tranport)
# download
# remote_filepath = '/root/test.txt'
# local_filepath = 'D:\\xxxx\\test1.txt'
# callback是回调函数,用于更新下载进度,后文会说
sftp.get(remote_filepath, local_filepath, callback=callback)
# close
if sftp:
sftp.close()
if tranport:
tranport.close()
print(local_filepath + ': 下载完成', end='\n')

显示下载进度

显示进度主要是通过print函数的end参数实现的。

print函数默认换行,是end='\n'在起作用,我们设置end=''测试出每次打印都会从本行头开始。

1
2
3
4
5
6
def callback(now, total, length=30, prefix='进度:', thread_name='', number=0):
nowSum[threading.current_thread().getName()] = now
totalSum[threading.current_thread().getName()] = total
print(
'\r文件数量:3, ' + '文件总大小:{}, '.format(str(format(sum(totalSum.values()) / 1024 / 1024, '.2f')) + 'M') + prefix + '{:.2%}\t'.format((sum(nowSum.values()) / sum(totalSum.values())))
+ '[' + '>' * int(sum(nowSum.values()) / sum(totalSum.values()) * length) + '-' * int(length - sum(nowSum.values()) / sum(totalSum.values()) * length) + ']', end='')

优化

通常,我们定位问题,需要查找指定字符,可以不需要下载后在本地查找,通过grep命令,完成在服务器端查找后,下载筛选后的内容,可以减少传输文件大小。

这里我们没有使用SFTPClient,而是使用了SSHClient,而是直接将命令输出内容写到本文文件中。

其实这里我们也可以采用SFTPClient,首先将grep查询内容输出到服务器的指定文件中,然后再将文件下载到本地。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def downloadGrepLog(thread_name, host, port, username, password, remote_filepath, local_filepath, grep_str):
# 按照指定字符串,筛选其前2行至后20行的内容,并输出到本地
command = 'grep -A 20 -B 2 "' + grep_str + '" ' + remote_filepath

s = paramiko.SSHClient()
s.set_missing_host_key_policy(paramiko.AutoAddPolicy())
s.connect(host, port, username, password)

print('grep命令' + command)
# 这里我们没有使用SFTPClient,而是直接将命令输出内容写到本文文件中
(stdin, stdout, stderr) = s.exec_command(command)
f = open(local_filepath,"w+")
for line in stdout:
f.write(line)
s.close()
print(local_filepath + ': 下载完成', end='\n')

完整代码

该程序的优点在于:

  1. 多日志多线程下载;
  2. 实时查看下载进度;
  3. 可使用grep命令筛选日志下载
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
import paramiko
import _thread
import os
import threading
import time

threadNames = ['1号服务器', '2号服务器', '3号服务器']
nowSum = {} # 当前下载进度
totalSum = {} # 文件总大小
for threadname in threadNames:
nowSum[threadname] = 0
totalSum[threadname] = 0

def callback(now, total, length=30, prefix='进度:', thread_name='', number=0):
nowSum[threading.current_thread().getName()] = now
totalSum[threading.current_thread().getName()] = total
print(
'\r文件数量:3, ' + '文件总大小:{}, '.format(str(format(sum(totalSum.values()) / 1024 / 1024, '.2f')) + 'M') + prefix + '{:.2%}\t'.format((sum(nowSum.values()) / sum(totalSum.values())))
+ '[' + '>' * int(sum(nowSum.values()) / sum(totalSum.values()) * length) + '-' * int(length - sum(nowSum.values()) / sum(totalSum.values()) * length) + ']', end='')

# time.strftime('%Y-%m-%d', time.localtime(time.time()))

# 提示输入日期
dateStr = input('请输入日期,格式为yyyy-mm-dd(默认今天):')
grep_str = input('请输入查询内容(默认全部):')

today = time.strftime('%Y-%m-%d', time.localtime(time.time()))
filename = 'MyProject.log'
if not dateStr:
dateStr = today
if today != dateStr:
# 服务器日志滚动格式为: MyPrject.log_yyyy-MM-dd.log
filename = 'MyProject.log_' + dateStr + '.log'

# 若拷贝自用,需要将服务器地址修改为自己的服务器地址,端口,用户名,密码与日志文件的路径
server1 = 'xxx.xxx.xxx.xx'
server1port = 22
server1username = 'xx'
server1password = 'xxxxxx'
server1remotefilepath = '/usr/local/....'
server1localfilepath = 'D:\\xxx\\log\\' + dateStr + '\\'+ grep_str +'$1\\'
server2 = 'xxx.xxx.xxx.xx'
server2port = 22
server2username = 'xxxxxx'
server2password = 'xxx'
server2remotefilepath = '/usr/local/....'
server2localfilepath = 'D:\\xxx\\log\\' + dateStr + '\\'+ grep_str +'$2\\'
server2remotefilepath2 = '/usr/local/....'
server2localfilepath2 = 'D:\\work\\log\\' + dateStr + '\\'+ grep_str +'$3\\'

def getFileDirPath(filepath):
print(filepath[0:filepath.rfind('\\')])

def getFileName(filepath):
print(filepath[filepath.rfind('\\'):])

def downloadLog(thread_name, host, port, username, password, remote_filepath, local_filepath):
# open a transport
tranport = paramiko.Transport((host, port))
# auth
tranport.connect(None, username, password)
# go!
sftp = paramiko.SFTPClient.from_transport(tranport)
# download
# remote_filepath = '/root/test.txt'
# local_filepath = 'D:\\xxxx\\test1.txt'
sftp.get(remote_filepath, local_filepath, callback=callback)
# close
if sftp:
sftp.close()
if tranport:
tranport.close()
print(local_filepath + ': 下载完成', end='\n')

def downloadGrepLog(thread_name, host, port, username, password, remote_filepath, local_filepath, grep_str):
command = 'grep -A 20 -B 2 "' + grep_str + '" ' + remote_filepath

s = paramiko.SSHClient()
s.set_missing_host_key_policy(paramiko.AutoAddPolicy())
s.connect(host, port, username, password)

print('grep命令' + command)
(stdin, stdout, stderr) = s.exec_command(command)
f = open(local_filepath,"w+")
for line in stdout:
f.write(line)
s.close()
print(local_filepath + ': 下载完成', end='\n')


class downloadThread(threading.Thread):
def __init__(self, name, host, port, username, password, remote_filepath, local_filepath, grep_str):
threading.Thread.__init__(self)
self.name = name
self.host = host
self.port = port
self.username = username
self.password = password
self.remote_filepath = remote_filepath
self.local_filepath = local_filepath
self.grep_str = grep_str
def run(self):
# 注释代码主要完成若已下载过log,则创建新的目录去下载
# local_file_dir_path = getFileDirPath(self.local_filepath)
# log_filename = getFileName(self.local_filepath)
# while os.path.exists(local_file_dir_path):
# # 文件夹已存在
# lastFileDirName = getFileName(local_file_dir_path)
# local_file_dir_path = getFileDirPath(local_file_dir_path)
# local_file_dir_path = local_file_dir_path
# + '\\'
# + time.strftime('%Y-%m-%d_%H_%M_%S', time.localtime(time.time())) + '\\' + lastFileDirName
# if not os.path.exists(local_file_dir_path):
# os.mkdir(local_file_dir_path)
# self.local_filepath = local_file_dir_path + '\\' + log_filename
if not self.grep_str:
downloadLog(self.name, self.host, self.port, self.username, self.password, self.remote_filepath, self.local_filepath)
else:
downloadGrepLog(self.name, self.host, self.port, self.username, self.password, self.remote_filepath, self.local_filepath,self.grep_str)

if not os.path.exists(server1localfilepath):
os.makedirs(server1localfilepath)
if not os.path.exists(server2localfilepath):
os.makedirs(server2localfilepath)
if not os.path.exists(server2localfilepath2):
os.makedirs(server2localfilepath2)

thread1 = downloadThread('1号服务器', server1, server1port, server1username, server1password, server1remotefilepath + filename,
server1localfilepath + filename, grep_str)
thread2 = downloadThread('2号服务器-1', server2, server2port, server2username, server2password, server2remotefilepath + filename,
server2localfilepath + filename, grep_str)
thread3 = downloadThread('2号服务器', server2, server2port, server2username, server2password, server2remotefilepath2 + filename,
server2localfilepath2 + filename, grep_str)

thread1.setDaemon(True)
thread1.start()

thread2.setDaemon(True)
thread2.start()

thread3.setDaemon(True)
thread3.start()

thread1.join()
thread2.join()
thread3.join()


print('主线程结束:')

运行效果

image.png

4b9f17d033134a9682ca2546c39b96a3_tplv-k3u1fbpfcp-watermark.jpg

57e035cd66ea4f748423f7b639771044_tplv-k3u1fbpfcp-watermark.jpg

这是我参与更文挑战的第4天,活动详情查看: 更文挑战

工作环境主要是Java,听说Go挺快的。前些天参加掘金Go主题月学习了一些基本语法,刷了些题,现在尝试着使用Go的web框架做一些基本服务。

并不一定选用Go,主要是因为有别的同事用Gin写过小服务,因此,如果Gin不错,或许可以在同事间推广一下,毕竟,Go与C很像,学习蛮简单的。

Gin的相关资料

源码地址:https://github.com/gin-gonic/gin, https://gitee.com/github-image/gin-gonic-gin (国内可用)

官网文档地址:https://gin-gonic.com/zh-cn/docs/,支持中文

Gin关键词:Web框架,httprouter, 快速,支持中间件,Crash处理,JSON验证,路由组,错误处理,内置渲染,可扩展行。

上手

Gin要求Go 1.13及以上,检查Go的版本:

1
2
go version
// output: go version go1.16.2 windows/amd64
不支持Go-mod的方法

更推荐大家使用Go-mod方法。

安装引入Gin,net/http包

1
2
go get -u github.com/gin-gonic/gin
// go: downloading xxxx.......

创建.go代码文件,并按照官方教程拷贝一个模板文件:

1
2
3
curl https://raw.githubusercontent.com/gin-gonic/examples/master/basic/main.go > main.go
// curl: The term 'curl' is not recognized as a name of a cmdlet, function, script file, or executable program.
// Check the spelling of the name, or if a path was included, verify that the path is correct and try again.

windows powershell不支持curl命令,我们手动拷贝,main.go代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
package main

import (
"net/http"

"github.com/gin-gonic/gin"
)

var db = make(map[string]string)

func setupRouter() *gin.Engine {
// Disable Console Color
// gin.DisableConsoleColor()
r := gin.Default()

// Ping test
r.GET("/ping", func(c *gin.Context) {
c.String(http.StatusOK, "pong")
})

// Get user value
r.GET("/user/:name", func(c *gin.Context) {
user := c.Params.ByName("name")
value, ok := db[user]
if ok {
c.JSON(http.StatusOK, gin.H{"user": user, "value": value})
} else {
c.JSON(http.StatusOK, gin.H{"user": user, "status": "no value"})
}
})

// Authorized group (uses gin.BasicAuth() middleware)
// Same than:
// authorized := r.Group("/")
// authorized.Use(gin.BasicAuth(gin.Credentials{
// "foo": "bar",
// "manu": "123",
//}))
authorized := r.Group("/", gin.BasicAuth(gin.Accounts{
"foo": "bar", // user:foo password:bar
"manu": "123", // user:manu password:123
}))

/* example curl for /admin with basicauth header
Zm9vOmJhcg== is base64("foo:bar")

curl -X POST \
http://localhost:8080/admin \
-H 'authorization: Basic Zm9vOmJhcg==' \
-H 'content-type: application/json' \
-d '{"value":"bar"}'
*/
authorized.POST("admin", func(c *gin.Context) {
user := c.MustGet(gin.AuthUserKey).(string)

// Parse JSON
var json struct {
Value string `json:"value" binding:"required"`
}

if c.Bind(&json) == nil {
db[user] = json.Value
c.JSON(http.StatusOK, gin.H{"status": "ok"})
}
})

return r
}

func main() {
r := setupRouter()
// Listen and Server in 0.0.0.0:8080
r.Run(":8080")
}

然后按照代码错误提示,手动将对应目录的软件包拷贝至$GOPATH/src目录下。重申:更推荐大家使用Go-mod方法。

支持Go-mod的方法

cd到工作目录下之后:

1
2
3
4
cd D:\lean_space\gin
// 拷贝文件
go mod init mygin.web
go mod tidy

开始运行:

1
go run main.go

打开postman获取浏览器,输入网址:http://localhost:8080/,显示如下:

1
404 page not found

我们看到代码中有如下代码,猜测路由监听路径有/ping,正如我们所料,获取了字符串pong。

1
2
3
r.GET("/ping", func(c *gin.Context) {
c.String(http.StatusOK, "pong")
})

我们分别按照如下方式发起请求:

  1. GET http://localhost:8080/ping 返回 pond

  2. POST http://localhost:8080/admin header中配置
    { authorization: “ Basic Zm9vOmJhcg==”, content-type: “application/json” }
    Postman-body-raw配置: {“value”:”bar”}

    1
    2
    3
    {
    "status": "ok"
    }
  3. GET http://localhost:8080/user/foo

    1
    2
    3
    4
    {
    "user": "foo",
    "value": "bar"
    }

测试成功!

控制台输出如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
PS D:\lean_space\gin> go run main.go
[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.

[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
- using env: export GIN_MODE=release
- using code: gin.SetMode(gin.ReleaseMode)

[GIN-debug] GET /ping --> main.setupRouter.func1 (3 handlers)
[GIN-debug] GET /user/:name --> main.setupRouter.func2 (3 handlers)
[GIN-debug] POST /admin --> main.setupRouter.func3 (4 handlers)
[GIN-debug] Listening and serving HTTP on :8080
[GIN] 2021/06/03 - 21:35:31 |?[97;42m 200 ?[0m| 0s | ::1 |?[97;46m POST ?[0m "/admin"
[GIN] 2021/06/03 - 21:35:35 |?[97;42m 200 ?[0m| 0s | ::1 |?[97;44m GET ?[0m "/ping"
[GIN] 2021/06/03 - 21:35:39 |?[97;42m 200 ?[0m| 0s | ::1 |?[97;44m GET ?[0m "/user/foo"

我们仿照其他接口,写一个我们自己的接口,接口路径为:/hellogin,代码如下:

1
2
3
r.GET("/hellogin", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"result": "hello, go-gin! hello go Web!"})
})

测试结果如下图:

image.png

显然,r.GET("/xxx")表示该方法可以服务于访问路径/xxx的HTTP-GET请求。

c.JSON表示返回的数据格式是JSON。查阅资料发现,gin支持XML/JSON/YAML/ProtoBuf 渲染,也支持JSONP,PureJSON, SecureJson等。

https://gin-gonic.com/zh-cn/docs/examples/中可以找到现成的gin-web实例以解决实际生产问题。

习惯了Java那种巨型架构之后,使用Gin的体验就是,这就真的可以了吗?这样跑到生产环境没问题吧~~~

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
package cn.kevinq.article.stream;

import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Random;
import java.util.stream.Collectors;
import java.util.stream.DoubleStream;
import java.util.stream.IntStream;
import java.util.stream.LongStream;
import java.util.stream.Stream;

public class SteamTest {

public static void main(String[] args) {
// 辅助初始化
List<String> list = new ArrayList<String>();
list.add("a");
list.add("b");
String[] array = new String[] {"a", "b", "c"};


// https://www.baeldung.com/java-8-streams
/**
* Stream初始化
*/
// 空流
Stream<String> emptyStream = Stream.empty(); // []
// Collection(List等), Array(数组)转为Stream
Stream<String> listToStream = list.stream(); // [a, b]
Stream<String> arrayToStream = Stream.of(array); // [a, b, c]
Stream<String> arrayToStream2 = Stream.of("a", "b", "c"); // [a, b, c]
// 取数组的一部分,[1, 3)
Stream<String> arrayToStream3 = Arrays.stream(array, 1, 3); // [b, c]
// builder()创建
Stream<String> streamBuilder = Stream.<String>builder().add("a").add("b").add("c").build(); // [a, b, c]
// generate(func),无限顺序无序流,使用时需要指定长度,否则会一直生成知道占满内存
Stream<String> streamGenrated = Stream.<String>generate(() -> {
Random random = new Random();
return "" + random.nextInt(100);
}).limit(10); // [45, 44, 51, 60, 62, 3, 57, 48, 80, 27]
// iterate(Element(1), Element(n) -> Element(n+1))), 有序无限连续流,指定第一元素与生成后续元素的方法
Stream<String> streamIterate = Stream.<String>iterate("a", e -> {
return (char) (e.charAt(e.length()-1) + 1) + "";
}).limit(26); // [a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, v, w, x, y, z]
// int,long,double有各自的封装良好的流IntStream, LongStream, DoubleStream
IntStream intStream = IntStream.rangeClosed(1, 10);// [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
LongStream longStream = LongStream.range(1, 5); // [1, 2, 3, 4]
Random random = new Random();
DoubleStream doubleStream = random.doubles(3); // [0.8078472890889652, 0.33210715318105766, 0.9277914692743904]
DoubleStream doubleStream2 = DoubleStream.iterate(1l, n -> n + 1l).limit(10); //[1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0]

System.out.println(emptyStream.collect(Collectors.toList()));
// System.out.println(listToStream.collect(Collectors.toList()));
// Stream无法复用,下面一行代码会报错:java.lang.IllegalStateException: stream has already been operated upon or closed
// System.out.println(listToStream.collect(Collectors.toList()));
System.out.println(arrayToStream.collect(Collectors.toList()));
System.out.println(arrayToStream2.collect(Collectors.toList()));
System.out.println(arrayToStream3.collect(Collectors.toList()));
System.out.println(streamBuilder.collect(Collectors.toList()));
System.out.println(streamGenrated.collect(Collectors.toList()));
System.out.println(streamIterate.collect(Collectors.toList()));
System.out.println(intStream.boxed().collect(Collectors.toList()));
System.out.println(longStream.boxed().collect(Collectors.toList()));
System.out.println(doubleStream.boxed().collect(Collectors.toList()));
System.out.println(doubleStream2.boxed().collect(Collectors.toList()));

// 中间操作
// distinct()
// filter
// skip
// limit(long maxSize)
// map 给定函数应用于此流的元素的结果组成的流
// flatMap 通过将提供的映射函数应用于每个元素而产生的映射流的内容来替换该流的每个元素的结果的流。
// sorted



// 终止操作
// reduce

// 其他操作
// count() 此流中的元素数
// findAny() 返回某个元素的Optional
// findFirst() 返回第一个元素的Optional
// forEach(element -> action) 对此流的每个元素执行操作


System.out.println(listToStream.findFirst().orel);

}

/**
* 无限Stream。
* 该方法返回无限素数Stream
* // 前20个素数
primes().limit(20).forEach(System.out::println);
* @return
*/
static Stream<BigInteger> primes() {
return Stream.iterate(BigInteger.ONE.add(BigInteger.ONE), BigInteger::nextProbablePrime);
}

}

这是我参与更文挑战的第3天,活动详情查看: 更文挑战

微博上看到的一个关于javascript的集合操作函数解释,很有意思。

1
2
3
4
5
6
7
[■,■,■,■].map(■→●) => [●,●,●,●]
[■,●,■,▲].filter(■→true) => [■,■]
[■,●,■,▲].find(●→true) => ●
[■,●,■,▲].findIndex(●→true) => 1
[■,●,■,▲].fill(●) => [●,●,●,●]
[■,●,■,▲].some(●→true) => true
[■,●,■,▲].every(●→true) => false

Stream的操作分为中间操作与终止操作。

每个中间操作都通过某种方式对Stream进行转换(映射,过滤等等),将一个Stream转换成另一个Stream,其元素类型可能相同,也可能不同。

终止操作会在最后一个中间操作产生的Stream上执行一个最终计算,如打印,保存到集合中,或返回某个元素。

Stream流管道操作通常是lazy的,直到调用终止操作时才会开始计算,对于完成终止操作不需要的数据元素,将永远都不会被计算。这使得无限Stream成为可能。

无限Stream的一个样例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 无限Stream。
* 该方法返回无限素数Stream
* @return
*/
static Stream<BigInteger> primes() {
return Stream.iterate(BigInteger.ONE.add(BigInteger.ONE), BigInteger::nextProbablePrime);
}

/**
* 打印出前20个素数
*/
primes().limit(20).forEach(System.out::println);

【Effective Java】[第三版]第45条,谨慎使用Stream,滥用Stream会使程序代码难以读懂和维护。

Stream支持对象引用和int,long与double。不支持char。最好避免利用Stream来处理char。

Stream适合以下工作:

  1. 统一转换元素的序列
  2. 过滤元素的序列
  3. 利用单个操作(如添加、连接或者计算其最小值)合并元素的顺序
  4. 将元素的序列存放到一个集合中,比如根据某些公共属性进行分组
  5. 搜索满足某些条件的元素的序列

类似文章开头的JavaScript,我们列举一下Stream中的一些操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
[■,■,■,■].map(■→●) => [●,●,●,●]
[[■, ■], [■, ■]].flatMap([■, ■] -> stream([■, ■])) => [■, ■, ■, ■]
[■,■,■,■].forEach(■→●) => [●,●,●,●]
[■,●,■,▲].filter(■→true) => [■,■]
[■,●,■,▲].limit(2) => [■,●]
[4,5,2,1].sorted() => [1,2,4,5]
[■,●,■,▲].filter(■→true) => [■,■]
[■,●,■,▲].collect(groupingBy(type)) => [[■,■], [●], [▲]]
[.......].collect(Collectors.toList()) // 各种聚合操作, 具体查看Collectors

// 欢迎补充


不管是lambda,还是Stream,都是为了让代码更加易读,代码最终还是写给人看的。