开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

Java注解概要

发表于 2021-11-28

什么是注解

在各种框架如Spring,Springboot中,我们经常能看到各种注解,比如@Autowired,@ControllerAdvice等等,如果我们想要深入得去了解其中得内容的话,注解是一个必须要了解的知识。

Java注解又称为标注,是Java从1.5开始支持加入源码的特殊语法元数据;Java中的类、方法、变量、参数、包都可以被注解,在java.lang.annotation.Annotation接口中有这样的一句话,用来描述注解:
The common interface extended by all annotation types 所有的注解类型都继承自这个普通的接口(Annotation)
以重载@Override这样的注解为例

1.PNG
我们从左边的结构中可以看到,Override所有的方法都是继承自Annotation这样的接口,我们可以暂时这样定义Java中的注解:

一个注解准确意义上来说,只不过是一种特殊的注释而已,这种注释都是继承自Annotation接口中。

既然是注释,那么需要对这种注解进行处理,处理的方式主要有以下两种:

一种是编译期直接的扫描,一种是运行期反射。反射的事情我们待会说,而编译器的扫描指的是编译器在对 java 代码编译字节码的过程中会检测到某个类或者方法被一些注解修饰,这时它就会对于这些注解进行某些处理。

典型的就是注解 @Override,一旦编译器检测到某个方法被修饰了 @Override 注解,编译器就会检查当前方法的方法签名是否真正重写了父类的某个方法,也就是比较父类中是否具有一个同样的方法签名。

这一种情况只适用于那些编译器已经熟知的注解类,比如 JDK 内置的几个注解,而你自定义的注解,编译器是不知道你这个注解的作用的,当然也不知道该如何处理,往往只是会根据该注解的作用范围来选择是否编译进字节码文件,仅此而已。

元注解

对于Override上的注解,可能会好奇他们是用来做什么的,这些注解被称为元注解,是一种修饰注解的注解。
Java中主要有以下几个元注解:

  1. @Target:注解的作用目标,通过ElementType来进行声明,可以表示当前注解都可以放在哪些对象上,如类,属性,方法,构造器等
  2. @Retention:注解的生命周期
    这里的 RetentionPolicy 依然是一个枚举类型,它有以下几个枚举值可取:
* RetentionPolicy.SOURCE:当前注解编译期可见,不会写入 class 文件
* RetentionPolicy.CLASS:类加载阶段丢弃,会写入 class 文件
* RetentionPolicy.RUNTIME:永久保存,可以反射获取
  1. @Documented:注解是否应当被包含在 JavaDoc 文档中
  2. @Inherited:是否允许子类继承该注解

Java内置三大注解

除去上述四种元注解以外,Java还为我们预定义了另外三种注解

  • @Override
  • @Deprecated
  • @SuppressWarnings

@Override注解

如上文所示,@Override的注解如下所示:

1
2
3
4
less复制代码@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface Override {
}

它没有任何的属性,所以并不能存储任何其他信息。它只能作用于方法之上,编译结束后将被丢弃。

所以你看,它就是一种典型的『标记式注解』,仅被编译器可知,编译器在对 java 文件进行编译成字节码的过程中,一旦检测到某个方法上被修饰了该注解,就会去匹对父类中是否具有一个同样方法签名的函数,如果不是,自然不能通过编译。

@Deprecated注解

1
2
3
4
5
less复制代码@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(value={CONSTRUCTOR, FIELD, LOCAL_VARIABLE, METHOD, PACKAGE, PARAMETER, TYPE})
public @interface Deprecated {
}

依然是一种『标记式注解』,永久存在,可以修饰所有的类型,作用是,标记当前的类或者方法或者字段等已经不再被推荐使用了,可能下一次的 JDK 版本就会删除。

当然,编译器并不会强制要求你做什么,只是告诉你 JDK 已经不再推荐使用当前的方法或者类了,建议你使用某个替代者。

@SuppressWarnings

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
less复制代码@Target({TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE})
@Retention(RetentionPolicy.SOURCE)
public @interface SuppressWarnings {
/**
* The set of warnings that are to be suppressed by the compiler in the
* annotated element. Duplicate names are permitted. The second and
* successive occurrences of a name are ignored. The presence of
* unrecognized warning names is <i>not</i> an error: Compilers must
* ignore any warning names they do not recognize. They are, however,
* free to emit a warning if an annotation contains an unrecognized
* warning name.
*
* <p> The string {@code "unchecked"} is used to suppress
* unchecked warnings. Compiler vendors should document the
* additional warning names they support in conjunction with this
* annotation type. They are encouraged to cooperate to ensure
* that the same names work across multiple compilers.
* @return the set of warnings to be suppressed
*/
String[] value();
}

那注解怎么用呢

之前我们说过,注解类似于一个标签,是贴在程序代码上供另一个程序读取的,所以我们需要定义注解,使用注解,读取注解,接下来自定义一个Junit类似的组件,来尝试下注解的基本使用方法。
可以参考github以下地址,通过定义MyAfter,MyBefore注解模拟方法执行的先后顺序
github.com/TakeatEasy/…

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

java动态代理 一、代理模式 二、静态代理 三、动态代理

发表于 2021-11-28

一、代理模式

为其他对象提供一个代理以控制对某个对象的访问,代理类负责在被代理类执行服务前后进行预处理和后置处理,真实的服务提供者仍是被代理类。
比如一个用户类具有登陆功能,可以提供一个代理类,在不修改源代码的情况下给其加上权限认证。

二、静态代理

静态代理就是一个代理类只能为一个被代理类提供服务,以明星和经纪人举例,明星(被代理类)本人有收钱的行为,有了经纪人(代理类)就可以委托经纪人(代理类)进行收款,在此基础上经纪人可以在收款前发送卡号,收款后发送确认信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码class Singer implements Person{  //被代理类明星
@Override
public void collectMoney() {
System.out.println("明星本人收钱");
}
}
class SingerProxy implements Person{ //代理类 (可以理解为明星经纪人)
private Person person=new Singer();
@Override
public void collectMoney() {
System.out.println("Before invoke collectMoney");
person.collectMoney();
System.out.println("After invoke collectMoney");
}
}
1
2
3
4
5
java复制代码public static void main(String[] args) {
//静态代理
SingerProxy singerProxy=new SingerProxy();
singerProxy.collectMoney();
}

运行结果:Before invoke collectMoney 指在执行收钱行为之前一系列预处理操作

After invoke collectMoney指在收钱后一系列后置操作

1.png

三、动态代理 Proxy

java.lang.reflect.Proxy 中提供了Proxy类来实现动态代理。动态代理可以实现一个代理类代理多个被代理类

提供了下面这一获取代理实例的静态方法

static Object newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)

参数描述:

1
2
3
makefile复制代码loader:定义代理类的类加载器
interfaces:代理类要实现的接口列表
h:指派方法调用的调用处理程序

java.lang.reflect包中提供了一个接口InvocationHandler

Object invoke(Object proxy, Method method, Object[] args)

参数描述:

proxy - 在其上调用方法的代理实例

method - 对应于在代理实例上调用的接口方法的 Method 实例。 Method 对象的声明类将是在其中声明方法的接口,该接口可以是代理类赖以继承方法的代理接口的超接口。

args - 包含传入代理实例上方法调用的参数值的对象数组,如果接口方法不使用参数,则为 null。基本类型的参数被包装在适当基本包装器类(如 java.lang.Integer 或 java.lang.Boolean)的实例中

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
java复制代码interface Person{
void collectMoney();
}
class Singer1 implements Person{ //被代理类明星
@Override
public void collectMoney() {
System.out.println("明星1本人收钱");
}
}

class Singer2 implements Person{ //被代理类明星
@Override
public void collectMoney() {
System.out.println("明星2本人收钱");
}
}

interface Animal{
void eat();
}
class Cat implements Animal{

@Override
public void eat() {
System.out.println("猫吃饭");
}
}

//动态代理
class ProxyHandler implements InvocationHandler{
private Object object;
public ProxyHandler(Object object){
this.object=object;
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("Before invoke"+method.getName());
method.invoke(object,args);
System.out.println("After invoke"+method.getName());
return null;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码public static void main(String[] args) {
//动态代理
Person singer1=new Singer1();//被代理类singer1
Person singer2=new Singer2();//被代理类singer2
Animal cat=new Cat();//被代理类cat
ProxyHandler proxyHandler1 = new ProxyHandler(singer1);//创建singer1的代理类的处理程序
ProxyHandler proxyHandler2 = new ProxyHandler(singer2);//创建singer2的代理类的处理程序
ProxyHandler proxyHandler3 = new ProxyHandler(cat);//创建singer3的代理类的处理程序

Person singer1Proxy = (Person)Proxy.newProxyInstance(singer1.getClass().getClassLoader(), singer1.getClass().getInterfaces(), proxyHandler1);//创建singer1的代理类实例
Person singer2Proxy = (Person)Proxy.newProxyInstance(singer2.getClass().getClassLoader(),singer2.getClass().getInterfaces(),proxyHandler2);//创建singer2的代理类实例
Animal catProxy = (Animal)Proxy.newProxyInstance(cat.getClass().getClassLoader(), cat.getClass().getInterfaces(), proxyHandler3);//创建singer3的代理类实例

singer1Proxy.collectMoney();
singer2Proxy.collectMoney();
catProxy.eat();
}

运行结果:

1.png

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

【算法攻坚】回溯模板解题

发表于 2021-11-28

这是我参与11月更文挑战的第27天,活动详情查看:2021最后一次更文挑战

今日题目

给定一个数组 candidates 和一个目标数 target ,找出 candidates 中所有可以使数字和为 target 的组合。

candidates 中的每个数字在每个组合中只能使用一次。

注意:解集不能包含重复的组合。

示例 1:

输入: candidates = [10,1,2,7,6,1,5], target = 8,
输出:
[
[1,1,6],
[1,2,5],
[1,7],
[2,6]
]

示例 2:

输入: candidates = [2,5,2,1,2], target = 5,
输出:
[
[1,2,2],
[5]
]

提示:

1 <= candidates.length <= 100
1 <= candidates[i] <= 50
1 <= target <= 30

思路

这道题相较于组合总和1的区别是:

  • 1、每个元素不能再无限使用,而是只能用一次,因此每次递归的下标不能再从当前小标开始,而是i+1开始。
  • 2、元素有重复元素,因此需要去重。

上述两点都有去重的效果,一个是在结果上保证了没有重复元素,一个是保证在横向遍历的时候保证没有重复搜索。
除此之外与大多数回溯思路一样。

递归遍历数组,每次首先判断当前下标元素是否与前一个元素相同,如果相同同时判断上一个元素是否正在使用,如果没有使用则直接跳过,因为上一个元素前面已经遍历过了。

然后判断当前元素是否小于等于target,如果大于target,则说明如果target减去当前元素差一定小于0,不满足等于0,直接break循环就可以了。因为该元素之后的元素一定是大于等于当前元素的,因此target减去该元素的差也一定是小于0的,所以直接跳过。这是一步非常重要的剪枝操作。

将当前元素加入path列表,将当前vis[i]设置为true表示已访问。

进入递归,判断target是否为0,如果为0,加入答案列表。

出递归,需要进行回溯,将当前元素从path列表中删除,并将访问置为false。

总结成两个问题:

  • 1.如何保证每个数字在每个组合中只使用一次?
    答:每次递归调用方法时,从当前数的下一个数开始
  • 2.如何保证解集不能包含重复的组合?
    答:在有序数组中,如果索引i处的数字 等于 索引i-1处的数字,就直接跳过i,进入下一轮循环

还是使用如下框架代码:

1
2
3
4
5
6
7
8
9
10
java复制代码result = []
backtrack(路径, 选择列表):
if 满足结束条件:
result.add(路径)
return

for 选择 in 选择列表:
做选择
backtrack(路径, 选择列表)
撤销选择

代码实现:

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
java复制代码List<List<Integer>> result = new LinkedList<>();

public List<List<Integer>> combinationSum2(int[] candidates, int target) {
List<Integer> list = new ArrayList<>();
Arrays.sort(candidates);
combinationList(candidates, 0, target, list);
return result;
}

public void combinationList(int[] candidates, int start, int target, List<Integer> list{
if (target == 0) {
result.add(new ArrayList<>(list));
return;
}
for (int i = start; i < candidates.length; i++) {
// 剪裁情况一
if (target - candidates[i] < 0) {
break;
}
// 剪裁情况二
if (i > start && candidates[i] == candidates[i - 1]) {
continue;
}
list.add(candidates[i]);
// 此处为i+1,因为元素不可重复使用,当用start+1时,会导致重复计算
combinationList(candidates, i + 1, target - candidates[i], list);
// 回溯
list.removeLast();
}
}

执行用时:2 ms, 在所有 Java 提交中击败了99.32%的用户

内存消耗:38.4 MB, 在所有 Java 提交中击败了85.81%的用户

小结

回溯算法解题整体都是有着类似的目的,都是在寻找所有可行解的题,我们都可以尝试用搜索回溯的方法来解决。同时要记住算法实现的基本框架,可以大大减少实现难度。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

小程序开发环境搭建

发表于 2021-11-28

这是我参与11月更文挑战的第28天,活动详情查看:2021最后一次更文挑战

首先我们需要下载微信开发者工具,选择对应的版本即可。

新建项目

image-20211127172925173

这里的项目名称和目录自己指定就好,AppID 比较重要,它是小程序的唯一标识,一个小程序的账号对应与一个 AppID 所以这里我们要先去注册一个小程序。

在注册的时候我们输入对应的信息即可,一个邮箱只能对应一个公众号,或是小程序,所以你可能还是需要先创建一个邮箱。在注册完毕后,登录到小程序的后台查看 AppID。下拉最下面有一个设置,点击设置下拉可以看到 AppID。

开发模式就选择小程序,这里可以根据自己的需要选择是否是云开发,上一节也说过这个问题,语言选择 JavaScript 即可。

image-20211127174239033

初始化的一个项目大概就是这样,整体来看,分为三个模块,模拟器,编译器和调试器。这三个模块都是可以收缩的,点击右上角的模块就行。

模拟器适在右上方可以设置模拟的手机型号。

编辑器也就是我们源代码的位置。

调试器可以用来看一个 console log debug 使用,这里有一个 tab 使用的也比较多,就是 AppData 点到这个 tab 我们就可以看到这里页面所有的数据,非常好用。调试器也是用的最多的地方可以用来定位源码位置,就类似与我们浏览器的 F12。

小程序的目录结构

image-20211127175115963

sitemap.json 这个文件对于开发来说没有用,sitemap对搜索引擎非常友好,如果你的网站上产生了新的文件,使用sitemap可以让搜索引擎快速收录,是网站SEO的最常用手段。

project.config.json 这个文件保存的一起项目配置,比方说什么项目名啊 AppID 之类的可以不用管。

app.js / json / wxss 这些都是比较重要的配置了,类似于全局的交互,全局配置文件,全局的样式文件。与之对应的在 pages 目录下面的每一个 page 都有 4 个文件,js json wxml wxss 这些就类似我们前端中的 JS 页面配置,HTML CSS 。

这么一看是不是就简单清晰许多了,wxml 负责页面的骨架,wxss 负责页面的样式,js 负责页面的交互,和后端的交互,json 负责页面的配置。

这里有一点说明一下,我们新建一个页面的顺序是,首先在 pages 目录下新建一个文件夹,名字就是你想新建的 page 名,然后右键文件夹,新建 page 名字还是叫文件名,就像上图中的 index page 一样,先有一个 index 文件夹,再有一套 page 页面(JS json wxml wxss)。

编译模式

说一下小程序的编译模式,我们每次进入小程序或是保存某个页面之后,小程序会自动编译执行,然而这里有一个问题,怎么才能设置直接编译我修改的页面呢,小程序提供了添加编译模式的功能,为什么刚进入小程序的时候,会加载 index 这个页面呢,这是由 app.json 中的 pages 标签的顺序决定的,加如我们想首先编译 logs 这个页面,有两中方式,第一就是修改页面在 pages 标签中的顺序。但是这种方式不推荐,因为我们在开发的时候,希望的是开发到哪个页面,我们就编译哪个页面,这样就需要不断的修改 pages 标签。

另外一种方式就是在开发工具的顶部,点击普通编译,选择添加编译模式来修改页面的编译顺序。

1
2
3
4
json复制代码  "pages": [
  "pages/index/index",
  "pages/logs/logs"
],

image-20211127212145286

总结

到这里为止,我们就介绍了一下小程序的环境搭建过程,后续就是一些具体的开发的方法论了,我认为还是非常有用的,大家持续关注哈

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

网络协议-TCP的三次握手 结束语

发表于 2021-11-28

本文正在参与 “网络协议必知必会

山有峰顶,海有彼岸,漫漫长路,终有回转,余味苦涩,终会有回甘。别被眼前的磨难打败了,或许光明就在你放弃前的那一刻。带着愉快的心情做一个愉快的梦,醒来后,又是新的一天。

世界上任何的书籍都不能带给你好运,但是它们能让你悄悄的成为你自己的

前言

TCP位于传输层,是一个可靠的连接服务,为了准确的传输数据,TCP采用了三次握手,四次挥手策略. 这里讲的是三次握手

TCP首部格式

TCP首部数据格式,通常是20个字节再加可变字段,其中有几个特殊的标识bit,分别是URG,ACK,PSH,RST,SYN,FIN等,位置见下图

image.png

标识位 含义
URG 紧急指针有效
ACK 确认序号有效
PSH 接收方应该尽快将这个报文段交给应用层
RST 复位,关闭异常连接
SYN 同步序号用来发起一个连接
FIN 发端完成发送任务
…

本文中用到的标识位是SYN ACK , 使用的时候bit位设置为1,否则默认为0. 也用到了32位序号(Sequence number)和32位确认序号(Acknowledgment number),这里是用来存放双发的初始序列号(ISN)的.

大写的SYN ACK 代表标志位,
小写的seq代表Sequence number,
小写的ack代表Acknowledgment number,

三次握手

通俗的说

就像两个人打电话,为了确定双方的连接状况就行的以下对话:

A : 喂,我是A,你能听清我说话吗?

B : 你好A,我是B,我能听清你说话,你能听清我说话吗?

A : 你好B,我能听清你说话.

到这里两个人都知道通信质量不错,就开始巴拉巴拉的聊起了天

正常的说

在三次握手过程中,会使用SYN ACK,seq和ack.

第一次握手(客户端): 发送请求,TCP中设置SYN=1 ACK=0,seq设置为本机的ISN.

第二次握手(服务端): 收到客户端的数据之后,同意连接后,发送请求,TCP中设置SYN=1 ACK=1,seq设置为本机的ISN,并将ack设置为客户端的ISN+1

第三次握手(请求端): 接收到服务端信息后, 发送请求, TCP中设置SYN=0 ACK=1,seq设置为本机的ISN+1 ,ack设置为服务器的ISN+1

图如下

image.png

示例如下
20.1.0.1是我的电脑,20.1.0.128电脑上的一个虚拟机

在128上使用tcpdump监听ens33(虚拟机网卡)的80(nginx)端口

1
css复制代码tcpdump -i ens33 port 80 and host 20.1.0.1 -S -n

在1电脑上使用telnet请求20.1.0.128的80端口

1
复制代码telnet 20.1.0.128 80

tcpdump监听日志如下

1
2
3
yaml复制代码10:26:24.162036 IP 20.1.0.1.57520 > 20.1.0.128.http: Flags [S], seq 1045570310, win 64240, options [mss 1460,nop,wscale 8,nop,nop,sackOK], length 0
10:26:24.162273 IP 20.1.0.128.http > 20.1.0.1.57520: Flags [S.], seq 3342867055, ack 1045570311, win 29200, options [mss 1460,nop,nop,sackOK,nop,wscale 7], length 0
10:26:24.162497 IP 20.1.0.1.57520 > 20.1.0.128.http: Flags [.], ack 3342867056, win 2053, length 0

针对三次握手的攻击

SYN Flood

当客户端发送了第一次握手之后,不再回应或者失联,服务端仍然会发送SYN+ACK,发现失败了就会重试 多次重试仍然失败之后,服务器才会丢弃这个连接,这个时间约有30s-2min左右(设置不同时间不同). 如果大量的用户模拟此种数据,服务器把所有的资源全部用于响应错误的数据,那么服务器无法正常的响应真实客户的请求,此时客户就会以为服务器出问题了

…

结束语

尽信书则不如,以上内容,纯属一家之言,因个人能力有限,难免有疏漏和错误之处,如发现bug或者有更好的建议,欢迎批评指正,不吝感激

如果您喜欢我的文章,可以[关注]+[点赞]+[评论],您的三连是我前进的动力,期待与您共同成长~

1
2
3
4
arduino复制代码    作者:ZOUZDC
链接:https://juejin.cn/post/7028963866063306760
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Echo 框架:日志配置管理 介绍 安装 简述概念 快速开始

发表于 2021-11-28

这是我参与11月更文挑战的第21天,活动详情查看:2021最后一次更文挑战

介绍

通过一个完整例子,在 Echo 框架中合理管理日志。

有什么使用场景?

  • 日志自动滚动
  • 分成多个日志文件
  • 日志格式修改
  • 等等

我们将会使用 rk-boot 来启动 Echo 框架的微服务。

请访问如下地址获取完整教程:

  • rkdocs.netlify.app/cn

安装

1
2
go复制代码go get github.com/rookie-ninja/rk-boot
go get github.com/rookie-ninja/rk-echo

简述概念

rk-boot 使用如下两个库管理日志。

  • zap 管理日志实例
  • lumberjack 管理日志滚动

rk-boot 定义了两种日志类型,会在后面详细介绍,这里先做个简短介绍。

  • ZapLogger: 标准日志,用于记录 Error, Info 等。
  • EventLogger: JSON 或者 Console 格式,用于记录 Event,例如 RPC 请求。

快速开始

在这个例子中,我们会试着改变 zap 日志的路径和格式。

1.创建 boot.yaml

1
2
3
4
5
6
7
8
9
10
yaml复制代码---
zapLogger:
- name: zap-log # Required
zap:
encoding: json # Optional, options: console, json
outputPaths: ["logs/zap.log"] # Optional
echo:
- name: greeter
port: 8080
enabled: true

2.创建 main.go

往 zap-log 日志实例中写个日志。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
go复制代码package main

import (
"context"
"github.com/rookie-ninja/rk-boot"
"github.com/rookie-ninja/rk-entry/entry"
_ "github.com/rookie-ninja/rk-echo/boot"
)

func main() {
// Create a new boot instance.
boot := rkboot.NewBoot()

// Bootstrap
boot.Bootstrap(context.Background())

// Write zap log
rkentry.GlobalAppCtx.GetZapLoggerEntry("zap-log").GetLogger().Info("This is zap-log")

// Wait for shutdown sig
boot.WaitForShutdownSig(context.Background())
}

4.验证

文件夹结构

1
2
3
4
5
6
go复制代码├── boot.yaml
├── go.mod
├── go.sum
├── logs
│ └── zap.log
└── main.go

日志输出

1
json复制代码{"level":"INFO","ts":"2021-10-21T02:10:09.279+0800","msg":"This is zap-log"}

配置 EventLogger

上面的例子中,我们配置了 zap 日志,这回我们修改一下 EventLogger。

1.创建 boot.yaml

1
2
3
4
5
6
7
8
9
yaml复制代码---
eventLogger:
- name: event-log # Required
encoding: json # Optional, options: console, json
outputPaths: ["logs/event.log"] # Optional
echo:
- name: greeter
port: 8080
enabled: true

2.创建 main.go

往 event-log 实例中写入日志。

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
go复制代码package main

import (
"context"
"github.com/rookie-ninja/rk-boot"
"github.com/rookie-ninja/rk-entry/entry"
_ "github.com/rookie-ninja/rk-echo/boot"
)

func main() {
// Create a new boot instance.
boot := rkboot.NewBoot()

// Bootstrap
boot.Bootstrap(context.Background())

// Write event log
helper := rkentry.GlobalAppCtx.GetEventLoggerEntry("event-log").GetEventHelper()
event := helper.Start("demo-event")
event.AddPair("key", "value")
helper.Finish(event)

// Wait for shutdown sig
boot.WaitForShutdownSig(context.Background())
}

3.启动 main.go

1
go复制代码$ go run main.go

4.验证

文件夹结构

1
2
3
4
5
6
go复制代码├── boot.yaml
├── go.mod
├── go.sum
├── logs
│ └── event.log
└── 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
json复制代码{
"endTime":"2021-11-27T01:56:56.001+0800",
"startTime":"2021-11-27T01:56:56.001+0800",
"elapsedNano":423,
"timezone":"CST",
"ids":{
"eventId":"70b034b8-27af-43ad-97a5-82c99292297d"
},
"app":{
"appName":"echo-demo",
"appVersion":"master-f948c90",
"entryName":"",
"entryType":""
},
"env":{
"arch":"amd64",
"az":"*",
"domain":"*",
"hostname":"lark.local",
"localIP":"10.8.0.2",
"os":"darwin",
"realm":"*",
"region":"*"
},
"payloads":{},
"error":{},
"counters":{},
"pairs":{
"key":"value"
},
"timing":{},
"remoteAddr":"localhost",
"operation":"demo-event",
"eventStatus":"Ended",
"resCode":"OK"
}

概念

上面的例子中,我们尝试了 ZapLogger 和 EventLogger。接下来我们看看 rk-boot 是如何实现的,并且怎么使用。

架构

echo-logger-arch.png

ZapLoggerEntry

ZapLoggerEntry 是 zap 实例的一个封装。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
go复制代码// ZapLoggerEntry contains bellow fields.
// 1: EntryName: Name of entry.
// 2: EntryType: Type of entry which is ZapLoggerEntryType.
// 3: EntryDescription: Description of ZapLoggerEntry.
// 4: Logger: zap.Logger which was initialized at the beginning.
// 5: LoggerConfig: zap.Logger config which was initialized at the beginning which is not accessible after initialization..
// 6: LumberjackConfig: lumberjack.Logger which was initialized at the beginning.
type ZapLoggerEntry struct {
EntryName string `yaml:"entryName" json:"entryName"`
EntryType string `yaml:"entryType" json:"entryType"`
EntryDescription string `yaml:"entryDescription" json:"entryDescription"`
Logger *zap.Logger `yaml:"-" json:"-"`
LoggerConfig *zap.Config `yaml:"zapConfig" json:"zapConfig"`
LumberjackConfig *lumberjack.Logger `yaml:"lumberjackConfig" json:"lumberjackConfig"`
}

如何在 boot.yaml 里配置 ZapLoggerEntry?

ZapLoggerEntry 完全兼容 zap 和 lumberjack 的 YAML 结构。
用户可以根据需求,配置多个 ZapLogger 实例,并且通过 name 来访问。

完整配置:

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
yaml复制代码---
zapLogger:
- name: zap-logger # Required
description: "Description of entry" # Optional
zap:
level: info # Optional, default: info, options: [debug, DEBUG, info, INFO, warn, WARN, dpanic, DPANIC, panic, PANIC, fatal, FATAL]
development: true # Optional, default: true
disableCaller: false # Optional, default: false
disableStacktrace: true # Optional, default: true
sampling: # Optional, default: empty map
initial: 0
thereafter: 0
encoding: console # Optional, default: "console", options: [console, json]
encoderConfig:
messageKey: "msg" # Optional, default: "msg"
levelKey: "level" # Optional, default: "level"
timeKey: "ts" # Optional, default: "ts"
nameKey: "logger" # Optional, default: "logger"
callerKey: "caller" # Optional, default: "caller"
functionKey: "" # Optional, default: ""
stacktraceKey: "stacktrace" # Optional, default: "stacktrace"
lineEnding: "\n" # Optional, default: "\n"
levelEncoder: "capitalColor" # Optional, default: "capitalColor", options: [capital, capitalColor, color, lowercase]
timeEncoder: "iso8601" # Optional, default: "iso8601", options: [rfc3339nano, RFC3339Nano, rfc3339, RFC3339, iso8601, ISO8601, millis, nanos]
durationEncoder: "string" # Optional, default: "string", options: [string, nanos, ms]
callerEncoder: "" # Optional, default: ""
nameEncoder: "" # Optional, default: ""
consoleSeparator: "" # Optional, default: ""
outputPaths: [ "stdout" ] # Optional, default: ["stdout"], stdout would be replaced if specified
errorOutputPaths: [ "stderr" ] # Optional, default: ["stderr"], stderr would be replaced if specified
initialFields: # Optional, default: empty map
key: "value"
lumberjack: # Optional
filename: "rkapp-event.log" # Optional, default: It uses <processname>-lumberjack.log in os.TempDir() if empty.
maxsize: 1024 # Optional, default: 1024 (MB)
maxage: 7 # Optional, default: 7 (days)
maxbackups: 3 # Optional, default: 3 (days)
localtime: true # Optional, default: true
compress: true # Optional, default: true

如何在代码里获取 ZapLogger?

通过 name 来访问。

1
2
3
4
5
6
7
8
9
10
11
go复制代码// Access entry
rkentry.GlobalAppCtx.GetZapLoggerEntry("zap-logger")

// Access zap logger
rkentry.GlobalAppCtx.GetZapLoggerEntry("zap-logger").GetLogger()

// Access zap logger config
rkentry.GlobalAppCtx.GetZapLoggerEntry("zap-logger").GetLoggerConfig()

// Access lumberjack config
rkentry.GlobalAppCtx.GetZapLoggerEntry("zap-logger").GetLumberjackConfig()

EventLoggerEntry

rk-boot 把每一个 RPC 请求看作一个 Event,并且使用 rk-query 中的 Event 类型来记录日志。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
go复制代码// EventLoggerEntry contains bellow fields.
// 1: EntryName: Name of entry.
// 2: EntryType: Type of entry which is EventLoggerEntryType.
// 3: EntryDescription: Description of EventLoggerEntry.
// 4: EventFactory: rkquery.EventFactory was initialized at the beginning.
// 5: EventHelper: rkquery.EventHelper was initialized at the beginning.
// 6: LoggerConfig: zap.Config which was initialized at the beginning which is not accessible after initialization.
// 7: LumberjackConfig: lumberjack.Logger which was initialized at the beginning.
type EventLoggerEntry struct {
EntryName string `yaml:"entryName" json:"entryName"`
EntryType string `yaml:"entryType" json:"entryType"`
EntryDescription string `yaml:"entryDescription" json:"entryDescription"`
EventFactory *rkquery.EventFactory `yaml:"-" json:"-"`
EventHelper *rkquery.EventHelper `yaml:"-" json:"-"`
LoggerConfig *zap.Config `yaml:"zapConfig" json:"zapConfig"`
LumberjackConfig *lumberjack.Logger `yaml:"lumberjackConfig" json:"lumberjackConfig"`
}

EventLogger 字段

我们可以看到 EventLogger 打印出来的日志里,包含字段,介绍一下这些字段。

字段 详情
endTime 结束时间
startTime 开始时间
elapsedNano Event 时间开销(Nanoseconds)
timezone 时区
ids 包含 eventId, requestId 和 traceId。如果原数据拦截器被启动,或者 event.SetRequest() 被用户调用,新的 RequestId 将会被使用,同时 eventId 与 requestId 会一模一样。 如果调用链拦截器被启动,traceId 将会被记录。
app 包含 appName, appVersion, entryName, entryType。
env 包含 arch, az, domain, hostname, localIP, os, realm, region. realm, region, az, domain 字段。这些字段来自系统环境变量(REALM,REGION,AZ,DOMAIN)。 “*“ 代表环境变量为空。
payloads 包含 RPC 相关信息。
error 包含错误。
counters 通过 event.SetCounter() 来操作。
pairs 通过 event.AddPair() 来操作。
timing 通过 event.StartTimer() 和 event.EndTimer() 来操作。
remoteAddr RPC 远程地址。
operation RPC 名字。
resCode RPC 返回码。
eventStatus Ended 或者 InProgress

例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
ini复制代码------------------------------------------------------------------------
endTime=2021-11-27T02:30:27.670807+08:00
startTime=2021-11-27T02:30:27.670745+08:00
elapsedNano=62536
timezone=CST
ids={"eventId":"4bd9e16b-2b29-4773-8908-66c860bf6754"}
app={"appName":"echo-demo","appVersion":"master-f948c90","entryName":"greeter","entryType":"EchoEntry"}
env={"arch":"amd64","az":"*","domain":"*","hostname":"lark.local","localIP":"10.8.0.6","os":"darwin","realm":"*","region":"*"}
payloads={"apiMethod":"GET","apiPath":"/rk/v1/healthy","apiProtocol":"HTTP/1.1","apiQuery":"","userAgent":"curl/7.64.1"}
error={}
counters={}
pairs={}
timing={}
remoteAddr=localhost:61726
operation=/rk/v1/healthy
resCode=200
eventStatus=Ended
EOE

如何在 boot.yaml 里配置 EventLoggerEntry?

EventLoggerEntry 将会把 Application 名字注入到 Event 中。启动器会从 go.mod 文件中提取 Application 名字。 如果没有 go.mod 文件,启动器会使用默认的名字。

用户可以根据需求,配置多个 EventLogger 实例,并且通过 name 来访问。

完整配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
yaml复制代码---
eventLogger:
- name: event-logger # Required
description: "This is description" # Optional
encoding: console # Optional, default: console, options: console and json
outputPaths: ["stdout"] # Optional
lumberjack: # Optional
filename: "rkapp-event.log" # Optional, default: It uses <processname>-lumberjack.log in os.TempDir() if empty.
maxsize: 1024 # Optional, default: 1024 (MB)
maxage: 7 # Optional, default: 7 (days)
maxbackups: 3 # Optional, default: 3 (days)
localtime: true # Optional, default: true
compress: true # Optional, default: true

如何在代码里获取 EventLogger?

通过 name 来访问。

1
2
3
4
5
6
7
8
9
10
11
go复制代码// Access entry
rkentry.GlobalAppCtx.GetEventLoggerEntry("event-logger")

// Access event factory
rkentry.GlobalAppCtx.GetEventLoggerEntry("event-logger").GetEventFactory()

// Access event helper
rkentry.GlobalAppCtx.GetEventLoggerEntry("event-logger").GetEventHelper()

// Access lumberjack config
rkentry.GlobalAppCtx.GetEventLoggerEntry("event-logger").GetLumberjackConfig()

如何使用 Event?

Event 是一个 interface,包含了若干方法,请参考:Event

常用方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
go复制代码// Get EventHelper to create Event instance
helper := rkentry.GlobalAppCtx.GetEventLoggerEntry("event-log").GetEventHelper()

// Start and finish event
event := helper.Start("demo-event")
helper.Finish(event)

// Add K/V
event.AddPair("key", "value")

// Start and end timer
event.StartTimer("my-timer")
event.EndTimer("my-timer")

// Set counter
event.SetCounter("my-counter", 1)

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Go(七)你说你不会并发?

发表于 2021-11-28

这是我参与11月更文挑战的第12天,活动详情查看:2021最后一次更文挑战

作者:lomtom

个人网站:lomtom.top,

个人公众号:博思奥园

你的支持就是我最大的动力。

Go系列:

  1. Go(一)基础入门
  2. Go(二)结构体
  3. Go(三)Go配置文件
  4. Go(四)Redis操作
  5. Go(五)Go不知道怎么用Gorm?
  6. Go(六)来来来,教你怎么远程调用
  7. Go(七)你说你不会并发?

不要通过共享内存来通信,而应通过通信来共享内存。

协程(goroutine)

Go 协程具有简单的模型:它是与其它Go 协程并发运行在同一地址空间的函数。它是轻量级的, 所有消耗几乎就只有栈空间的分配。而且栈最开始是非常小的,所以它们很廉价, 仅在需要时才会随着堆空间的分配(和释放)而变化。

Go 协程在多线程操作系统上可实现多路复用,因此若一个线程阻塞,比如说等待I/O, 那么其它的线程就会运行。

Go 协程的设计隐藏了线程创建和管理的诸多复杂性。

在函数或方法前添加 go 关键字能够在新的Go 协程中调用它。当调用完成后, 该Go 协程也会安静地退出。(效果有点像Unix Shell中的 & 符号,它能让命令在后台运行。)

1
go复制代码go myFunc()  // 同时运行 myFunc 不需要等待

匿名函数在协程中调用非常方便:

1
2
3
4
5
6
7
8
go复制代码func TestGo(t *testing.T) {
s := "你好吗"
go func() {
fmt.Println(s)
}()
ss := "小道科不好"
fmt.Println(ss)
}

结果输出:

1
2
bash复制代码小道科不好
你好吗

在Go中,匿名函数都是闭包:其实现在保证了函数内引用变量的生命周期与函数的活动时间相同。

所以,值得注意的是,如果主函数执行完,而go后面的方法未执行完,程序同样停止。
例如,我们在go后面的方法中,让函数睡眠1秒钟,这样主程序运行完就会退出,而不会输出s

1
2
3
4
5
6
7
8
9
go复制代码func TestGo(t *testing.T) {
s := "你好吗"
go func() {
time.Sleep(1 * time.Second)
fmt.Println(s)
}()
ss := "小道科不好"
fmt.Println(ss)
}

输出:

1
bash复制代码小道科不好

那么如何避免这样的情况呢?后续会讲到

这些函数没什么实用性,因为它们没有实现完成时的信号处理。因此,我们需要信道。

管道(channel)

为什么需要channel?
单纯地将函数并发执行是没有意义的。函数与函数间需要交换数据才能体现并发执行函数的意义。

Go 语言中的通道(channel)是一种特殊的类型。

通道像一个传送带或者队列,总是遵循先入先出(First In First Out)的规则,保证收发数据的顺序。每一个通道都是一个具体类型的导管,也就是声明channel的时候需要为其指定元素类型。

管道与映射一样,也需要通过 make 来分配内存。

其结果值充当了对底层数据结构的引用。 若提供了一个可选的整数形参,它就会为该信道设置缓冲区大小。

默认值是零,表示不带缓冲的或同步的信道。

  1. 创建(关键词chan)
1
2
3
go复制代码c := make(chan int)            // 整数无缓冲信道
c := make(chan int, 0) // 整数无缓冲信道
c := make(chan *os.File, 100) // 指向文件的指针的缓冲信道
  1. 插入
1
go复制代码c <- a
  1. 读取
1
go复制代码num := <- c
  1. 声明读写管道
1
go复制代码var c int
  1. 声明只写管道
1
go复制代码var c chan<- int
  1. 声明只读管道
1
go复制代码var c <-chan int

例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
go复制代码func TestGo(t *testing.T) {
// 创建一个无缓冲的类型为整型的 channel
c := make(chan int)
// 执行自定义方法;方法结束时,会在信道上发信号
go func() {
// doSomething
for i := 0; i < 5; i++ {
// 发送一个信号
c <- i
}
// 关闭管道
close(c)
}()
// doSomething
// 等待自定义方法执行完成,然后从 channel 取值
for i := range c{
fmt.Println(i)
}
}

接收者在收到数据前会一直阻塞。

  1. 若信道是不带缓冲的,那么在接收者收到值前, 发送者会一直阻塞;
  2. 若信道是带缓冲的,则发送者仅在值被复制到缓冲区前阻塞;
  3. 若缓冲区已满,发送者会一直等待直到某个接收者取出一个值为止。

sync

sync.WaitGroup

在go中可以使用chan来实现通信,同样,Go 也提供了WaitGroup来实现同步。

上面的例子很好说明了没有使用WaitGroup存在的同步问题

1
2
3
4
5
6
7
8
9
go复制代码func TestGo(t *testing.T) {
s := "你好,世界"
go func() {
time.Sleep(1 * time.Second)
fmt.Println(s)
}()
ss := "小道科不好"
fmt.Println(ss)
}

这里重新举一个例子,我在主函数里输出十次,在myFunc里也输出十次,理论情况,都会输出。

为了模拟在myFunc未执行完,而主程序执行完的情况下,在myFunc中加入time.Sleep(time.Second*1)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
go复制代码func TestGo1(t *testing.T) {
go myFunc()
for i := 0; i < 10; i++ {
fmt.Println("main()测试,这是第" + strconv.Itoa(i) + "次")
}
wg.Wait()
}

func myFunc() {
for i := 0; i < 10; i++ {
fmt.Println("test()测试,这是第" + strconv.Itoa(i) + "次")
time.Sleep(time.Second*1)
}
}

输出如下:

1
2
3
4
5
6
7
8
9
10
11
bash复制代码main()测试,这是第0次
main()测试,这是第1次
main()测试,这是第2次
main()测试,这是第3次
main()测试,这是第4次
main()测试,这是第5次
main()测试,这是第6次
main()测试,这是第7次
main()测试,这是第8次
main()测试,这是第9次
test()测试,这是第0次

这显然不是我们所要的结果,那么最简单的就是在主函数的循环中也加入time.Sleep(time.Second*1)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
go复制代码func TestGo1(t *testing.T) {
go myFunc()
for i := 0; i < 10; i++ {
fmt.Println("main()测试,这是第" + strconv.Itoa(i) + "次")
time.Sleep(time.Second*1)
}
wg.Wait()
}

func myFunc() {
for i := 0; i < 10; i++ {
fmt.Println("test()测试,这是第" + strconv.Itoa(i) + "次")
time.Sleep(time.Second*1)
}
}

输出如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
bash复制代码main()测试,这是第0次
test()测试,这是第0次
main()测试,这是第1次
test()测试,这是第1次
main()测试,这是第2次
test()测试,这是第2次
test()测试,这是第3次
main()测试,这是第3次
main()测试,这是第4次
test()测试,这是第4次
test()测试,这是第5次
main()测试,这是第5次
main()测试,这是第6次
test()测试,这是第6次
test()测试,这是第7次
main()测试,这是第7次
main()测试,这是第8次
test()测试,这是第8次
test()测试,这是第9次
main()测试,这是第9次

这虽然达到了我们的预想的效果,但在正式情况下,我们并不会知道代码的执行速度与时间,所以这个一秒,理论可行,实际却很拉垮。

那么就可以使用WaitGroup来控制。

  1. 只要开启一个协程,就Add(1),表示开启一个协程
  2. 协程执行完毕,则需要Done(),表示从协程序等待组里删除
  3. 只有当所有的协程都Done()后,才会继续执行Wait()后续代码。
  4. Add的协程数量,需要和Done的协程数对应,否则死锁报错
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
go复制代码func TestGo1(t *testing.T) {
var wg sync.WaitGroup
go myFunc(&wg)
for i := 0; i < 10; i++ {
fmt.Println("main()测试,这是第" + strconv.Itoa(i) + "次")
//time.Sleep(time.Second*1)
}
wg.Wait()
}

func myFunc(wg *sync.WaitGroup) {
wg.Add(1)
for i := 0; i < 10; i++ {
fmt.Println("test()测试,这是第" + strconv.Itoa(i) + "次")
time.Sleep(time.Second*1)
}
wg.Done()
}

输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
bash复制代码main()测试,这是第0次
main()测试,这是第1次
main()测试,这是第2次
main()测试,这是第3次
main()测试,这是第4次
main()测试,这是第5次
test()测试,这是第0次
main()测试,这是第6次
main()测试,这是第7次
main()测试,这是第8次
main()测试,这是第9次
test()测试,这是第1次
test()测试,这是第2次
test()测试,这是第3次
test()测试,这是第4次
test()测试,这是第5次
test()测试,这是第6次
test()测试,这是第7次
test()测试,这是第8次
test()测试,这是第9次

sync.Once

在编程的很多场景下我们需要确保某些操作在高并发的场景下只执行一次,例如只加载一次配置文件、
只关闭一次通道等。

Go语言中的sync包中提供了一个针对只执行一次场景的解决方案:sync.Once。

sync.Once对外提供的操作只有一个Do方法:

1
2
3
4
5
go复制代码func (o *Once) Do(f func()) {
if atomic.LoadUint32(&o.done) == 0 {
o.doSlow(f)
}
}

在使用上,我们只需要将需要执行的方法传入即可。

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
go复制代码var db *gorm.DB

var loadDbConf sync.Once

// GetDb 获取连接
func GetDb() *gorm.DB {
loadDbConf.Do(DbInit)
return db
}

// DbInit 数据库连接池初始化
func DbInit() {
newLogger := logger.New(
log.New(os.Stdout, "\r\n", log.LstdFlags), // io writer
logger.Config{
SlowThreshold: time.Second, // Slow SQL threshold
LogLevel: logger.Info, // Log level
IgnoreRecordNotFoundError: true, // Ignore ErrRecordNotFound error for logger
Colorful: true, // Disable color
},
)
conn, err1 := gorm.Open(mysql.Open(mySQLUri()), &gorm.Config{
Logger: newLogger,
})
if err1 != nil {
log.Printf("mysql connect get failed.%v", err1)
return
}
db = conn
log.Printf("mysql init success")
}
1
2
3
4
go复制代码type Once struct {
done uint32
m Mutex
}

sync.Once其实内部包含一个互斥锁和一个布尔值,互斥锁保证布尔值和数据的安全,而布尔值用来记
录初始化是否完成。

这样设计就能保证初始化操作的时候是并发安全的并且初始化操作也不会被执行多次。

sync.Map

Go语言中内置的map不是并发安全的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
go复制代码var m = make(map[string]int)

func get(key string) int {
return m[key]
}

func set(key string, value int) {
m[key] = value
}

func TestGo5(t *testing.T) {
wg := sync.WaitGroup{}
for i := 0; i < 20; i++ {
wg.Add(1)
go func(n int) {
key := strconv.Itoa(n)
set(key, n)
fmt.Printf("k=:%v,v:=%v\n", key, get(key))
wg.Done()
}(i)
}
wg.Wait()
}

上面的代码开启少量几个goroutine的时候可能没什么问题,当并发多了之后执行上面的代码就会报fatal error: concurrent map writes错误。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
bash复制代码k=:0,v:=0
k=:2,v:=2
k=:4,v:=4
k=:5,v:=5
k=:6,v:=6
k=:7,v:=7
k=:1,v:=1
k=:8,v:=8
k=:3,v:=3
k=:11,v:=11
k=:10,v:=10
k=:12,v:=12
fatal error: concurrent map writes
k=:13,v:=13
k=:14,v:=14

goroutine 38 [running]:
runtime.throw(0xe6715c, 0x15)
E:/program/go/src/runtime/panic.go:1117 +0x79 fp=0xc000337ec8 sp=0xc000337e98 pc=0x7ac6f9
runtime.mapassign_faststr(0xd9b800, 0xc00003c2a0, 0xe8046a, 0x2, 0x0)

像这种场景下就需要为map加锁来保证并发的安全性了,Go语言的sync包中提供了一个开箱即用的并发安全版map–sync.Map。

开箱即用表示不用像内置的map一样使用make函数初始化就能直接使用。同时sync.Map内置了诸如Store、Load、LoadOrStore、Delete、Range等操作方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
go复制代码var m1 = sync.Map{}

func TestGo6(t *testing.T) {
wg := sync.WaitGroup{}
for i := 0; i < 20; i++ {
wg.Add(1)
go func(n int) {
k := strconv.Itoa(n)
m1.Store(k,n)
v, _ := m1.Load(k)
fmt.Printf("k=:%v,v:=%v\n", k, v)
wg.Done()
}(i)
}
wg.Wait()
}

并行化

1 - n 的和

这些设计的另一个应用是在多CPU核心上实现并行计算。

如果计算过程能够被分为几块 可独立执行的过程,它就可以在每块计算结束时向信道发送信号,从而实现并行处理。

在下面这个例子,我需要计算1 到 n的和,一般来说简单的直接一个循环搞定。

1
2
3
4
5
6
7
8
go复制代码func TestGo(t *testing.T){
n := 100000
var s int
for i := 0;i < n;i++{
s += i
}
log.Println(s)
}

但是对于数据量比较大的,这样显然不适合出现在我们的代码中,那么就可以采用并行计算来实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
go复制代码func TestGo3(t *testing.T){
t1 := time.Now()
n := 100
num := 10
c := make(chan int,num)
for i := 0;i < num;i++ {
go func() {
start := n / num * i
end := n / num * (i + 1)
var s int
for j := start; j < end;j++ {
s += j
}
c <- s
}()
}
var s int
for i := 0;i < num;i++ {
s += <- c
}
t2 := time.Since(t1)
log.Println(t2)
log.Println(s)
}

当然也可以借用WaitGroup

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
go复制代码func TestGo3(t *testing.T){
t1 := time.Now()
n := 100
num := 10
c := make(chan int,num)
var wg sync.WaitGroup
for i := 0;i < num;i++ {
wg.Add(1)
go func() {
start := n / num * i
end := n / num * (i + 1)
var s int
for j := start; j < end;j++ {
s += j
}
c <- s
wg.Done()
}()
}
wg.Wait()
close(c)
var s int
for item := range c{
s += item
}
t2 := time.Since(t1)
log.Println(t2)
log.Println(s)
}

我们在循环中启动了独立的处理块,每个CPU将执行一个处理。

它们有可能以乱序的形式完成并结束,但这没有关系; 我们只需在所有Go协程开始后接收,并统计信道中的完成信号即可。

除了直接设置 num 常量值以外,我们还可以向 runtime 询问一个合理的值。

函数 runtime.NumCPU 可以返回硬件 CPU 上的核心数量,如此使用:

1
go复制代码var num = runtime.NumCPU()

另外一个需要知道的函数是 runtime.GOMAXPROCS,会返回用户设置可用 CPU 数量。默认情况下使用 runtime.NumCPU的值,但是可以被命令行环境变量,或者调用此函数并传参正整数。传参 0 的话会返回值,假如说我们尊重用户对资源的分配,

就应该这么写:

1
go复制代码var numCPU = runtime.GOMAXPROCS(0)

注意不要混淆并发(concurrency)和并行(parallelism)的概念:
并发是用可独立执行组件构造程序的方法, 而并行则是为了效率在多 CPU 上平行地进行计算。

尽管 Go 的并发特性能够让某些问题更易构造成并行计算, 但 Go 仍然是种并发而非并行的语言,且 Go 的模型并不适合所有的并行问题。

问题

但是,如果你跑过了前面的代码,就会发现一个问题,在后面的计算其实是不准确的。

1 - 100的值应该为4950,而他每次输出的值基本都不会相同,更不可能是4950.

问题出现在Go的 for 循环中,该循环变量在每次迭代时会被重用,因此 i变量会在所有的Go协程间共享,这不是我们想要的。

我们需要确保 i 对于每个Go协程来说都是唯一的。

这里有几种方法来实现:

  1. 第一种:多写一层调用
    将下面的匿名方法抽成一个一般方法,将i作为参数传入。
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
go复制代码func TestGo3(t *testing.T){
t1 := time.Now()
n := 100
num := 10
c := make(chan int,num)
var wg sync.WaitGroup
for i := 0;i < num;i++ {
wg.Add(1)
go myFunc1(n,num,i,c,&wg)
}
wg.Wait()
close(c)
var s int
for item := range c{
s += item
}
t2 := time.Since(t1)
log.Println(t2)
log.Println(s)
}

func myFunc1(n,num,i int,c chan int,wg *sync.WaitGroup) {
start := n / num * i
end := n / num * (i + 1)
var s int
for j := start; j < end;j++ {
s += j
}
c <- s
wg.Done()
}
  1. 第二种:传入参数的闭包(以i为参数传入)
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
go复制代码func TestGo3(t *testing.T){
t1 := time.Now()
n := 100
num := 10
c := make(chan int,num)
var wg sync.WaitGroup
for i := 0;i < num;i++ {
wg.Add(1)
// 以i为参数传入
go func(i int) {
start := n / num * i
end := n / num * (i + 1)
var s int
for j := start; j < end;j++ {
s += j
}
c <- s
wg.Done()
}(i)
}
wg.Wait()
close(c)
var s int
for item := range c{
s += item
}
t2 := time.Since(t1)
log.Println(t2)
log.Println(s)
}
  1. 第三种:重新申明
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
go复制代码func TestGo3(t *testing.T){
t1 := time.Now()
n := 100
num := 10
c := make(chan int,num)
var wg sync.WaitGroup
for i := 0;i < num;i++ {
wg.Add(1)
// 重新申明
i := i
go func() {
start := n / num * i
end := n / num * (i + 1)
var s int
for j := start; j < end;j++ {
s += j
}
c <- s
wg.Done()
}()
}
wg.Wait()
close(c)
var s int
for item := range c{
s += item
}
t2 := time.Since(t1)
log.Println(t2)
log.Println(s)
}

i:= i的写法看起来有点奇怪,但在 Go 中这样做是合法且常见的。

你用相同的名字获得了该变量的一个新的版本, 以此来局部地刻意屏蔽循环变量,使它对每个 Go 协程保持唯一。

性能对比

传统方法与并行计算:

这里不考虑越界情况(即不考虑计算正确性),因为他每次结果还是会计算的。

n值 传统 并行
10000000 3.7146ms 1.058ms
100000000 39.9925ms 7.1645ms
1000000000 344.0599ms 49.9023ms
10000000000 3.4797346s 501.5713ms
100000000000 34.5406926s 4.650136s

结果还是挺明显的。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

栈刷题记(二-用栈操作构建数组)

发表于 2021-11-28

这是我参与11月更文挑战的第14天,活动详情查看:2021最后一次更文挑战

前言

  • 各位读者,晚上好呀!,不知不觉,小嘟已经在掘金社区发布了15篇文章(算上今天这一篇),在这15天中小嘟觉得掘金这个社区很nice,是一个大佬云集、社区活跃、定位很准确的一个社区,除此外,我最大的感受就是掘金的活动力度很大(不知道掘金从哪里赚钱买礼物呢😂😂😂),不管啦不管啦,反正我就是喜欢掘金。
  • 小嘟叨叨完毕,开始进入正题...

正文

题目

image.png

题目约束条件

image.png

示例

image.png

题目分析

  • 小嘟在这里大胆猜测一下:肯定有读者看到这个题目的文字好多,然后就已经做好了放弃的准备😂😂😂 ,最后也就确实放弃了。
  • 有位伟人曾经说过:文字越长题越简单,越长意味着给的信息点也就越多。(小嘟我明人不说暗话,其实小嘟就是那个伟人😆😆😆)
  • 首先,我们要明白题目想要表达的意思是什么?它让我们干什么事情呢?
  • 小嘟在这里举一个例子:比方说n=5,也就意味list=[1,2,3,4,5],我们要做的是从左到右依次拿出一个数(依次就是一个数的前边没数字了才能取这个数),然后把它放到target数组中,我们将放进去的操作称为“Push”,拿出来的操作称为“Pop”,最终得到target数组,本题的结果就是要我们求出操作的集合。
  • 那么我们该怎么做呢?
  • 第一件事,想都不用想,首先肯定要遍历;
  • 第二件事,我们怎样遍历?(重点)
+ **首先**,我们知道,操作无非**就是Push,或者是Pop这两种操作**,然后我们在遍历的过程中**肯定会比较**的,然后再想如果`比较为相等`,该**怎么办**呢?`答`:**相等那就是只进行了Push操作,没有Pop操作**。那如果`不相等呢`?`答`:**不相等那就是先进行“Push”操作,再接着进行“Pop操作”**。
+ `读者`:小嘟你**不能一直遍历,你还要退出循环呢**?
+ `小嘟`:对的,我还要退出呢,那么**我什么时候退出呢**?
+ `读者`:**是不是target数组遍历完了呢**?
+ `小嘟`:读者真厉害!我们要在比较完之后退出,小嘟已经说到了这个地方了,那就直接看代码吧!

代码及其结果展示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
ini复制代码var buildArray = function(target, n) {
let j = 0,i=0;
let res = [];
while(j<target.length){
i++;
res.push('Push');
if(i == target[j]){
j++;
continue;
}
else res.push('Pop');
}
return res;
}

image.png

本题回顾

  • 小嘟把Push操作放在了比较的前边,这个位置小嘟也不是一次就想到的,所以,当读者看一些很难懂的代码的时候,不要觉得自己不行,在小嘟看来,这些时间复杂度和空间复杂度都很好的代码,肯定是经过很多次打磨了。
  • 代码比较简单,读者加油!

结尾

  • 学习算法,我们要学的是算法的思想,而不是每道题的代码应该怎样写。
  • 学习思路框架,可以让我们能更轻松的处理问题。
  • 最后的最后,小嘟想说一句话与读者共勉:成功不是一蹴而就的,而是一步一步熬出来的。成功的人懂得熬,所以成功;失败的人只会逃避困难,必然失败。
  • 愿读者能够坚持下去!
  • 明天是工作日,大家晚安啦!

附件

题目直通车 👉 leetcode-cn.com/problems/bu…

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

从JDK中学习设计模式——模板方法模式

发表于 2021-11-28

这是我参与11月更文挑战的第28天,活动详情查看:2021最后一次更文挑战

概述

模板方法模式(Template Method Pattern)定义一个操作中算法的框架,而将一些步骤延迟到子类中,使子类可以在不改变算法结构的情况下,重新定义该算法的某些特定步骤。因此,模板方法模式是一种基于继承的代码复用技术,属于类行为型模式。

结构

模板方法模式UML.png

  • AbstractClass(抽象类):一般是抽象类或接口,其中定义了算法的框架(模板方法)和一系列基本操作(基本方法)。在模板方法中,可以调用在抽象类中实现的基本方法,也可以调用在抽象类的子类中实现的基本方法,还可以调用其它对象中的方法。基本方法可以是具体的,也可以是抽象的,在其子类中可以重定义或实现这些方法。
  • ConcreteClass(具体子类):用于实现在父类中声明的抽象基本操作以完成子类特定算法的步骤,也可以覆盖在父类中已经实现的具体基本操作。

优点

  1. 在父类中定义算法的框架,在子类中实现具体的步骤,降低了系统的耦合度。
  2. 通过父类可以调用子类的操作,通过对子类的扩展可以增加新的行为,符合开闭原则。
  3. 通过继承实现了代码的复用,可以将公共行为放在父类中,然后通过子类来实现不同的行为。
  4. 实现了反向控制结构,可以通过子类覆盖父类的钩子方法来决定某一特定步骤是否执行。

缺点

  1. 增加了系统中类的数量,提升了系统的复杂度。
  2. 子类执行的结果会影响父类的结果,降低了代码的可读性。

应用场景

  1. 多个子类有公有的方法,并且逻辑基本相同,可以将公共的行为提取出来,集中到公共父类中,可以避免代码的重复。
  2. 重要、复杂的算法,可以把核心算法设计为模板方法,相关的细节功能则由子类实现。
  3. 子类可以决定父类中的某个步骤是否执行,实现子类对父类的反向控制。

JDK 中的应用

在 JDK 中,在以下类中都使用了模板方法模式:

  • java.util.Collections#sort()
  • java.io.InputStream#skip()
  • java.io.InputStream#read()
  • java.util.AbstractList#indexOf()
  • java.io.InputStream / java.io.OutputStream 的所有非抽象方法
  • java.io.Reader / java.io.Writer 的所有非抽象方法
  • java.util.AbstractList / java.util.AbstractSet / java.util.AbstractMap 的所有非抽象方法

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Spring Cloud / Alibaba 微服务架构

发表于 2021-11-28

这是我参与11月更文挑战的第27天,活动详情查看:2021最后一次更文挑战。

上篇文章中介绍了添加前缀的局部过滤器 PrefixPathGatewayFilterFacotry和去除前缀的局部过滤器 StripPrefixGatewayFilterFactory,这篇文章我们将自定义一个局部过滤器,用来校验Http请求头部中的Token。

自定义局部过滤器

1、创建对应的包和类

在e-commerce-gateway子模块下的com.sheep.ecommerce包下创建一个filter包。在包下创建HeaderTokenGatewayFilter类,即HTTP请求头部携带Token验证过滤器。

在filter包下再创建一个factory包,在包下创建HeaderTokenAbstractGatewayFilterFactory类。

2、编写过滤器 HeaderTokenGatewayFilter

对于局部过滤器我们需要去实现GatewayFilter和Ordered。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
kotlin复制代码public class HeaderTokenGatewayFilter implements GatewayFilter, Ordered {

@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String name = exchange.getRequest().getHeaders().getFirst("token");
if ("hello".equals(name)) {
return chain.filter(exchange);
}
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
}

@Override
public int getOrder() {
return HIGHEST_PRECEDENCE + 2;
}
}

先看一下getOrder方法,return 最高优先级+2,因为order越高,优先级越低,所以我们加二,这样返回的就不是最高优先级了。

接下来再解读一下filter方法。首先是从 HTTP Header 中寻找 key 为 token, value 为 hello 的键值对。如果没有的话,就标记此次请求没有权限, 并结束这次请求。这样就保证了我们的这个请求里必须有”tooken”:”hello”这样的键值对。

这样我们的过滤器实现就编写完了,接下来我们还要实现一个过滤器工厂GatewayFilterFactory。

3、编写过滤器工厂 HeaderTokenAbstractGatewayFilterFactory

要继承自AbstractGatewayFilterFactory,否则我们编写的局部过滤器是不生效的。需要的泛型我们定义为Object,接下来去重载里面的apply方法,直接return前面编写的HeaderTokenGatewayFilter方法即可。这样我们的局部过滤器代码编写就完成了。

4、配置

之前在配置网关的Nacos里有e-commerce-nacos-client这样一个路由配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
json复制代码{
"id": "e-commerce-nacos-client",
"predicates": [
{
"args": {
"pattern": "/imooc/ecommerce-nacos-client/**"
},
"name": "Path"
}
],
"uri": "lb://e-commerce-nacos-client",
"filters": [
{
"name": "HeaderToken"
},
{
"name": "StripPrefix",
"args": {
"parts": "1"
}
}
]
}

其中filters就代表的是局部过滤器,因为全局过滤器不需要配置。filters里还有一个name是StripPrefix的过滤器,也就是之前我们有介绍过的,去除前缀的过滤器,parts为1代表去除一个前缀。

小结

简单来说,要实现一个自定义的局部过滤器,我们需要完成以下几件事:

1)实现GatewayFilter和Ordered

2)需要加到过滤器工厂里,并将工厂注册到Spring容器中(加上@Component注解)

3)在配置文件中进行配置,如果不配置则代表当前过滤器规则不会被启用

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…131132133…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%