Java多线程学习(吐血超详细总结)

InterviewCoder

# Java 多线程学习(吐血超详细总结)

写在前面的话:此文只能说是 java 多线程的一个入门,其实 Java 里头线程完全可以写一本书了,但是如果最基本的你都学掌握好,又怎么能更上一个台阶呢?如果你觉得此文很简单,那推荐你看看 Java 并发包的的线程池(Java 并发编程与技术内幕:线程池深入理解),或者看这个专栏:Java 并发编程与技术内幕。你将会对 Java 里头的高并发场景下的线程有更加深刻的理解。

目录 (?)[-]

  1. 一扩展 javalangThread 类
  2. 二实现 javalangRunnable 接口
  3. 三 Thread 和 Runnable 的区别
  4. 四线程状态转换
  5. 五线程调度
  6. 六常用函数说明
    1. 使用方式
    2. 为什么要用 join 方法
  7. 七常见线程名词解释
  8. 八线程同步
  9. 九线程数据传递

​ 本文主要讲了 java 中多线程的使用方法、线程同步、线程数据传递、线程状态及相应的一些线程函数用法、概述等。在这之前,首先让我们来了解下在操作系统中进程和线程的区别:

进程:每个进程都有独立的代码和数据空间(进程上下文),进程间的切换会有较大的开销,一个进程包含 1–n 个线程。(进程是资源分配的最小单位)

线程:同一类线程共享代码和数据空间,每个线程有独立的运行栈和程序计数器 (PC),线程切换开销小。(线程是 cpu 调度的最小单位)

线程和进程一样分为五个阶段:创建、就绪、运行、阻塞、终止。

多进程是指操作系统能同时运行多个任务(程序)。

多线程是指在同一程序中有多个顺序流在执行。

java 中要想实现多线程,有两种手段,一种是继续 Thread 类,另外一种是实现 Runable 接口.(其实准确来讲,应该有三种,还有一种是实现 Callable 接口,并与 Future、线程池结合使用,此文这里不讲这个,有兴趣看这里 Java 并发编程与技术内幕:Callable、Future、FutureTask、CompletionService )

# 一、扩展 java.lang.Thread 类

这里继承 Thread 类的方法是比较常用的一种,如果说你只是想起一条线程。没有什么其它特殊的要求,那么可以使用 Thread.(笔者推荐使用 Runable,后头会说明为什么)。下面来看一个简单的实例

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
package com.multithread.learning;

class Thread1 extends Thread{

private String name;

public Thread1(String name) {

this.name=name;

}



public void run() {



for (int i = 0; i < 5; i++) {



System.out.println(name + "运行 : " + i);



try {



sleep((int) Math.random() * 10);



} catch (InterruptedException e) {



e.printStackTrace();



}



}







}



}



public class Main {







public static void main(String[] args) {



Thread1 mTh1=new Thread1("A");



Thread1 mTh2=new Thread1("B");



mTh1.start();



mTh2.start();







}







}

输出:

A 运行 : 0
B 运行 : 0
A 运行 : 1
A 运行 : 2
A 运行 : 3
A 运行 : 4
B 运行 : 1
B 运行 : 2
B 运行 : 3
B 运行 : 4

再运行一下:

A 运行 : 0
B 运行 : 0
B 运行 : 1
B 运行 : 2
B 运行 : 3
B 运行 : 4
A 运行 : 1
A 运行 : 2
A 运行 : 3
A 运行 : 4

说明:

程序启动运行 main 时候,java 虚拟机启动一个进程,主线程 main 在 main () 调用时候被创建。随着调用 MitiSay 的两个对象的 start 方法,另外两个线程也启动了,这样,整个应用就在多线程下运行。

注意:start () 方法的调用后并不是立即执行多线程代码,而是使得该线程变为可运行态(Runnable),什么时候运行是由操作系统决定的。

从程序运行的结果可以发现,多线程程序是乱序执行。因此,只有乱序执行的代码才有必要设计为多线程。

Thread.sleep () 方法调用目的是不让当前线程独自霸占该进程所获取的 CPU 资源,以留出一定时间给其他线程执行的机会。

实际上所有的多线程代码执行顺序都是不确定的,每次执行的结果都是随机的。

但是 start 方法重复调用的话,会出现 java.lang.IllegalThreadStateException 异常。

1
2
3
4
5
6
7
8
9
10
11
12
13
Thread1 mTh1=new Thread1("A");



Thread1 mTh2=mTh1;



mTh1.start();



mTh2.start();

输出:

Exception in thread “main” java.lang.IllegalThreadStateException
at java.lang.Thread.start(Unknown Source)
at com.multithread.learning.Main.main(Main.java:31)
A 运行 : 0
A 运行 : 1
A 运行 : 2
A 运行 : 3
A 运行 : 4

# 二、实现 java.lang.Runnable 接口

采用 Runnable 也是非常常见的一种,我们只需要重写 run 方法即可。下面也来看个实例。

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
/**



*@functon 多线程学习



*@author 林炳文



*@time 2015.3.9



*/



package com.multithread.runnable;



class Thread2 implements Runnable{



private String name;







public Thread2(String name) {



this.name=name;



}







@Override



public void run() {



for (int i = 0; i < 5; i++) {



System.out.println(name + "运行 : " + i);



try {



Thread.sleep((int) Math.random() * 10);



} catch (InterruptedException e) {



e.printStackTrace();



}



}







}







}



public class Main {







public static void main(String[] args) {



new Thread(new Thread2("C")).start();



new Thread(new Thread2("D")).start();



}







}

输出:

C 运行 : 0
D 运行 : 0
D 运行 : 1
C 运行 : 1
D 运行 : 2
C 运行 : 2
D 运行 : 3
C 运行 : 3
D 运行 : 4
C 运行 : 4

说明:

Thread2 类通过实现 Runnable 接口,使得该类有了多线程类的特征。run()方法是多线程程序的一个约定。所有的多线程代码都在 run 方法里面。Thread 类实际上也是实现了 Runnable 接口的类。

在启动的多线程的时候,需要先通过 Thread 类的构造方法 Thread (Runnable target) 构造出对象,然后调用 Thread 对象的 start () 方法来运行多线程代码。

实际上所有的多线程代码都是通过运行 Thread 的 start () 方法来运行的。因此,不管是扩展 Thread 类还是实现 Runnable 接口来实现多线程,最终还是通过 Thread 的对象的 API 来控制线程的,熟悉 Thread 类的 API 是进行多线程编程的基础。

# 三、Thread 和 Runnable 的区别

如果一个类继承 Thread,则不适合资源共享。但是如果实现了 Runable 接口的话,则很容易的实现资源共享。

** 总结:
**

实现 Runnable 接口比继承 Thread 类所具有的优势:

1):适合多个相同的程序代码的线程去处理同一个资源

2):可以避免 java 中的单继承的限制

3):增加程序的健壮性,代码可以被多个线程共享,代码和数据独立

4):线程池只能放入实现 Runable 或 callable 类线程,不能直接放入继承 Thread 的类

* 提醒一下大家:**main** 方法其实也是一个线程。在 **java** 中所以的线程都是同时启动的,至于什么时候,哪个先执行,完全看谁先得到 **CPU** 的资源。*

java 中,每次程序运行至少启动 2 个线程。一个是 main 线程,一个是垃圾收集线程。因为每当使用 java 命令执行一个类的时候,实际上都会启动一个JVM,每一个jVM实习在就是在操作系统中启动了一个进程。

# 四、线程状态转换

下面的这个图非常重要!你如果看懂了这个图,那么对于多线程的理解将会更加深刻!

img

1、新建状态(New):新创建了一个线程对象。

2、就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的 start () 方法。该状态的线程位于可运行线程池中,变得可运行,等待获取 CPU 的使用权。

3、运行状态(Running):就绪状态的线程获取了 CPU,执行程序代码。

4、阻塞状态(Blocked):阻塞状态是线程因为某种原因放弃 CPU 使用权,暂时停止运行。直到线程进入就绪状态,才有机会转到运行状态。阻塞的情况分三种:

(一)、等待阻塞:运行的线程执行 wait () 方法,JVM 会把该线程放入等待池中。(wait 会释放持有的锁)

(二)、同步阻塞:运行的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则 JVM 会把该线程放入锁池中。

(三)、其他阻塞:运行的线程执行 sleep () 或 join () 方法,或者发出了 I/O 请求时,JVM 会把该线程置为阻塞状态。当 sleep () 状态超时、join () 等待线程终止或者超时、或者 I/O 处理完毕时,线程重新转入就绪状态。(注意,sleep 是不会释放持有的锁)

5、死亡状态(Dead):线程执行完了或者因异常退出了 run () 方法,该线程结束生命周期。

# 五、线程调度

线程的调度

1、调整线程优先级:Java 线程有优先级,优先级高的线程会获得较多的运行机会。

Java 线程的优先级用整数表示,取值范围是 1~10,Thread 类有以下三个静态常量:

1
2
3
4
5
6
static int MAX_PRIORITY
线程可以具有的最高优先级,取值为10
static int MIN_PRIORITY
线程可以具有的最低优先级,取值为1
static int NORM_PRIORITY
分配给线程的默认优先级,取值为5

Thread 类的 setPriority () 和 getPriority () 方法分别用来设置和获取线程的优先级。

每个线程都有默认的优先级。主线程的默认优先级为 Thread.NORM_PRIORITY。

线程的优先级有继承关系,比如 A 线程中创建了 B 线程,那么 B 将和 A 具有相同的优先级。

JVM 提供了 10 个线程优先级,但与常见的操作系统都不能很好的映射。如果希望程序能移植到各个操作系统中,应该仅仅使用 Thread 类有以下三个静态常量作为优先级,这样能保证同样的优先级采用了同样的调度方式。

2、线程睡眠:Thread.sleep (long millis) 方法,使线程转到阻塞状态。millis 参数设定睡眠的时间,以毫秒为单位。当睡眠结束后,就转为就绪(Runnable)状态。sleep () 平台移植性好。

3、线程等待:Object 类中的 wait () 方法,导致当前的线程等待,直到其他线程调用此对象的 notify () 方法或 notifyAll () 唤醒方法。这个两个唤醒方法也是 Object 类中的方法,行为等价于调用 wait (0) 一样。

4、线程让步:Thread.yield () 方法,暂停当前正在执行的线程对象,把执行机会让给相同或者更高优先级的线程。

5、线程加入:join () 方法,等待其他线程终止。在当前线程中调用另一个线程的 join () 方法,则当前线程转入阻塞状态,直到另一个进程运行结束,当前线程再由阻塞转为就绪状态。

6、线程唤醒:Object 类中的 notify () 方法,唤醒在此对象监视器上等待的单个线程。如果所有线程都在此对象上等待,则会选择唤醒其中一个线程。选择是任意性的,并在对实现做出决定时发生。线程通过调用其中一个 wait 方法,在对象的监视器上等待。 直到当前的线程放弃此对象上的锁定,才能继续执行被唤醒的线程。被唤醒的线程将以常规方式与在该对象上主动同步的其他所有线程进行竞争;例如,唤醒的线程在作为锁定此对象的下一个线程方面没有可靠的特权或劣势。类似的方法还有一个 notifyAll (),唤醒在此对象监视器上等待的所有线程。

注意:Thread 中 suspend () 和 resume () 两个方法在 JDK1.5 中已经废除,不再介绍。因为有死锁倾向。

# 六、常用函数说明

**①sleep (long millis): 在指定的毫秒数内让当前正在执行的线程休眠(暂停执行)

②join (): 指等待 t 线程终止。**

# 使用方式。

join 是 Thread 类的一个方法,启动线程后直接调用,即 join () 的作用是:“等待该线程终止”,这里需要理解的就是该线程是指的主线程等待子线程的终止。也就是在子线程调用了 join () 方法后面的代码,只有等到子线程结束了才能执行。

1
Thread t = new AThread(); t.start(); t.join();

# 为什么要用 join () 方法

在很多情况下,主线程生成并起动了子线程,如果子线程里要进行大量的耗时的运算,主线程往往将于子线程之前结束,但是如果主线程处理完其他的事务后,需要用到子线程的处理结果,也就是主线程需要等待子线程执行完成之后再结束,这个时候就要用到 join () 方法了。

不加 join。

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
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
/**



*@functon 多线程学习,join



*@author 林炳文



*@time 2015.3.9



*/



package com.multithread.join;



class Thread1 extends Thread{



private String name;



public Thread1(String name) {



super(name);



this.name=name;



}



public void run() {



System.out.println(Thread.currentThread().getName() + " 线程运行开始!");



for (int i = 0; i < 5; i++) {



System.out.println("子线程"+name + "运行 : " + i);



try {



sleep((int) Math.random() * 10);



} catch (InterruptedException e) {



e.printStackTrace();



}



}



System.out.println(Thread.currentThread().getName() + " 线程运行结束!");



}



}







public class Main {







public static void main(String[] args) {



System.out.println(Thread.currentThread().getName()+"主线程运行开始!");



Thread1 mTh1=new Thread1("A");



Thread1 mTh2=new Thread1("B");



mTh1.start();



mTh2.start();



System.out.println(Thread.currentThread().getName()+ "主线程运行结束!");







}







}












输出结果:
main 主线程运行开始!
main 主线程运行结束!
B 线程运行开始!
子线程 B 运行 : 0
A 线程运行开始!
子线程 A 运行 : 0
子线程 B 运行 : 1
子线程 A 运行 : 1
子线程 A 运行 : 2
子线程 A 运行 : 3
子线程 A 运行 : 4
A 线程运行结束!
子线程 B 运行 : 2
子线程 B 运行 : 3
子线程 B 运行 : 4
B 线程运行结束!
发现主线程比子线程早结束

加 join

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
public class Main {







public static void main(String[] args) {



System.out.println(Thread.currentThread().getName()+"主线程运行开始!");



Thread1 mTh1=new Thread1("A");



Thread1 mTh2=new Thread1("B");



mTh1.start();



mTh2.start();



try {



mTh1.join();



} catch (InterruptedException e) {



e.printStackTrace();



}



try {



mTh2.join();



} catch (InterruptedException e) {



e.printStackTrace();



}



System.out.println(Thread.currentThread().getName()+ "主线程运行结束!");







}







}

运行结果:
main 主线程运行开始!
A 线程运行开始!
子线程 A 运行 : 0
B 线程运行开始!
子线程 B 运行 : 0
子线程 A 运行 : 1
子线程 B 运行 : 1
子线程 A 运行 : 2
子线程 B 运行 : 2
子线程 A 运行 : 3
子线程 B 运行 : 3
子线程 A 运行 : 4
子线程 B 运行 : 4
A 线程运行结束!
主线程一定会等子线程都结束了才结束

③yield (): 暂停当前正在执行的线程对象,并执行其他线程。

​ Thread.yield () 方法作用是:暂停当前正在执行的线程对象,并执行其他线程。

​ ****yield () 应该做的是让当前运行线程回到可运行状态,以允许具有相同优先级的其他线程获得运行机会。**** 因此,使用 yield () 的目的是让相同优先级的线程之间能适当的轮转执行。但是,实际中无法保证 yield () 达到让步目的,因为让步的线程还有可能被线程调度程序再次选中。

结论:yield () 从未导致线程转到等待 / 睡眠 / 阻塞状态。在大多数情况下,yield () 将导致线程从运行状态转到可运行状态,但有可能没有效果。可看上面的图。

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
/**



*@functon 多线程学习 yield



*@author 林炳文



*@time 2015.3.9



*/



package com.multithread.yield;



class ThreadYield extends Thread{



public ThreadYield(String name) {



super(name);



}







@Override



public void run() {



for (int i = 1; i <= 50; i++) {



System.out.println("" + this.getName() + "-----" + i);



// 当i为30时,该线程就会把CPU时间让掉,让其他或者自己的线程执行(也就是谁先抢到谁执行)



if (i ==30) {



this.yield();



}



}







}



}







public class Main {







public static void main(String[] args) {







ThreadYield yt1 = new ThreadYield("张三");



ThreadYield yt2 = new ThreadYield("李四");



yt1.start();



yt2.start();



}







}

运行结果:

第一种情况:李四(线程)当执行到 30 时会 CPU 时间让掉,这时张三(线程)抢到 CPU 时间并执行。

第二种情况:李四(线程)当执行到 30 时会 CPU 时间让掉,这时李四(线程)抢到 CPU 时间并执行。

sleep () 和 yield () 的区别
sleep () 和 yield () 的区别):sleep () 使当前线程进入停滞状态,所以执行 sleep () 的线程在指定的时间内肯定不会被执行;yield () 只是使当前线程重新回到可执行状态,所以执行 yield () 的线程有可能在进入到可执行状态后马上又被执行。
sleep 方法使当前运行中的线程睡眼一段时间,进入不可运行状态,这段时间的长短是由程序设定的,yield 方法使当前线程让出 CPU 占有权,但让出的时间是不可设定的。实际上,yield () 方法对应了如下操作:先检测当前是否有相同优先级的线程处于同可运行状态,如有,则把 CPU 的占有权交给此线程,否则,继续运行原来的线程。所以 yield () 方法称为 “退让”,它把运行机会让给了同等优先级的其他线程
另外,sleep 方法允许较低优先级的线程获得运行机会,但 yield () 方法执行时,当前线程仍处在可运行状态,所以,不可能让出较低优先级的线程些时获得 CPU 占有权。在一个运行系统中,如果较高优先级的线程没有调用 sleep 方法,又没有受到 I\O 阻塞,那么,较低优先级线程只能等待所有较高优先级的线程运行结束,才有机会运行。

④setPriority (): 更改线程的优先级。

MIN_PRIORITY = 1
   NORM_PRIORITY = 5
MAX_PRIORITY = 10

用法:

1
2
3
4
Thread4 t1 = new Thread4("t1");
Thread4 t2 = new Thread4("t2");
t1.setPriority(Thread.MAX_PRIORITY);
t2.setPriority(Thread.MIN_PRIORITY);

*⑤interrupt (): 不要以为它是中断某个线程!它只是线线程发送一个中断信号,让线程在无限等待时(如死锁时)能抛出抛出,从而结束线程,但是如果你吃掉了这个异常,那么 ** 这个线程还是不会中断的!*

⑥wait()

Obj.wait (),与 Obj.notify () 必须要与 synchronized (Obj) 一起使用,也就是 wait, 与 notify 是针对已经获取了 Obj 锁进行操作,从语法角度来说就是 Obj.wait (),Obj.notify 必须在 synchronized (Obj){…} 语句块内。从功能上来说 wait 就是说线程在获取对象锁后,主动释放对象锁,同时本线程休眠。直到有其它线程调用对象的 notify () 唤醒该线程,才能继续获取对象锁,并继续执行。相应的 notify () 就是对对象锁的唤醒操作。但有一点需要注意的是 notify () 调用后,并不是马上就释放对象锁的,而是在相应的 synchronized (){} 语句块执行结束,自动释放锁后,JVM 会在 wait () 对象锁的线程中随机选取一线程,赋予其对象锁,唤醒线程,继续执行。这样就提供了在线程间同步、唤醒的操作。Thread.sleep () 与 Object.wait () 二者都可以暂停当前线程,释放 CPU 控制权,主要的区别在于 Object.wait () 在释放 CPU 同时,释放了对象锁的控制。

单单在概念上理解清楚了还不够,需要在实际的例子中进行测试才能更好的理解。对 Object.wait (),Object.notify () 的应用最经典的例子,应该是三线程打印 ABC 的问题了吧,这是一道比较经典的面试题,题目要求如下:

建立三个线程,A 线程打印 10 次 A,B 线程打印 10 次 B,C 线程打印 10 次 C,要求线程同时运行,交替打印 10 次 ABC。这个问题用 Object 的 wait (),notify () 就可以很方便的解决。代码如下:

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
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
/**



* wait用法



* @author DreamSea



* @time 2015.3.9



*/



package com.multithread.wait;



public class MyThreadPrinter2 implements Runnable {







private String name;



private Object prev;



private Object self;







private MyThreadPrinter2(String name, Object prev, Object self) {



this.name = name;



this.prev = prev;



this.self = self;



}







@Override



public void run() {



int count = 10;



while (count > 0) {



synchronized (prev) {



synchronized (self) {



System.out.print(name);



count--;







self.notify();



}



try {



prev.wait();



} catch (InterruptedException e) {



e.printStackTrace();



}



}







}



}







public static void main(String[] args) throws Exception {



Object a = new Object();



Object b = new Object();



Object c = new Object();



MyThreadPrinter2 pa = new MyThreadPrinter2("A", c, a);



MyThreadPrinter2 pb = new MyThreadPrinter2("B", a, b);



MyThreadPrinter2 pc = new MyThreadPrinter2("C", b, c);











new Thread(pa).start();



Thread.sleep(100); //确保按顺序A、B、C执行



new Thread(pb).start();



Thread.sleep(100);



new Thread(pc).start();



Thread.sleep(100);



}



}




输出结果:

ABCABCABCABCABCABCABCABCABCABC

先来解释一下其整体思路,从大的方向上来讲,该问题为三线程间的同步唤醒操作,主要的目的就是 ThreadA->ThreadB->ThreadC->ThreadA 循环执行三个线程。为了控制线程执行的顺序,那么就必须要确定唤醒、等待的顺序,所以每一个线程必须同时持有两个对象锁,才能继续执行。一个对象锁是 prev,就是前一个线程所持有的对象锁。还有一个就是自身对象锁。主要的思想就是,为了控制执行的顺序,必须要先持有 prev 锁,也就前一个线程要释放自身对象锁,再去申请自身对象锁,两者兼备时打印,之后首先调用 self.notify () 释放自身对象锁,唤醒下一个等待线程,再调用 prev.wait () 释放 prev 对象锁,终止当前线程,等待循环结束后再次被唤醒。运行上述代码,可以发现三个线程循环打印 ABC,共 10 次。程序运行的主要过程就是 A 线程最先运行,持有 C,A 对象锁,后释放 A,C 锁,唤醒 B。线程 B 等待 A 锁,再申请 B 锁,后打印 B,再释放 B,A 锁,唤醒 C,线程 C 等待 B 锁,再申请 C 锁,后打印 C,再释放 C,B 锁,唤醒 A。看起来似乎没什么问题,但如果你仔细想一下,就会发现有问题,就是初始条件,三个线程按照 A,B,C 的顺序来启动,按照前面的思考,A 唤醒 B,B 唤醒 C,C 再唤醒 A。但是这种假设依赖于 JVM 中线程调度、执行的顺序。
wait 和 sleep 区别
共同点:

\1. 他们都是在多线程的环境下,都可以在程序的调用处阻塞指定的毫秒数,并返回。
\2. wait () 和 sleep () 都可以通过 interrupt () 方法 打断线程的暂停状态 ,从而使线程立刻抛出 InterruptedException。
如果线程 A 希望立即结束线程 B,则可以对线程 B 对应的 Thread 实例调用 interrupt 方法。如果此刻线程 B 正在 wait/sleep/join,则线程 B 会立刻抛出 InterruptedException,在 catch () {} 中直接 return 即可安全地结束线程。
需要注意的是,InterruptedException 是线程自己从内部抛出的,并不是 interrupt () 方法抛出的。对某一线程调用 interrupt () 时,如果该线程正在执行普通的代码,那么该线程根本就不会抛出 InterruptedException。但是,一旦该线程进入到 wait ()/sleep ()/join () 后,就会立刻抛出 InterruptedException 。
不同点:
\1. Thread 类的方法:sleep (),yield () 等
Object 的方法:wait () 和 notify () 等
\2. 每个对象都有一个锁来控制同步访问。Synchronized 关键字可以和对象的锁交互,来实现线程的同步。
sleep 方法没有释放锁,而 wait 方法释放了锁,使得其他线程可以使用同步控制块或者方法。
\3. wait,notify 和 notifyAll 只能在同步控制方法或者同步控制块里面使用,而 sleep 可以在任何地方使用
所以 sleep () 和 wait () 方法的最大区别是:
    sleep () 睡眠时,保持对象锁,仍然占有该锁;
    而 wait () 睡眠时,释放对象锁。
  但是 wait () 和 sleep () 都可以通过 interrupt () 方法打断线程的暂停状态,从而使线程立刻抛出 InterruptedException(但不建议使用该方法)。
sleep()方法
sleep () 使当前线程进入停滞状态(阻塞当前线程),让出 CUP 的使用、目的是不让当前线程独自霸占该进程所获的 CPU 资源,以留一定时间给其他线程执行的机会;
   sleep () 是 Thread 类的 Static (静态) 的方法;因此他不能改变对象的机锁,所以当在一个 Synchronized 块中调用 Sleep () 方法是,线程虽然休眠了,但是对象的机锁并木有被释放,其他线程无法访问这个对象(即使睡着也持有对象锁)。
  在 sleep () 休眠时间期满后,该线程不一定会立即执行,这是因为其它线程可能正在运行而且没有被调度为放弃执行,除非此线程具有更高的优先级。
wait()方法
wait () 方法是 Object 类里的方法;当一个线程执行到 wait () 方法时,它就进入到一个和该对象相关的等待池中,同时失去(释放)了对象的机锁(暂时失去机锁,wait (long timeout) 超时时间到后还需要返还对象锁);其他线程可以访问;
  wait () 使用 notify 或者 notifyAlll 或者指定睡眠时间来唤醒当前等待池中的线程。
  wiat () 必须放在 synchronized block 中,否则会在 program runtime 时扔出”java.lang.IllegalMonitorStateException“异常。

# 七、常见线程名词解释

主线程:JVM 调用程序 main () 所产生的线程。

当前线程:这个是容易混淆的概念。一般指通过 Thread.currentThread () 来获取的进程。

后台线程:指为其他线程提供服务的线程,也称为守护线程。JVM 的垃圾回收线程就是一个后台线程。 用户线程和守护线程的区别在于,是否等待主线程依赖于主线程结束而结束

前台线程:是指接受后台线程服务的线程,其实前台后台线程是联系在一起,就像傀儡和幕后操纵者一样的关系。傀儡是前台线程、幕后操纵者是后台线程。由前台线程创建的线程默认也是前台线程。可以通过 isDaemon () 和 setDaemon () 方法来判断和设置一个线程是否为后台线程。

** 线程类的一些常用方法:

sleep (): 强迫一个线程睡眠N毫秒。
  isAlive (): 判断一个线程是否存活。
  join (): 等待线程终止。
  activeCount (): 程序中活跃的线程数。
  enumerate (): 枚举程序中的线程。
currentThread (): 得到当前线程。
  isDaemon (): 一个线程是否为守护线程。
  setDaemon (): 设置一个线程为守护线程。(用户线程和守护线程的区别在于,是否等待主线程依赖于主线程结束而结束)
  setName (): 为线程设置一个名称。
  wait (): 强迫一个线程等待。
  notify (): 通知一个线程继续运行。
  setPriority (): 设置一个线程的优先级。
**

# 八、线程同步

1、synchronized 关键字的作用域有二种:
1)是某个对象实例内,synchronized aMethod (){} 可以防止多个线程同时访问这个对象的 synchronized 方法(如果一个对象有多个 synchronized 方法,只要一个线程访问了其中的一个 synchronized 方法,其它线程不能同时访问这个对象中任何一个 synchronized 方法)。这时,不同的对象实例的 synchronized 方法是不相干扰的。也就是说,其它线程照样可以同时访问相同类的另一个对象实例中的 synchronized 方法;
2)是某个类的范围,synchronized static aStaticMethod {} 防止多个线程同时访问这个类中的 synchronized static 方法。它可以对类的所有对象实例起作用。

2、除了方法前用 synchronized 关键字,synchronized 关键字还可以用于方法中的某个区块中,表示只对这个区块的资源实行互斥访问。用法是: synchronized (this){/ 区块 /},它的作用域是当前对象;

3、synchronized 关键字是不能继承的,也就是说,基类的方法 synchronized f (){} 在继承类中并不自动是 synchronized f (){},而是变成了 f (){}。继承类需要你显式的指定它的某个方法为 synchronized 方法;

Java 对多线程的支持与同步机制深受大家的喜爱,似乎看起来使用了 synchronized 关键字就可以轻松地解决多线程共享数据同步问题。到底如何?――还得对 synchronized 关键字的作用进行深入了解才可定论。

总的说来,synchronized 关键字可以作为函数的修饰符,也可作为函数内的语句,也就是平时说的同步方法和同步语句块。如果再细的分类,synchronized 可作用于 instance 变量、object reference(对象引用)、static 函数和 class literals (类名称字面常量) 身上。

在进一步阐述之前,我们需要明确几点:

A.无论 synchronized 关键字加在方法上还是对象上,它取得的锁都是对象,而不是把一段代码或函数当作锁――而且同步方法很可能还会被其他线程的对象访问。

B.每个对象只有一个锁(lock)与之相关联。

C.实现同步是要很大的系统开销作为代价的,甚至可能造成死锁,所以尽量避免无谓的同步控制。

接着来讨论 synchronized 用到不同地方对代码产生的影响:

假设 P1、P2 是同一个类的不同对象,这个类中定义了以下几种情况的同步块或同步方法,P1、P2 就都可以调用它们。

1. 把 synchronized 当作函数修饰符时,示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
Public synchronized void methodAAA()



{



//….



}

这也就是同步方法,那这时 synchronized 锁定的是哪个对象呢?它锁定的是调用这个同步方法对象。也就是说,当一个对象 P1 在不同的线程中执行这个同步方法时,它们之间会形成互斥,达到同步的效果。但是这个对象所属的 Class 所产生的另一对象 P2 却可以任意调用这个被加了 synchronized 关键字的方法。

上边的示例代码等同于如下代码:

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
public void methodAAA()



{



synchronized (this) // (1)



{



//…..



}



}

(1) 处的 this 指的是什么呢?它指的就是调用这个方法的对象,如 P1。可见同步方法实质是将 synchronized 作用于 object reference。――那个拿到了 P1 对象锁的线程,才可以调用 P1 的同步方法,而对 P2 而言,P1 这个锁与它毫不相干,程序也可能在这种情形下摆脱同步机制的控制,造成数据混乱:(

2.同步块,示例代码如下:

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
            public void method3(SomeObject so)



{



synchronized(so)



{



//…..



}



}

这时,锁就是 so 这个对象,谁拿到这个锁谁就可以运行它所控制的那段代码。当有一个明确的对象作为锁时,就可以这样写程序,但当没有明确的对象作为锁,只是想让一段代码同步时,可以创建一个特殊的 instance 变量(它得是一个对象)来充当锁:

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
class Foo implements Runnable



{



private byte[] lock = new byte[0]; // 特殊的instance变量



Public void methodA()



{



synchronized(lock) { //… }



}



//…..



}

注:零长度的 byte 数组对象创建起来将比任何对象都经济――查看编译后的字节码:生成零长度的 byte [] 对象只需 3 条操作码,而 Object lock = new Object () 则需要 7 行操作码。

3.将 synchronized 作用于 static 函数,示例代码如下:

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
Class Foo



{



public synchronized static void methodAAA() // 同步的static 函数



{



//….



}



public void methodBBB()



{



synchronized(Foo.class) // class literal(类名称字面常量)



}



}

代码中的 methodBBB () 方法是把 class literal 作为锁的情况,它和同步的 static 函数产生的效果是一样的,取得的锁很特别,是当前调用这个方法的对象所属的类(Class,而不再是由这个 Class 产生的某个具体对象了)。

记得在《Effective Java》一书中看到过将 Foo.class 和 P1.getClass () 用于作同步锁还不一样,不能用 P1.getClass () 来达到锁这个 Class 的目的。P1 指的是由 Foo 类产生的对象。

可以推断:如果一个类中定义了一个 synchronized 的 static 函数 A,也定义了一个 synchronized 的 instance 函数 B,那么这个类的同一对象 Obj 在多线程中分别访问 A 和 B 两个方法时,不会构成同步,因为它们的锁都不一样。A 方法的锁是 Obj 这个对象,而 B 的锁是 Obj 所属的那个 Class。

总结一下:

1、线程同步的目的是为了保护多个线程反问一个资源时对资源的破坏。
2、线程同步方法是通过锁来实现,每个对象都有切仅有一个锁,这个锁与一个特定的对象关联,线程一旦获取了对象锁,其他访问该对象的线程就无法再访问该对象的其他非同步方法
3、对于静态同步方法,锁是针对这个类的,锁对象是该类的 Class 对象。静态和非静态方法的锁互不干预。一个线程获得锁,当在一个同步方法中访问另外对象上的同步方法时,会获取这两个对象锁。
4、对于同步,要时刻清醒在哪个对象上同步,这是关键。
5、编写线程安全的类,需要时刻注意对多个线程竞争访问资源的逻辑和安全做出正确的判断,对 “原子” 操作做出分析,并保证原子操作期间别的线程无法访问竞争资源。
6、当多个线程等待一个对象锁时,没有获取到锁的线程将发生阻塞。
7、死锁是线程间相互等待锁锁造成的,在实际中发生的概率非常的小。真让你写个死锁程序,不一定好使,呵呵。但是,一旦程序发生死锁,程序将死掉。

# 九、线程数据传递

在传统的同步开发模式下,当我们调用一个函数时,通过这个函数的参数将数据传入,并通过这个函数的返回值来返回最终的计算结果。但在多线程的异步开发模式下,数据的传递和返回和同步开发模式有很大的区别。由于线程的运行和结束是不可预料的,因此,在传递和返回数据时就无法象函数一样通过函数参数和 return 语句来返回数据。

9.1、通过构造方法传递数据
在创建线程时,必须要建立一个 Thread 类的或其子类的实例。因此,我们不难想到在调用 start 方法之前通过线程类的构造方法将数据传入线程。并将传入的数据使用类变量保存起来,以便线程使用 (其实就是在 run 方法中使用)。下面的代码演示了如何通过构造方法来传递数据:

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
package mythread; 



public class MyThread1 extends Thread



{



private String name;



public MyThread1(String name)



{



this.name = name;



}



public void run()



{



System.out.println("hello " + name);



}



public static void main(String[] args)



{



Thread thread = new MyThread1("world");



thread.start();



}



}

由于这种方法是在创建线程对象的同时传递数据的,因此,在线程运行之前这些数据就就已经到位了,这样就不会造成数据在线程运行后才传入的现象。如果要传递更复杂的数据,可以使用集合、类等数据结构。使用构造方法来传递数据虽然比较安全,但如果要传递的数据比较多时,就会造成很多不便。由于 Java 没有默认参数,要想实现类似默认参数的效果,就得使用重载,这样不但使构造方法本身过于复杂,又会使构造方法在数量上大增。因此,要想避免这种情况,就得通过类方法或类变量来传递数据。

9.2、通过变量和方法传递数据
向对象中传入数据一般有两次机会,第一次机会是在建立对象时通过构造方法将数据传入,另外一次机会就是在类中定义一系列的 public 的方法或变量(也可称之为字段)。然后在建立完对象后,通过对象实例逐个赋值。下面的代码是对 MyThread1 类的改版,使用了一个 setName 方法来设置 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
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
package mythread; 



public class MyThread2 implements Runnable



{



private String name;



public void setName(String name)



{



this.name = name;



}



public void run()



{



System.out.println("hello " + name);



}



public static void main(String[] args)



{



MyThread2 myThread = new MyThread2();



myThread.setName("world");



Thread thread = new Thread(myThread);



thread.start();



}



}

9.3、通过回调函数传递数据

上面讨论的两种向线程中传递数据的方法是最常用的。但这两种方法都是 main 方法中主动将数据传入线程类的。这对于线程来说,是被动接收这些数据的。然而,在有些应用中需要在线程运行的过程中动态地获取数据,如在下面代码的 run 方法中产生了 3 个随机数,然后通过 Work 类的 process 方法求这三个随机数的和,并通过 Data 类的 value 将结果返回。从这个例子可以看出,在返回 value 之前,必须要得到三个随机数。也就是说,这个 value 是无法事先就传入线程类的。

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
150
151
152
153
package mythread; 



class Data



{



public int value = 0;



}



class Work



{



public void process(Data data, Integer numbers)



{



for (int n : numbers)



{



data.value += n;



}



}



}



public class MyThread3 extends Thread



{



private Work work;



public MyThread3(Work work)



{



this.work = work;



}



public void run()



{



java.util.Random random = new java.util.Random();



Data data = new Data();



int n1 = random.nextInt(1000);



int n2 = random.nextInt(2000);



int n3 = random.nextInt(3000);



work.process(data, n1, n2, n3); // 使用回调函数



System.out.println(String.valueOf(n1) + "+" + String.valueOf(n2) + "+"



+ String.valueOf(n3) + "=" + data.value);



}



public static void main(String[] args)



{



Thread thread = new MyThread3(new Work());



thread.start();



}



}

好了,Java 多线程的基础知识就讲到这里了,有兴趣研究多线程的推荐直接看 java 的源码,你将会得到很大的提升!

# 关于我

Brath 是一个热爱技术的 Java 程序猿,公众号「InterviewCoder」定期分享有趣有料的精品原创文章!

InterviewCoder

非常感谢各位人才能看到这里,原创不易,文章如果有帮助可以关注、点赞、分享或评论,这都是对我的莫大支持!

JVM学习记录

InterviewCoder

# JVM 位置:在操作系统之上运行,操作系统在硬件之上运行

JDK(JRE(JVM)): JDK 包含了 JRE,JRE 包含了 JVM

# JVM 体系结构:

JVM底层结构画图(精细)

Java File

=> Class File

=> Class Loader SubSystem {

1 . Loading : [1 . ApplicationClassLoader 2 . ExtClassLoader 3 . BootStrapClassLoader]

2 . Linking : [1. Verify 2.Prepare 3. Resolve ]

3 . Initialization

}

=> RuntimeData Areas :(1,2) : Memory sharing,(3,4,5) : Memory is not shared{

1 . Method Area

2 . Heap Area

3 . Stack Area {

T1 [1. Thread 2. Stack Frame ]

T2 [1. Thread 2. Stack Frame ]

}

4 . Native MethodStack Area --> JNI

5 . Program Counter Register [PC Registers for Thread]

}

=> Execution Engine {

1 . interpreter

2 . JIT Compiler {

Intermediate Code Generator

=> Code Optimizer

=> Target Code Generator

}

3 . Profiler

4 . Garbage Collection

}

=> Java Native Method Interface (JNI)

=> Native Method Library

# 类加载器: ClassLoader SubSystem : 类加载器子系统 运行时在堆中运行 不运行时是独立子系统

application ClassLoader 应用程序加载器 主要负责加载应用程序的主函数类

Ext ClassLoader 扩展加载器 主要负责加载 jre/lib/ext 目录下的一些扩展的 jar。

BootStrap ClassLoader 根类加载器 主要负责加载核心的类库 (java.lang.* 等)

加载过程:class File => Loading 加载 => Linking (验证,准备,解析) 链接 => Initialization 初始化

# 双亲委派机制

img

从上图中我们就更容易理解了,当一个 Hello.class 这样的文件要被加载时。不考虑我们自定义类加载器,首先会在 AppClassLoader 中检查是否加载过,如果有那就无需再加载了。如果没有,那么会拿到父加载器,然后调用父加载器的 loadClass 方法。父类中同理也会先检查自己是否已经加载过,如果没有再往上。注意这个类似递归的过程,直到到达 Bootstrap classLoader 之前,都是在检查是否加载过,并不会选择自己去加载。直到 BootstrapClassLoader,已经没有父加载器了,这时候开始考虑自己是否能加载了,如果自己无法加载,会下沉到子加载器去加载,一直到最底层,如果没有任何加载器能加载,就会抛出 ClassNotFoundException。那么有人就有下面这种疑问了?

为什么要设计这种机制
这种设计有个好处是,如果有人想替换系统级别的类:String.java。篡改它的实现,在这种机制下这些系统的类已经被 Bootstrap classLoader 加载过了(为什么?因为当一个类需要加载的时候,最先去尝试加载的就是 BootstrapClassLoader),所以其他类加载器并没有机会再去加载,从一定程度上防止了危险代码的植入。

沙箱安全机制

在 Java 中将执行程序分成本地代码和远程代码两种,本地代码默认视为可信任的,而远程代码则被看作是不受信的。对于授信的本地代码,可以访问一切本地资源。而对于非授信的远程代码在早期的 Java 实现中,安全依赖于沙箱 (Sandbox) 机制。

img

当前最新的安全机制实现,则引入了域 (Domain) 的概念。虚拟机会把所有代码加载到不同的系统域和应用域,系统域部分专门负责与关键资源进行交互,而各个应用域部分则通过系统域的部分代理来对各种需要的资源进行访问。虚拟机中不同的受保护域 (Protected Domain),对应不一样的权限 (Permission)。存在于不同域中的类文件就具有了当前域的全部权限,如下图所示

# Native

native 即 JNI,Java Native Interface

Native 关键字是 JNI,也就是 java 本地方法接口,用来调用本地方法库,可以调用本地方法中的 C 语言代码

# PC 寄存器

每次线程启动的时候会创建一个 PC 寄存器,保存正在执行的 JVM 指令地址,每个线程都有自己的 PC 寄存器,是一个比较小的内存空间,是为一个一个不会出现 OutOfMemoryError 的内存区域,它的生命周期随着线程的创建而创建,随着线程的结束而死亡。

# 方法区

Hotspot 虚拟机,方法区别称:non-heap(非堆),其实就是存储堆类型的数据,而不占据堆内存的空间

方法区和堆区线程共享,方法区大小和堆一样,可以选择固定大小或者拓展

方法区大小决定了系统可以保存多少个类,如果系统定义了太多的类,导致方法区溢出,虚拟机同样会抛出内存溢出错误。

关闭 JVM 就会释放这个区域的内存

设置方法区的内存大小:

# JDK1.7 之前:

-XX:PermSize 设置永久代初始分配空间 默认是 20.75m

-XX:MaxPermSize 设置最大永久代分配空间,32 位机器默认 64m,64 位机器默认 82m

# JDK1.8 及以后:永久代被元空间代替(MetaSpace)

元空间大小可以用参数:-XX:MetaspaceSzie 和 -XX:MaxMetaspaceSize 指定

-XX:MetaspaceSzie 默认 21.75m -XX:MaxMetaspaceSize 默认 - 1 没有限制

默认情况下,虚拟机会耗尽所有可用的系统内存,如果元空间发生溢出,虚拟机照样会抛出 OOM

如果初始高位线设置过低,通过垃圾回收器日志可以观察到 Full GC 多次调用,为了避免频繁 GC,

建议将 - XX:MetaspaceSize 设置为一个相对较高的值。

# 栈:数据结构

程序 = 数据结构 + 算法

先进后出,后进先出

栈是桶的概念:先进去的后出来,后进去的先出来

栈有压栈、弹栈

队列是管道概念:先进先出 (F I F O) ,后进后出 First Input First Out

喝多了吐就是栈,吃多了拉就是队列

栈:主管程序运行,生命周期和线程同步;

main 主线程结束,栈内存也就释放了,对于栈来说,不存在垃圾回收问题,一旦线程结束,栈就 Over 了。

栈存什么东西:

八大基本数据类型 + 对象的引用地址 + 实例方法

栈运行原理:栈帧:方法索引,输入输出参数,本地变量,类引用,父帧,子帧

栈满了就会抛出错误 Error:StackOverflowError 栈溢出

# 堆:Heap

​ 一个 JVM 只有一个堆内存,堆内存大小是可以调节的。

​ 类加载器读取的类文件后,一般会把:类,方法,常量,变量,引用类型的真实对象放到堆中。

​ 堆内存还要细分为三个区域:{

新生代:Eden Space:

Survior0 区和 Survior1 区:幸存者 0/1 区

经过新生代的轻 GC 15 次的考验进入老年代。

老年代:重量级 Full GC 垃圾回收

永久代:(1.8 移除)

GC 垃圾回收集中在新生代和老年代执行。

假设内存满了,爆出 OOM,堆内存溢出。

内存溢出代码怎么写:

利用 while 循环一个字符串无限 += 随机数

JDK8 以后,永久代被移除,更名为元空间:MetaSpace

}

# 详解:

# 新生代:

​ 类:诞生成长的地方,也可能类死亡的地方

​ Eden 区、幸存者 0 和 1 区,

​ 所有对象都是在 Eden 区 new 出来的

​ Eden 区没死亡的对象在幸存者区存活

​ 如果 Eden 区存储触发了轻 GC 回收机制,就会对 Eden 区进行清除,存活下来对象在幸存区,清除的对象会消失。当 Eden 区和幸存者区都满了之后会执行一次 Full GC,再次存活下来的对象会进入老年代。

Tips:新生代 99% 都是临时对象!,能进入老年代的对象并不多。所以平时很少见到 OOM 的错误

# 老年代:

每次发生轻 GC 会对新生代进行对象清理,当新生代和幸存区的对象在轻 GC 清除下存活 15 次之后,进入老年代

永久代:用来存放 jdk 自身携带的 calss 对象,interface 元数据,存储的是运行的一些时环境,永久代不存在垃圾回收,关闭 JJVM 虚拟机就会释放永久代内存。

假设一个启动类,加载了大量的第三方 jar 包,或者一个 tomcat 部署了太多应用,或者大量动态生成的反射类,如果不断的加载,可能会导致永久代内存溢出。

jdk1.6 之前:永久代,常量池存在于方法区中

jdk1.7:去永久代,常量池在堆中

jdk1.8:无永久代,常量池在元空间中

元空间:

(方法区):非堆

非堆指的是空间上,不属于堆空间;但是由于存储的内容,特性上又被称为堆

默认情况下:虚拟机被分配到的总内存是电脑内存的 1/4 ,而初始化的内存只有 1/64

因为与堆共享内存,逻辑上存在,物理上不存在

image-20210908225948384

image-20210908230652551

dump 文件~Jprofiler 插件

-Xms : 初始化内存

-Xmx : 最大内存

-XX:+PrintGCDetails 打印 GC 回收的详细信息

-XX:+HeapDumpOnOutOfMemoryError 发生 OOM 异常打印 dump 内存快照

-XX:MaxTenuringReshlod 设置最大存活时间,默认是 15 次

GC:垃圾回收

JVM 在进行 GC 时:大部分回收都在新生代,并不是三个区都回收

新生代

幸存区(form、to)Survior0、Survior1 区

老年代

GC 两个种类:轻 GC(GC)、重 GC(Full GC)

轻 GC 对新生代和幸存区进行回收

重 GC 进行全局回收

如何区分 from 和 to 区:谁空谁是 to 区

1 . 每次 GC 都会将 Eden 区活的对象移到幸存区中:一旦 Eden 区被 GC 后,就会是空的!

2 . 当一个对象经历了 15 次 GC,还没有死,就会进入老年代

-XX:MaxTenuringReshold,通过这个参数可以设定进入老年代的时间(指定在 0~15 次之间)

# 常用算法:

**1 . 标记清除 **

分为两步骤:标记 ---- 清除

标记:扫描,对存活的对象进行标记

清除:扫描,对没有标记的对象进行清除

优点:简单,成功率高

缺点:两次扫描严重耗时,清除会产生内存碎片

2 . 标记压缩(标记 — 清除 — 压缩)

标记清除的优化版:防止内存碎片产生

在标记清除基础上,再次扫描,向一端移动仍存活的对象,清除另一外的碎片

优点:在标记清除优点上优化了内存碎片

缺点:对于标记清除又多了一次移动成本,时间增加

3 . 复制算法(新生代主要用的复制算法)

把 from 区向 to 区复制一份,然后清空 from 区,这时 from 区会变成 to 区,复制过去的 to 区变成 from 区等待回收。

优点:没有内存碎片

缺点:浪费内存空间(多了一半空间永远是 to 区,假设对象 100% 存活)极端情况下不适用

复制算法最佳使用场景:对象存活度较低的时候,新生区使用复制算法!

4 . 分代收集算法:

由于每个收集算法都无法符合所有的场景,就好比每个对象所在的内存阶段不一样,被回收的概率也不一样,比如在新生代,基本 90% 的对象会被回收,而到了老年代则一半以上的对象存活,所以针对不同的场景,回收的策略也就不一样,所以引出了分代收集算法,根据新生代和老年代不同的场景下使用不同的算法,比如新生代用复制算法,老年代则用标记整理算法

5. 引用计数器(不高效)

给每个对象设置计数器,只要有引用就会计数,当一个对象计数器为 0 时,进行回收。缺点:效率低下,现在基本不用

# 总结:

内存效率:复制算法 > 标记清除算法 > 标记压缩(时间复杂度)

内存整齐度:复制算法 == 标记压缩算法 > 标记清除算法

内存利用率:标记压缩算法 == 标记清除算法 > 复制算法

// 思考一个问题:难道没有最优算法吗?

答案:永远不能有最优算法。只有最合适的算法。

GC:分代收集算法:根据每个代需求来配置不同算法

年轻代:存活率低:需求时间短:所以用复制算法

老年代:存活率高、区域大:标记清除 + 标记压缩混合

内存碎片不是很多就标记清除,内存碎片太多就用压缩

JMM:

1 . 什么是 JMM:Java 内存模型(Java Memory Model)

2 . JMM 作用:

3 . 如何学习:

经历过很多面试大部分都会问一句: 你知道 Java 内存模型么? 然后我就 pulapula 的说一大堆什么堆呀,栈呀,GC 呀什么的,这段时间把 JVM 虚拟机和多线程编程完整的学习了一遍,发现 JMM 和堆 / 栈这些完全不是一个概念,不知道是不是就是因为这才被拒了十来次的 / 尴尬。

JVM 是 Java 实现的虚拟计算机(Java Virtual Machine),对于熟悉计算机结构的同学,我感觉把这些概念和物理机对应起来更好理解。

JVM 对应的就是物理机,它有存放数据的存储区:堆、栈等由 JVM 管理的内存(对应于物理机的内存)、执行数据计算的执行单元:线程(对应于物理机的 CPU)、加速线程执行的本次存储区:可能会从存储区里分配一块空间来存储线程本地数据,比如栈(对应于物理机的 cache)。

众所周知,现代计算机一般都会包含多个处理器,多个处理器共享主内存。为了提升性能,会在每个处理器上增加一个小容量的 cache 加速数据读写。cache 会导致了缓存一致性问题,为了解决缓存一致性问题又引入了一系列 Cache 一致性协议(比如 MSI、MESI、MOSI、Synapse、Firefly 及 Dragon Protocol)来解决 CPU 本地缓存和主内存数据不一致问题。

而 JVM 中管理下的存储空间(包括堆、栈等)就对应与物理机的内存;

线程本次存储区(例如栈)就对应于物理机的 cache;

而 JMM 就对应于类似于 MSI、MESI、MOSI、Synapse、Firefly 及 Dragon Protocol 这样的缓存一致性协议,用于定义数据读写的规则。

JMM 相对于物理机的缓存一致性协议来说它还要处理 JVM 自身特有的问题:重排序问题,参见: http://cmsblogs.com/?p=2116。

那么 JMM 都有哪些内容呢?

官方文档: http://101.96.10.64/www.cs.umd.edu/~pugh/java/memoryModel/CommunityReview.pdf

通俗理解就是 happens-before 原则 https://www.cnblogs.com/chenssy/p/6393321.html

# 关于我

Brath 是一个热爱技术的 Java 程序猿,公众号「InterviewCoder」定期分享有趣有料的精品原创文章!

InterviewCoder

非常感谢各位人才能看到这里,原创不易,文章如果有帮助可以关注、点赞、分享或评论,这都是对我的莫大支持!

mysql查询技巧

InterviewCoder

# mysql 查询当天、昨天、本周、上周、本月、上月、今年、去年数据

# mysql 查询今天、昨天、7 天、近 30 天、本月、上一月 数据

今天 select * from 表名 where to_days (时间字段名) = to_days (now ());

昨天 SELECT * FROM 表名 WHERE TO_DAYS (NOW () ) - TO_DAYS ( 时间字段名) = 1

近 7 天 SELECT * FROM 表名 where DATE_SUB (CURDATE (), INTERVAL 7 DAY) <= date (时间字段名)

查询当前这周的数据
SELECT name,submittime FROM enterprise WHERE YEARWEEK (date_format (submittime,’% Y-% m-% d’)) = YEARWEEK (now ());

查询上周的数据
SELECT name,submittime FROM enterprise WHERE YEARWEEK (date_format (submittime,’% Y-% m-% d’)) = YEARWEEK (now ())-1;

近 30 天 SELECT * FROM 表名 where DATE_SUB (CURDATE (), INTERVAL 30 DAY) <= date (时间字段名) 本月 SELECT * FROM 表名 WHERE DATE_FORMAT ( 时间字段名,‘% Y% m’ ) = DATE_FORMAT ( CURDATE ( ) , ‘% Y% m’ )

上一月 SELECT * FROM 表名 WHERE PERIOD_DIFF (date_format ( now () , ‘% Y% m’ ) , date_format ( 时间字段名,‘% Y% m’ ) ) =1

查询距离当前现在 6 个月的数据
select name,submittime from enterprise where submittime between date_sub (now (),interval 6 month) and now (); #查询本季度数据 select * from ht_invoice_information where QUARTER(create_date)=QUARTER(now());

查询上季度数据 select * from ht_invoice_information where QUARTER(create_date)=QUARTER(DATE_SUB(now(),interval 1 QUARTER));

查询本年数据 select * from ht_invoice_information where YEAR(create_date)=YEAR(NOW());

查询上年数据 select * from ht_invoice_information where year(create_date)=year(date_sub(now(),interval 1 year));

# 关于我

Brath 是一个热爱技术的 Java 程序猿,公众号「InterviewCoder」定期分享有趣有料的精品原创文章!

InterviewCoder

非常感谢各位人才能看到这里,原创不易,文章如果有帮助可以关注、点赞、分享或评论,这都是对我的莫大支持!

ElasticSearch 处理检索海量数据“神器”?

InterviewCoder


海量数据我们是如何去检索数据呢,如何快速定位呢,去查询后台数据库吗?还是走缓存,是什么缓存能承载这么大的符合呢,并且快速检索出来?对于海量的数据是对系统极大的压力我们该从什么角度去处理这个棘手的问题呢

在这里插入图片描述

# ElasticSearch 处理检索海量数据 “神器”?

在这里插入图片描述

# 1.1 介绍

Elasticsearch 是一个基于 Lucene 的搜索服务器。它提供了一个分布式多用户能力的全文搜索引擎,基于 RESTful
web 接口。Elasticsearch 是用 Java 语言开发的,并作为 Apache 许可条款下的开放源码发布,是一种流行的企业级搜索引擎。Elasticsearch 用于云计算中,能够达到实时搜索,稳定,可靠,快速,安装使用方便。官方客户端在 Java、.NET(C#)、PHP、Python、Apache
Groovy、Ruby 和许多其他语言中都是可用的。根据 DB-Engines 的排名显示,Elasticsearch 是最受欢迎的企业搜索引擎,其次是 Apache
Solr,也是基于 Lucene。

# 1.2 es 介绍 “官网地址们”

  1. 官方文档
  2. 中文官方文档 3. 中文社区

# 2.1 基本介绍

1、Index(索引) 动词,相当于 MySQL 中的 insert; 名词,相当于 MySQL 中的 Database
2、Type(类型) 在 Index(索引)中,可以定义一个或多个类型。 类似于 MySQL 中的 Table;每一种类型的数据放在一起;
3、Document(文档)
保存在某个索引(Index)下,某种类型(Type)的一个数据(Document),文档是 JSON 格
式的,Document 就像是 MySQL 中的某个 Table 里面的内容;

数据概念

# 2.2 ES 是如何进行检索的呢

在这里插入图片描述

# 2.3 ElasticSearch 长啥样呢,有无操作界面

ElasticSearch
是有操作界面的,它需要配置 Kibana,和它一起操作方便,也是主流的一种搭配方式,安装这两个工具,不做过多介绍

Kibana 介绍 Kibana 是一款开源的数据分析和可视化平台,它是 Elastic Stack 成员之一,设计用于和 Elasticsearch 协作。您可以使用 Kibana 对 Elasticsearch 索引中的数据进行搜索、查看、交互操作。您可以很方便的利用图表、表格及地图对数据进行多元化的分析和呈现

在这里插入图片描述

# 2.4 初步检索

1
2
3
4
5
6
7
8
9
10
11
12
13
14
1、_cat
GET /_cat/nodes:查看所有节点
GET /_cat/health:查看 es 健康状况
GET /_cat/master:查看主节点
GET /_cat/indices:查看所有索引 show databases;
2、索引一个文档(保存)
保存一个数据,保存在哪个索引的哪个类型下,指定用哪个唯一标识
PUT customer/external/1;在 customer 索引下的 external 类型下保存 1 号数据为
12345678
PUT 和 POST 都可以,
POST 新增。如果不指定 id,会自动生成 id。指定 id 就会修改这个数据,并新增版本号
PUT 可以新增可以修改。PUT 必须指定 id;由于 PUT 需要指定 id,我们一般都用来做修改
操作,不指定 id 会报错
1234

# 2.4.1 在 postMan 测试数据

1
2
3
4
PUT customer/external/1
{ "name": "John Doe"
}
123

2.4.2、查询文档

1
2
3
4
5
6
7
8
9
10
11
12
13
14
GET customer/external/1
1
结果:
{ "_index": "customer", //在哪个索引
"_type": "external", //在哪个类型
"_id": "1", //记录 id
"_version": 2, //版本号
"_seq_no": 1, //并发控制字段,每次更新就会+1,用来做乐观锁
"_primary_term": 1, //同上,主分片重新分配,如重启,就会变化
"found": true, "_source": { //真正的内容
"name": "John Doe"
}
}
1234567891011

2.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
POST customer/external/1/_update
{ "doc":{ "name": "John Doew"
}
}
或者
POST customer/external/1
{ "name": "John Doe2"
}
或者
PUT customer/external/1
{ "name": "John Doe"
}
? 不同:POST 操作会对比源文档数据,如果相同不会有什么操作,文档 version 不增加
PUT 操作总会将数据重新保存并增加 version 版本;
带_update 对比元数据如果一样就不进行任何操作。
看场景;
对于大并发更新,不带 update;
对于大并发查询偶尔更新,带 update;对比更新,重新计算分配规则。
? 更新同时增加属性
POST customer/external/1/_update
{ "doc": { "name": "Jane Doe", "age": 20 }
}
PUT 和 POST 不带_update 也可以
1234567891011121314151617181920212223

2.4.4、删除文档 & 索引

1
2
3
DELETE customer/external/1
DELETE customer
12

2.4.5 bulk 批量 API

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
POST customer/external/_bulk
{"index":{"_id":"1"}}
{"name": "John Doe" }
{"index":{"_id":"2"}}
{"name": "Jane Doe" }
语法格式:
{ action: { metadata }}\n
{ request body }\n
{ action: { metadata }}\n
{ request body }\n
复杂实例:
POST /_bulk
{ "delete": { "_index": "website", "_type": "blog", "_id": "123" }}
{ "create": { "_index": "website", "_type": "blog", "_id": "123" }}
{ "title": "My first blog post" }
{ "index": { "_index": "website", "_type": "blog" }}
{ "title": "My second blog post" }
{ "update": { "_index": "website", "_type": "blog", "_id": "123", "_retry_on_conflict" : 3} }
{ "doc" : {"title" : "My updated blog post"} }
bulk API 以此按顺序执行所有的 action(动作)。如果一个单个的动作因任何原因而失败,
它将继续处理它后面剩余的动作。当 bulk API 返回时,它将提供每个动作的状态(与发送
的顺序相同),所以您可以检查是否一个指定的动作是不是失败了。
12345678910111213141516171819202122

# 2.4 SearchAPI

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
ES 支持两种基本方式检索 :
? 一个是通过使用 REST request URI 发送搜索参数(uri+检索参数)
? 另一个是通过使用 REST request body 来发送它们(uri+请求体)
1)、检索信息
? 一切检索从_search 开始
GET bank/_search 检索 bank 下所有信息,包括 type 和 docs
GET bank/_search?q=*&sort=account_number:asc 请求参数方式检索
响应结果解释:
took - Elasticsearch 执行搜索的时间(毫秒)
time_out - 告诉我们搜索是否超时
_shards - 告诉我们多少个分片被搜索了,以及统计了成功/失败的搜索分片
hits - 搜索结果
hits.total - 搜索结果
hits.hits - 实际的搜索结果数组(默认为前 10 的文档)
sort - 结果的排序 key(键)(没有则按 score 排序)
score 和 max_score –相关性得分和最高得分(全文检索用)
12345678910111213141516

其他在 Kibana 操作的语句,都可以在 es 官网去查询,不做过多的赘述!!!

# 2.5、Mapping 映射

在这里插入图片描述

Mapping(映射) Mapping 是用来定义一个文档(document),以及它所包含的属性(field)是如何存储和
索引的。比如,使用 mapping 来定义: ? 哪些字符串属性应该被看做全文本属性(full text fields)。 ?
哪些属性包含数字,日期或者地理位置。 ? 文档中的所有属性是否都能被索引(_all 配置)。 ? 日期的格式。 ?
自定义映射规则来执行动态添加属性。

# 2.6 分词

个 tokenizer(分词器)接收一个字符流,将之分割为独立的 tokens(词元,通常是独立 的单词),然后输出 tokens 流。
例如,whitespace tokenizer 遇到空白字符时分割文本。它会将文本 “Quick brown fox!” 分割 为
[Quick, brown, fox!]。 该 tokenizer(分词器)还负责记录各个 term(词条)的顺序或 position
位置(用于 phrase 短 语和 word proximity 词近邻查询),以及 term(词条)所代表的原始 word(单词)的
start (起始)和 end(结束)的 character offsets(字符偏移量)(用于高亮显示搜索的内容)。
Elasticsearch 提供了很多内置的分词器,可以用来构建 custom analyzers(自定义分词器)。

在这里插入图片描述
2.6.1 安装分词器

注意:不能用默认 elasticsearch-plugin install xxx.zip 进行自动安装
https://github.com/medcl/elasticsearch-analysis-ik/releases?after=v6.4.2
对应 es 版本安装

# 总结

elasticsearch
功能是非常强大的,可以作为生成环境的 ELK 日志存储,方便检索,也可以部署集群,提高效率,最主要是它是走内存的,查询效率极高,为大数据检索而生!!!

# 使用场景(Es)

在这里插入图片描述


# 关于我

Brath 是一个热爱技术的 Java 程序猿,公众号「InterviewCoder」定期分享有趣有料的精品原创文章!

InterviewCoder

非常感谢各位人才能看到这里,原创不易,文章如果有帮助可以关注、点赞、分享或评论,这都是对我的莫大支持!

Go语言超全详解(入门级)

InterviewCoder

# Go 语言超全详解(入门级)

# 文章目录


# 1. Go 语言的出现

在具体学习 go 语言的基础语法之前,我们来了解一下 go 语言出现的时机及其特点。

Go 语言最初由 Google 公司的 Robert Griesemer、Ken Thompson 和 Rob Pike 三个大牛于 2007 年开始设计发明,他们最终的目标是设计一种适应网络和多核时代的 C 语言。所以 Go 语言很多时候被描述为 “类 C 语言”,或者是 “21 世纪的 C 语言”,当然从各种角度看,Go 语言确实是从 C 语言继承了相似的表达式语法、控制流结构、基础数据类型、调用参数传值、指针等诸多编程思想。但是 Go 语言更是对 C 语言最彻底的一次扬弃,它舍弃了 C 语言中灵活但是危险的指针运算,还重新设计了 C 语言中部分不太合理运算符的优先级,并在很多细微的地方都做了必要的打磨和改变。

# 2. go 版本的 hello world

在这一部分我们只是使用 “hello world” 的程序来向大家介绍一下 go 语言的所编写的程序的基本组成。

1
2
3
4
5
6
7
package main
import "fmt"
func main() {
// 终端输出hello world
fmt.Println("Hello world!")
}
123456

和 C 语言相似,go 语言的基本组成有:

  • 包声明,编写源文件时,必须在非注释的第一行指明这个文件属于哪个包,如 package main
  • 引入包,其实就是告诉 Go 编译器这个程序需要使用的包,如 import "fmt" 其实就是引入了 fmt 包。
  • 函数,和 c 语言相同,即是一个可以实现某一个功能的函数体,每一个可执行程序中必须拥有一个 main 函数。
  • 变量,Go 语言变量名由字母、数字、下划线组成,其中首个字符不能为数字。
  • 语句 / 表达式,在 Go 程序中,一行代表一个语句结束。每个语句不需要像 C 家族中的其它语言一样以分号;结尾,因为这些工作都将由 Go 编译器自动完成。
  • 注释,和 c 语言中的注释方式相同,可以在任何地方使用以 // 开头的单行注释。以 /* 开头,并以 */ 结尾来进行多行注释,且不可以嵌套使用,多行注释一般用于包的文档描述或注释成块的代码片段。

需要注意的是:标识符是用来命名变量、类型等程序实体。一个标识符实际上就是一个或是多个字母和数字、下划线_组成的序列,但是第一个字符必须是字母或下划线而不能是数字。

  1. 当标识符(包括常量、变量、类型、函数名、结构字段等等)以一个大写字母开头,如:Group1,那么使用这种形式的标识符的对象就可以被外部包的代码所使用(客户端程序需要先导入这个包),这被称为导出(像面向对象语言中的 public);
  2. 标识符如果以小写字母开头,则对包外是不可见的,但是他们在整个包的内部是可见并且可用的(像面向对象语言中的 protected)。

# 3. 数据类型

在 Go 编程语言中,数据类型用于声明函数和变量。

数据类型的出现是为了把数据分成所需内存大小不同的数据,编程的时候需要用大数据的时候才需要申请大内存,就可以充分利用内存。具体分类如下:

类型 详解
布尔型 布尔型的值只可以是常量 true 或者 false。
数字类型 整型 int 和浮点型 float。Go 语言支持整型和浮点型数字,并且支持复数,其中位的运算采用补码。
字符串类型 字符串就是一串固定长度的字符连接起来的字符序列。Go 的字符串是由单个字节连接起来的。Go 语言的字符串的字节使用 UTF-8 编码标识 Unicode 文本。
派生类型 (a) 指针类型(Pointer)(b) 数组类型 © 结构化类型 (struct)(d) Channel 类型 (e) 函数类型 (f) 切片类型 (g) 接口类型(interface)(h) Map 类型

# 3.0 定义变量

声明变量的一般形式是使用 var 关键字,具体格式为: var identifier typename 。如下的代码中我们定义了一个类型为 int 的变量。

1
2
3
4
5
6
7
package main
import "fmt"
func main() {
var a int = 27
fmt.Println(a);
}
123456

# 3.0.1 如果变量没有初始化

在 go 语言中定义了一个变量,指定变量类型,如果没有初始化,则变量默认为零值。零值就是变量没有做初始化时系统默认设置的值

类型 零值
数值类型 0
布尔类型 false
字符串 “”(空字符串)

# 3.0.2 如果变量没有指定类型

在 go 语言中如果没有指定变量类型,可以通过变量的初始值来判断变量类型。如下代码

1
2
3
4
5
6
7
package main
import "fmt"
func main() {
var d = true
fmt.Println(d)
}
123456

# 3.0.3 := 符号

当我们定义一个变量后又使用该符号初始化变量,就会产生编译错误,因为该符号其实是一个声明语句。

使用格式: typename := value

也就是说 intVal := 1 相等于:

1
2
3
var intVal int 
intVal =1
12

# 3.0.4 多变量声明

可以同时声明多个类型相同的变量(非全局变量),如下图所示:

1
2
3
4
var x, y int
var c, d int = 1, 2
g, h := 123, "hello"
123

关于全局变量的声明如下:
var ( vname1 v_type1 vname2 v_type2 )
具体举例如下:

1
2
3
4
5
var ( 
a int
b bool
)
1234

# 3.0.5 匿名变量

匿名变量的特点是一个下画线 _ ,这本身就是一个特殊的标识符,被称为空白标识符。它可以像其他标识符那样用于变量的声明或赋值(任何类型都可以赋值给它),但任何赋给这个标识符的值都将被抛弃,因此这些值不能在后续的代码中使用,也不可以使用这个标识符作为变量对其它变量进行赋值或运算。

使用匿名变量时,只需要在变量声明的地方使用下画线替换即可。

示例代码如下:

1
2
3
4
5
6
7
8
9
    func GetData() (int, int) {
return 10, 20
}
func main(){
a, _ := GetData()
_, b := GetData()
fmt.Println(a, b)
}
12345678

需要注意的是匿名变量不占用内存空间,不会分配内存。匿名变量与匿名变量之间也不会因为多次声明而无法使用。

# 3.0.6 变量作用域

作用域指的是已声明的标识符所表示的常量、类型、函数或者包在源代码中的作用范围,在此我们主要看一下 go 中变量的作用域,根据变量定义位置的不同,可以分为一下三个类型:

  1. 函数内定义的变量为局部变量,这种局部变量的作用域只在函数体内,函数的参数和返回值变量都属于局部变量。这种变量在存在于函数被调用时,销毁于函数调用结束后。
  2. 函数外定义的变量为全局变量,全局变量只需要在一个源文件中定义,就可以在所有源文件中使用,甚至可以使用 import 引入外部包来使用。全局变量声明必须以 var 关键字开头,如果想要在外部包中使用全局变量的首字母必须大写
  3. 函数定义中的变量成为形式参数,定义函数时函数名后面括号中的变量叫做形式参数(简称形参)。形式参数只在函数调用时才会生效,函数调用结束后就会被销毁,在函数未被调用时,函数的形参并不占用实际的存储单元,也没有实际值。形式参数会作为函数的局部变量来使用

# 3.1 基本类型

类型 描述
uint8 / uint16 / uint32 / uint64 无符号 8 / 16 / 32 / 64 位整型
int8 / int16 / int32 / int64 有符号 8 / 16 / 32 / 64 位整型
float32 / float64 IEEE-754 32 / 64 位浮点型数
complex64 / complex128 32 / 64 位实数和虚数
byte 类似 uint8
rune 类似 int32
uintptr 无符号整型,用于存放一个指针

以上就是 go 语言基本的数据类型,有了数据类型,我们就可以使用这些类型来定义变量,Go 语言变量名由字母、数字、下划线组成,其中首个字符不能为数字。

# 3.2 指针

与 C 相同,Go 语言让程序员决定何时使用指针。变量其实是一种使用方便的占位符,用于引用计算机内存地址。Go 语言中的的取地址符是 & ,放到一个变量前使用就会返回相应变量的内存地址。

指针变量其实就是用于存放某一个对象的内存地址。

# 3.2.1 指针声明和初始化

和基础类型数据相同,在使用指针变量之前我们首先需要申明指针,声明格式如下: var var_name *var-type ,其中的 var-type 为指针类型,var_name 为指针变量名,* 号用于指定变量是作为一个指针。

代码举例如下:

1
2
3
var ip *int        /* 指向整型*/
var fp *float32 /* 指向浮点型 */
12

指针的初始化就是取出相对应的变量地址对指针进行赋值,具体如下:

1
2
3
4
5
   var a int= 20   /* 声明实际变量 */
var ip *int /* 声明指针变量 */

ip = &a /* 指针变量的存储地址 */
1234

# 3.2.2 空指针

当一个指针被定义后没有分配到任何变量时,它的值为 nil,也称为空指针。它概念上和其它语言的 null、NULL 一样,都指代零值或空值。

# 3.3 数组

和 c 语言相同,Go 语言也提供了数组类型的数据结构,数组是具有相同唯一类型的一组已编号且长度固定的数据项序列,这种类型可以是任意的原始类型例如整型、字符串或者自定义类型。

# 3.3.1 声明数组

Go 语言数组声明需要指定元素类型及元素个数,语法格式如下:

1
var variable_name [SIZE] variable_type

以上就可以定一个一维数组,我们举例代码如下:

1
2
var balance [10] float32
1

# 3.3.2 初始化数组

数组的初始化方式有不止一种方式,我们列举如下:

  1. 直接进行初始化: var balance = [5]float32{1000.0, 2.0, 3.4, 7.0, 50.0}
  2. 通过字面量在声明数组的同时快速初始化数组: balance := [5]float32{1000.0, 2.0, 3.4, 7.0, 50.0}
  3. 数组长度不确定,编译器通过元素个数自行推断数组长度,在 [ ] 中填入 ... ,举例如下: var balance = [...]float32{1000.0, 2.0, 3.4, 7.0, 50.0}balance := [...]float32{1000.0, 2.0, 3.4, 7.0, 50.0}
  4. 数组长度确定,指定下标进行部分初始化: balanced := [5]float32(1:2.0, 3:7.0)

注意:

  • 初始化数组中 {} 中的元素个数不能大于 [] 中的数字。
    如果忽略 [] 中的数字不设置数组大小,Go 语言会根据元素的个数来设置数组的大小。

# 3.3.3 go 中的数组名意义

在 c 语言中我们知道数组名在本质上是数组中第一个元素的地址,而在 go 语言中,数组名仅仅表示整个数组,是一个完整的值,一个数组变量即是表示整个数组。

所以在 go 中一个数组变量被赋值或者被传递的时候实际上就会复制整个数组。如果数组比较大的话,这种复制往往会占有很大的开销。所以为了避免这种开销,往往需要传递一个指向数组的指针,这个数组指针并不是数组。关于数组指针具体在指针的部分深入的了解。

# 3.3.4 数组指针

通过数组和指针的知识我们就可以定义一个数组指针,代码如下:

1
2
3
var a = [...]int{1, 2, 3} // a 是一个数组
var b = &a // b 是指向数组的指针
12

数组指针除了可以防止数组作为参数传递的时候浪费空间,还可以利用其和 for range 来遍历数组,具体代码如下:

1
2
3
4
for i, v := range b {     // 通过数组指针迭代数组的元素
fmt.Println(i, v)
}
123

具体关于 go 语言的循环语句我们在后文中再进行详细介绍。

# 3.4 结构体

通过上述数组的学习,我们就可以直接定义多个同类型的变量,但这往往也是一种限制,只能存储同一种类型的数据,而我们在结构体中就可以定义多个不同的数据类型。

# 3.4.1 声明结构体

在声明结构体之前我们首先需要定义一个结构体类型,这需要使用 type 和 struct,type 用于设定结构体的名称,struct 用于定义一个新的数据类型。具体结构如下:

1
2
3
4
5
6
7
type struct_variable_type struct {
member definition
member definition
...
member definition
}
123456

定义好了结构体类型,我们就可以使用该结构体声明这样一个结构体变量,语法如下:

1
2
3
4
variable_name := structure_variable_type {value1, value2...valuen}

variable_name := structure_variable_type { key1: value1, key2: value2..., keyn: valuen}
123

# 3.4.2 访问结构体成员

如果要访问结构体成员,需要使用点号 . 操作符,格式为: 结构体变量名.成员名 。举例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main

import "fmt"

type Books struct {
title string
author string
}

func main() {
var book1 Books
Book1.title = "Go 语言入门"
Book1.author = "mars.hao"
}
1234567891011121314

# 3.4.3 结构体指针

关于结构体指针的定义和申明同样可以套用前文中讲到的指针的相关定义,从而使用一个指针变量存放一个结构体变量的地址。

定义一个结构体变量的语法: var struct_pointer *Books

这种指针变量的初始化和上文指针部分的初始化方式相同 struct_pointer = &Book1 ,但是和 c 语言中有所不同,使用结构体指针访问结构体成员仍然使用 . 操作符。格式如下: struct_pointer.title

# 3.5 字符串

一个字符串是一个不可改变的字节序列,字符串通常是用来包含人类可读的文本数据。和数组不同的是,字符串的元素不可修改,是一个只读的字节数组。每个字符串的长度虽然也是固定的,但是字符串的长度并不是字符串类型的一部分。

# 3.5.1 字符串定义和初始化

Go 语言字符串的底层结构在 reflect.StringHeader 中定义,具体如下:

1
2
3
4
5
type StringHeader struct {
Data uintptr
Len int
}
1234

也就是说字符串结构由两个信息组成:第一个是字符串指向的底层字节数组,第二个是字符串的字节的长度。

字符串其实是一个结构体,因此字符串的赋值操作也就是 reflect.StringHeader 结构体的复制过程,并不会涉及底层字节数组的复制,所以我们也可以将字符串数组看作一个结构体数组。

字符串和数组类似,内置的 len 函数返回字符串的长度。

# 3.5.2 字符串 UTF8 编码

根据 Go 语言规范,Go 语言的源文件都是采用 UTF8 编码。因此,Go 源文件中出现的字符串面值常量一般也是 UTF8 编码的(对于转义字符,则没有这个限制)。提到 Go 字符串时,我们一般都会假设字符串对应的是一个合法的 UTF8 编码的字符序列。

Go 语言的字符串中可以存放任意的二进制字节序列,而且即使是 UTF8 字符序列也可能会遇到坏的编码。如果遇到一个错误的 UTF8 编码输入,将生成一个特别的 Unicode 字符‘\uFFFD’,这个字符在不同的软件中的显示效果可能不太一样,在印刷中这个符号通常是一个黑色六角形或钻石形状,里面包含一个白色的问号‘ ’。

下面的字符串中,我们故意损坏了第一字符的第二和第三字节,因此第一字符将会打印为 “”,第二和第三字节则被忽略;后面的 “abc” 依然可以正常解码打印(错误编码不会向后扩散是 UTF8 编码的优秀特性之一)。代码如下:

1
2
fmt.Println("\xe4\x00\x00\xe7\x95\x8cabc") //  界abc
1

不过在 for range 迭代这个含有损坏的 UTF8 字符串时,第一字符的第二和第三字节依然会被单独迭代到,不过此时迭代的值是损坏后的 0:

1
2
3
4
5
6
7
8
// 0 65533  // \uFFFD, 对应  
// 1 0 // 空字符
// 2 0 // 空字符
// 3 30028 // 界
// 6 97 // a
// 7 98 // b
// 8 99 // c
1234567

# 3.5.3 字符串的强制类型转换

在上文中我们知道源代码往往会采用 UTF8 编码,如果不想解码 UTF8 字符串,想直接遍历原始的字节码:

  1. 可以将字符串强制转为 [] byte 字节序列后再行遍历(这里的转换一般不会产生运行时开销):
  2. 采用传统的下标方式遍历字符串的字节数组

除此以外,字符串相关的强制类型转换主要涉及到 [] byte 和 [] rune 两种类型。每个转换都可能隐含重新分配内存的代价,最坏的情况下它们的运算时间复杂度都是 O (n)。

不过字符串和 [] rune 的转换要更为特殊一些,因为一般这种强制类型转换要求两个类型的底层内存结构要尽量一致,显然它们底层对应的 [] byte 和 [] int32 类型是完全不同的内部布局,因此这种转换可能隐含重新分配内存的操作。

# 3.6 slice

简单地说,切片就是一种简化版的动态数组。因为动态数组的长度不固定,切片的长度自然也就不能是类型的组成部分了。数组虽然有适用它们的地方,但是数组的类型和操作都不够灵活,而切片则使用得相当广泛。

切片高效操作的要点是要降低内存分配的次数,尽量保证 append 操作(在后续的插入和删除操作中都涉及到这个函数)不会超出 cap 的容量,降低触发内存分配的次数和每次分配内存大小。

# 3.6.1 slice 定义

我们先看看切片的结构定义,reflect.SliceHeader:

1
2
3
4
5
6
type SliceHeader struct {
Data uintptr // 指向底层的的数组指针
Len int // 切片长度
Cap int // 切片最大长度
}
12345

和数组一样,内置的 len 函数返回切片中有效元素的长度,内置的 cap 函数返回切片容量大小,容量必须大于或等于切片的长度。

切片可以和 nil 进行比较,只有当切片底层数据指针为空时切片本身为 nil,这时候切片的长度和容量信息将是无效的。如果有切片的底层数据指针为空,但是长度和容量不为 0 的情况,那么说明切片本身已经被损坏了

只要是切片的底层数据指针、长度和容量没有发生变化的话,对切片的遍历、元素的读取和修改都和数组是一样的。在对切片本身赋值或参数传递时,和数组指针的操作方式类似,只是复制切片头信息(reflect.SliceHeader),并不会复制底层的数据。对于类型,和数组的最大不同是,切片的类型和长度信息无关,只要是相同类型元素构成的切片均对应相同的切片类型。

当我们想定义声明一个切片时可以如下:

在对切片本身赋值或参数传递时,和数组指针的操作方式类似,只是复制切片头信息・(reflect.SliceHeader),并不会复制底层的数据。对于类型,和数组的最大不同是,切片的类型和长度信息无关,只要是相同类型元素构成的切片均对应相同的切片类型

# 3.6.2 添加元素

append() :内置的泛型函数,可以向切片中增加元素。

  1. 在切片尾部追加 N 个元素
1
2
3
4
5
var a []int
a = append(a, 1) // 追加1个元素
a = append(a, 1, 2, 3) // 追加多个元素, 手写解包方式
a = append(a, []int{1,2,3}...) // 追加一个切片, 切片需要解包
1234

注意:尾部添加在容量不足的条件下需要重新分配内存,可能导致巨大的内存分配和复制数据代价。即使容量足够,依然需要用 append 函数的返回值来更新切片本身,因为新切片的长度已经发生了变化。

  1. 在切片开头位置添加元素
1
2
3
4
var a = []int{1,2,3}
a = append([]int{0}, a...) // 在开头位置添加1个元素
a = append([]int{-3,-2,-1}, a...) // 在开头添加1个切片
123

注意:在开头一般都会导致内存的重新分配,而且会导致已有的元素全部复制 1 次。因此,从切片的开头添加元素的性能一般要比从尾部追加元素的性能差很多。

  1. append 链式操作
1
2
3
4
var a []int
a = append(a[:i], append([]int{x}, a[i:]...)...) // 在第i个位置插入x
a = append(a[:i], append([]int{1,2,3}, a[i:]...)...) // 在第i个位置插入切片
123

每个添加操作中的第二个 append 调用都会创建一个临时切片,并将 a [i:] 的内容复制到新创建的切片中,然后将临时创建的切片再追加到 a [:i]。

  1. append 和 copy 组合
1
2
3
4
a = append(a, 0)     // 切片扩展1个空间
copy(a[i+1:], a[i:]) // a[i:]向后移动1个位置
a[i] = x // 设置新添加的元素
123

第三个操作中会创建一个临时对象,我们可以借用 copy 函数避免这个操作,这种方式操作语句虽然冗长了一点,但是相比前面的方法,可以减少中间创建的临时切片。

# 3.6.3 删除元素

根据要删除元素的位置有三种情况:

  1. 从开头位置删除;
  • 直接移动数据指针,代码如下:
1
2
3
4
a = []int{1, 2, 3, ...}
a = a[1:] // 删除开头1个元素
a = a[N:] // 删除开头N个元素
123
  • 将后面的数据向开头移动,使用 append 原地完成(所谓原地完成是指在原有的切片数据对应的内存区间内完成,不会导致内存空间结构的变化)
1
2
3
4
a = []int{1, 2, 3, ...}
a = append(a[:0], a[1:]...) // 删除开头1个元素
a = append(a[:0], a[N:]...) // 删除开头N个元素
123
  • 使用 copy 将后续数据向前移动,代码如下:
1
2
3
4
a = []int{1, 2, 3}
a = a[:copy(a, a[1:])] // 删除开头1个元素
a = a[:copy(a, a[N:])] // 删除开头N个元素
123
  1. 从中间位置删除;
    对于删除中间的元素,需要对剩余的元素进行一次整体挪动,同样可以用 append 或 copy 原地完成:
  • append 删除操作如下:
1
2
3
4
a = []int{1, 2, 3, ...}
a = append(a[:i], a[i+1], ...)
a = append(a[:i], a[i+N:], ...)
123
  • copy 删除操作如下:
1
2
3
4
a = []int{1, 2, 3}
a = a[:copy(a[:i], a[i+1:])] // 删除中间1个元素
a = a[:copy(a[:i], a[i+N:])] // 删除中间N个元素
123
  1. 从尾部删除。

代码如下所示:

1
2
3
4
5
a = []int{1, 2, 3, ...}

a = a[:len(a)-1] // 删除尾部1个元素
a = a[:len(a)-N] // 删除尾部N个元素
1234

删除切片尾部的元素是最快的

# 3.7 函数

为完成某一功能的程序指令 (语句) 的集合,称为函数。

# 3.7.1 函数分类

在 Go 语言中,函数是第一类对象,我们可以将函数保持到变量中。函数主要有具名匿名之分,包级函数一般都是具名函数,具名函数是匿名函数的一种特例,当匿名函数引用了外部作用域中的变量时就成了闭包函数,闭包函数是函数式编程语言的核心。

举例代码如下:

  1. 具名函数:就和 c 语言中的普通函数意义相同,具有函数名、返回值以及函数参数的函数。
1
2
3
4
func Add(a, b int) int {
return a+b
}
123
  1. 匿名函数:指不需要定义函数名的一种函数实现方式,它由一个不带函数名的函数声明和函数体组成。
1
2
3
4
var Add = func(a, b int) int {
return a+b
}
123

解释几个名词如下:

  1. 闭包函数:返回为函数对象,不仅仅是一个函数对象,在该函数外还包裹了一层作用域,这使得,该函数无论在何处调用,优先使用自己外层包裹的作用域。
  2. 一级对象:支持闭包的多数语言都将函数作为第一级对象,就是说函数可以存储到变量中作为参数传递给其他函数,最重要的是能够被函数动态创建和返回。
  3. 包:go 的每一个文件都是属于一个包的,也就是说 go 是以包的形式来管理文件和项目目录结构的。

# 3.7.2 函数声明和定义

Go 语言函数定义格式如下:

1
2
3
4
func fuction_name([parameter list])[return types]{
函数体
}
123
解析
func 函数由 func 开始声明
function_name 函数名称
parameter list 参数列表
return_types 返回类型
函数体 函数定义的代码集合

# 3.7.3 函数传参

Go 语言中的函数可以有多个参数和多个返回值,参数和返回值都是以传值的方式和被调用者交换数据。在语法上,函数还支持可变数量的参数,可变数量的参数必须是最后出现的参数,可变数量的参数其实是一个切片类型的参数。

当可变参数是一个空接口类型时,调用者是否解包可变参数会导致不同的结果,我们解释一下解包的含义,代码如下:

1
2
3
4
5
6
7
8
9
10
func main(){
var a = []int{1, 2, 3}
Print(a...) // 解包
Print(a) // 未解包
}

func Print(a ...int{}) {
fmt.Println(a...)
}
123456789

以上当传入参数为 a... 时即是对切片 a 进行了解包,此时其实相当于直接调用 Print(1,2,3) 。当传入参数直接为 a 时等价于直接调用 Print([]int{}{1,2,3})

# 3.7.4 函数返回值

不仅函数的参数可以有名字,也可以给函数的返回值命名。

举例代码如下:

1
2
3
4
5
func Find(m map[int]int, key int)(value int, ok bool) {
value,ok = m[key]
return
}
1234

如果返回值命名了,可以通过名字来修改返回值,也可以通过 defer 语句在 return 语句之后修改返回值,举例代码如下:

1
2
3
4
5
6
7
8
9
10
11
func mian() {
for i := 0 ; i<3; i++ {
defer func() { println(i) }
}
}

// 该函数最终的输出为:
// 3
// 3
// 3
12345678910

以上代码中如果没有 defer 其实返回值就是 0,1,2 ,但 defer 语句会在函数 return 之后才会执行,也就是或只有以上函数在执行结束 return 之后才会执行 defer 语句,而该函数 return 时的 i 值将会达到 3,所以最终的 defer 语句执行 printlin 的输出都是 3。

defer 语句延迟执行的其实是一个匿名函数,因为这个匿名函数捕获了外部函数的局部变量 v,这种函数我们一般叫闭包。闭包对捕获的外部变量并不是传值方式访问,而是以引用的方式访问。

这种方式往往会带来一些问题,修复方法为在每一轮迭代中都为 defer 函数提供一个独有的变量,修改代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func main() {
for i := 0; i < 3; i++ {
i := i // 定义一个循环体内局部变量i
defer func(){ println(i) } ()
}
}

func main() {
for i := 0; i < 3; i++ {
// 通过函数传入i
// defer 语句会马上对调用参数求值
// 不再捕获,而是直接传值
defer func(i int){ println(i) } (i)
}
}
123456789101112131415

# 3.7.5 递归调用

Go 语言中,函数还可以直接或间接地调用自己,也就是支持递归调用。Go 语言函数的递归调用深度逻辑上没有限制,函数调用的栈是不会出现溢出错误的,因为 Go 语言运行时会根据需要动态地调整函数栈的大小。这部分的知识将会涉及 goroutint 和动态栈的相关知识,我们将会在之后的博文中向大家解释。

它的语法和 c 很相似,格式如下:

1
2
3
4
5
6
7
8
func recursion() {
recursion() /* 函数调用自身 */
}

func main() {
recursion()
}
1234567

# 3.8 方法

方法一般是面向对象编程 (OOP) 的一个特性,在 C++ 语言中方法对应一个类对象的成员函数,是关联到具体对象上的虚表中的。但是 Go 语言的方法却是关联到类型的,这样可以在编译阶段完成方法的静态绑定。一个面向对象的程序会用方法来表达其属性对应的操作,这样使用这个对象的用户就不需要直接去操作对象,而是借助方法来做这些事情。

实现 C 语言中的一组函数如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 文件对象
type File struct {
fd int
}

// 打开文件
func OpenFile(name string) (f *File, err error) {
// ...
}

// 关闭文件
func CloseFile(f *File) error {
// ...
}

// 读文件数据
func ReadFile(f *File, offset int64, data []byte) int {
// ...
}
12345678910111213141516171819

以上的三个函数都是普通的函数,需要占用包级空间中的名字资源。不过 CloseFile 和 ReadFile 函数只是针对 File 类型对象的操作,这时候我们更希望这类函数和操作对象的类型紧密绑定在一起。

所以在 go 语言中我们修改如下:

1
2
3
4
5
6
7
8
9
10
// 关闭文件
func (f *File) CloseFile() error {
// ...
}

// 读文件数据
func (f *File) ReadFile(offset int64, data []byte) int {
// ...
}
123456789

将 CloseFile 和 ReadFile 函数的第一个参数移动到函数名的开头,这两个函数就成了 File 类型独有的方法了(而不是 File 对象方法)

从代码角度看虽然只是一个小的改动,但是从编程哲学角度来看,Go 语言已经是进入面向对象语言的行列了。我们可以给任何自定义类型添加一个或多个方法。每种类型对应的方法必须和类型的定义在同一个包中,因此是无法给 int 这类内置类型添加方法的(因为方法的定义和类型的定义不在一个包中)。对于给定的类型,每个方法的名字必须是唯一的,同时方法和函数一样也不支持重载。

# 3.9 接口

# 3.9.1 什么是接口

Go 语言提供了另外一种数据类型即接口,它把所有的具有共性的方法定义在一起,任何其他类型只要实现了这些方法就是实现了这个接口。

Go 的接口类型是对其它类型行为的抽象和概括;因为接口类型不会和特定的实现细节绑定在一起,通过这种抽象的方式我们可以让对象更加灵活和更具有适应能力。很多面向对象的语言都有相似的接口概念,但 Go 语言中接口类型的独特之处在于它是满足隐式实现的鸭子类型。

所谓鸭子类型说的是:只要走起路来像鸭子、叫起来也像鸭子,那么就可以把它当作鸭子。Go 语言中的面向对象就是如此,如果一个对象只要看起来像是某种接口类型的实现,那么它就可以作为该接口类型使用。

就比如说在 c 语言中,使用 printf 在终端输出的时候只能输出有限类型的几个变量,而在 go 中可以使用 fmt.Printf,实际上是 fmt.Fprintf 向任意自定义的输出流对象打印,甚至可以打印到网络甚至是压缩文件,同时打印的数据不限于语言内置的基础类型,任意隐士满足 fmt.Stringer 接口的对象都可以打印,不满足 fmt.Stringer 接口的依然可以通过反射的技术打印。

# 3.9.2 结构体类型

interface 实际上就是一个结构体,包含两个成员。其中一个成员是指向具体数据的指针,另一个成员中包含了类型信息。空接口和带方法的接口略有不同,下面分别是空接口的数据结构:

1
2
3
4
5
6
struct Eface
{
Type* type;
void* data;
};
12345

其中的 Type 指的是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct Type
{
uintptr size;
uint32 hash;
uint8 _unused;
uint8 align;
uint8 fieldAlign;
uint8 kind;
Alg *alg;
void *gc;
String *string;
UncommonType *x;
Type *ptrto;
};
1234567891011121314

和带方法的接口使用的数据结构:

1
2
3
4
5
6
struct Iface
{
Itab* tab;
void* data;
};
12345

其中的 Iface 指的是:

1
2
3
4
5
6
7
8
9
10
struct    Itab
{
InterfaceType* inter;
Type* type;
Itab* link;
int32 bad;
int32 unused;
void (*fun[])(void); // 方法表
};
123456789

# 3.9.3 具体类型向接口类型赋值

将一个具体类型数据赋值给 interface 这样的抽象类型,需要进行类型转换。这个转换过程中涉及哪些操作呢?

如果转换为空接口,返回一个 Eface,将 Eface 中的 data 指针指向原型数据,type 指针会指向数据的 Type 结构体。

如果将其转化为带方法的 interface,需要进行一次检测,该类型必须实现 interface 中声明的所有方法才可以进行转换,这个检测将会在编译过程中进行。检测过程具体实现式通过比较具体类型的方法表和接口类型的方法表来进行的。

  • 具体类型方法表:Type 的 UncommonType 中有一个方法表,某个具体类型实现的所有方法都会被收集到这张表中。
  • 接口类型方法表:Iface 的 Itab 的 InterfaceType 中也有一张方法表,这张方法表中是接口所声明的方法。Iface 中的 Itab 的 func 域也是一张方法表,这张表中的每一项就是一个函数指针,也就是只有实现没有声明。

这两处方法表都是排序过的,只需要一遍顺序扫描进行比较,应该可以知道 Type 中否实现了接口中声明的所有方法。最后还会将 Type 方法表中的函数指针,拷贝到 Itab 的 fun 字段中。Iface 中的 Itab 的 func 域也是一张方法表,这张表中的每一项就是一个函数指针,也就是只有实现没有声明。

# 3.9.4 获取接口类型数据的具体类型信息

接口类型转换为具体类型 (也就是反射,reflect),也涉及到了类型转换。reflect 包中的 TypeOf 和 ValueOf 函数来得到接口变量的 Type 和 Value。

# 3.10 channel

# 3.10.1 相关结构体定义

go 中的 channel 是可以被存储在变量中,可以作为参数传递给函数,也可以作为函数返回值返回,我们先来看一下 channel 的结构体定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct    Hchan
{
uintgo qcount; // 队列q中的总数据数量
uintgo dataqsize; // 环形队列q的数据大小
uint16 elemsize; // 当前队列的使用量
bool closed;
uint8 elemalign;
Alg* elemalg; // interface for element type
uintgo sendx; // 发送index
uintgo recvx; // 接收index
WaitQ recvq; // 因recv而阻塞的等待队列
WaitQ sendq; // 因send而阻塞的等待队列
Lock;
};
1234567891011121314

Hchan 结构体中的核心部分是存放 channel 数据的环形队列,相关数据的作用已经在其后做出了备注。在该结构体中没有存放数据的域,如果是带缓冲区的 chan,则缓冲区数据实际上是紧接着 Hchan 结构体中分配的。

另一个重要部分就是 recvq 和 sendq 两个链表,一个是因读这个通道而导致阻塞的 goroutine,另一个是因为写这个通道而阻塞的 goroutine。如果一个 goroutine 阻塞于 channel 了,那么它就被挂在 recvq 或 sendq 中。WaitQ 是链表的定义,包含一个头结点和一个尾结点,该链表中中存放的成员是一个 sudoG 结构体变量,具体定义如下:

1
2
3
4
5
6
7
8
9
struct    SudoG
{
G* g; // g and selgen constitute
uint32 selgen; // a weak pointer to g
SudoG* link;
int64 releasetime;
byte* elem; // data element
};
12345678

该结构体中最主要的是 g 和 elem。elem 用于存储 goroutine 的数据。读通道时,数据会从 Hchan 的队列中拷贝到 SudoG 的 elem 域。写通道时,数据则是由 SudoG 的 elem 域拷贝到 Hchan 的队列中。

Hchan 结构如下:
在这里插入图片描述

# 3.10.2 阻塞式读写 channel 操作

写操作代码如下,其中的 c 就是 channel,v 指的是数据:

1
2
c <- v
1

事实上基本的阻塞模式写 channel 操作在底层运行时库中对应的是一个 runtime.chansend 函数。具体如下:

1
void runtime·chansend(ChanType *t, Hchan *c, byte *ep, bool *pres, void *pc)

其中的 ep 指的是变量 v 的地址,这里的传值约定是调用者负责分配好 ep 的空间,仅需要简单的取变量地址就好了,pres 是在 select 中的通道操作中使用的。

在这里插入图片描述

阻塞模式读操作的核心函数有两种包装如下:

1
chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected, received bool)

以及

1
chanrecv(c *hchan, ep unsafe.Pointer, block bool) (selected)

这两种的区别主要在于返回值是否会返回一个 bool 类型值,该值只是用于判断 channel 是否能读取出数据。

读写操作的以上阻塞的过程类似,故而不再做出说明,我们补充三个细节:

  • 以上我们都强调是阻塞式的读写操作,其实相对应的也有非阻塞的读写操作,使用过 select-case 来进行调用的。
  • 空通道,指的是将一个 channel 赋值为 nil,或者调用后不适用 make 进行初始化。读写空通道是永远阻塞的。
  • 关闭的通道,永远不会阻塞,会返回一个通道数据类型的零值。首先将 closed 置为 1,第二步收集读等待队列 recvq 的所有 sg,每个 sg 的 elem 都设为类型零值,第三步收集写等待队列 sendq 的所有 sg,每个 sg 的 elem 都设为 nil,最后唤醒所有收集的 sg。

# 3.10.3 非阻塞式读写 channel 操作

如上文所说,非阻塞式其实就是使用 select-case 来实现,在编译时将会被编译为 if-else。

如:

1
2
3
4
5
6
7
select {
case v = <-c:
...foo
default:
...bar
}
123456

就会被编译为:

1
2
3
4
5
6
if selectnbrecv(&v, c) {
...foo
} else {
...bar
}
12345

至于其中的 selectnbrecv 相关的函数简单地调 runtime.chanrecv 函数,设置了一个参数,告诉 runtime.chanrecv 函数,当不能完成操作时不要阻塞,而是返回失败。

但是 select 中的 case 的执行顺序是随机的,而不像 switch 中的 case 那样一条一条的顺序执行。让每一个 select 都对应一个 Select 结构体。在 Select 数据结构中有个 Scase 数组,记录下了每一个 case,而 Scase 中包含了 Hchan。然后 pollorder 数组将元素随机排列,这样就可以将 Scase 乱序了。

# 3.11 map

map 表的底层原理是哈希表,其结构体定义如下:

1
2
3
4
5
6
7
8
9
type Map struct {
Key *Type // Key type
Elem *Type // Val (elem) type

Bucket *Type // 哈希桶
Hmap *Type // 底层使用的哈希表元信息
Hiter *Type // 用于遍历哈希表的迭代器
}
12345678

其中的 Hmap 的具体化数据结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
type hmap struct {
// Note: the format of the hmap is also encoded in cmd/compile/internal/gc/reflect.go.
// Make sure this stays in sync with the compiler's definition.
count int // map目前的元素数目
flags uint8 // map状态(正在被遍历/正在被写入)
B uint8 // 哈希桶数目以2为底的对数(哈希桶的数目都是 2 的整数次幂,用位运算来计算取余运算的值, 即 N mod M = N & (M-1)))
noverflow uint16 //溢出桶的数目, 这个数值不是恒定精确的, 当其 B>=16 时为近似值
hash0 uint32 // 随机哈希种子

buckets unsafe.Pointer // 指向当前哈希桶的指针
oldbuckets unsafe.Pointer // 扩容时指向旧桶的指针
nevacuate uintptr // 桶进行调整时指示的搬迁进度

extra *mapextra // 表征溢出桶的变量
}
123456789101112131415

以上 hmap 基本都是涉及到了哈希桶和溢出桶,我们首先看一下它的数据结构,如下:

1
2
3
4
5
6
7
8
type bmap struct {
topbits [8]uint8 // 键哈希值的高8位
keys [8]keytype // 哈希桶中所有键
elems [8]elemtype // 哈希桶中所有值
//pad uintptr(新的 go 版本已经移除了该字段, 我未具体了解此处的 change detail, 之前设置该字段是为了在 nacl/amd64p32 上的内存对齐)
overflow uintptr
}
1234567

我们会发现哈希桶 bmap 一般指定其能保存 8 个键值对,如果多于 8 个键值对,就会申请新的 buckets,并将其于之前的 buckets 链接在一起。

其中的联系如图所示:
在这里插入图片描述

在具体插入时,首先会根据 key 值采用相应的 hash 算法计算对应的哈希值,将哈希值的低 8 位作为 Hmap 结构体中 buckets 数组的索引,找到 key 值所对应的 bucket,将哈希值的高 8 位催出在 bucket 的 tophash 中。

特点如下:

  • map 是无序的(原因为无序写入以及扩容导致的元素顺序发生变化),每次打印出来的 map 都会不一样,它不能通过 index 获取,而必须通过 key 获取
  • map 的长度是不固定的,也就是和 slice 一样,也是一种引用类型
  • 内置的 len 函数同样适用于 map,返回 map 拥有的 key 的数量
  • map 的 key 可以是所有可比较的类型,如布尔型、整数型、浮点型、复杂型、字符串型…… 也可以键。

如下方式即可进行初始化:

1
2
var a map[keytype]valuetype
1
类型名 意义
a map 表名字
keytype 键类型
valuetype 键对应的值的类型

除此以外还可以使用 make 进行初始化,代码如下:

1
2
map_variable = make(map[key_data_type]value_data_type)
1

我们还可以使用初始值进行初始化,如下:

1
2
var m map[string]int = map[string]int{"hunter":12,"tony":10}
1

# 3.11.1 插入数据

map 的数据插入代码如下:

1
2
map_variable["mars"] = 27
1

插入过程如下:

  1. 根据 key 值计算出哈希值
  2. 取哈希值低位和 hmap.B 取模确定 bucket 位置
  3. 查找该 key 是否已经存在,如果存在则直接更新值
  4. 如果没有找到 key,则将这一对 key-value 插入

# 3.11.2 删除数据

delete(map, key) 函数用于删除集合的元素,参数为 map 和其对应的 key。删除函数不返回任何值。相关代码如下:

1
2
3
4
   countryCapitalMap := map[string] string {"France":"Paris","Italy":"Rome","Japan":"Tokyo","India":"New Delhi"}
/* 删除元素 */
delete(countryCapitalMap,"France");
123

# 3.11.3 查找数据

通过 key 获取 map 中对应的 value 值。语法为: map[key] . 但是当 key 如果不存在的时候,我们会得到该 value 值类型的默认值,比如 string 类型得到空字符串,int 类型得到 0。但是程序不会报错。

所以我们可以使用 ok-idiom 获取值,如下: value, ok := map[key] ,其中的 value 是返回值,ok 是一个 bool 值,可知道 key/value 是否存在。

在 map 表中的查找过程如下:

  1. 查找或者操作 map 时,首先 key 经过 hash 函数生成 hash 值
  2. 通过哈希值的低 8 位来判断当前数据属于哪个桶
  3. 找到桶之后,通过哈希值的高八位与 bucket 存储的高位哈希值循环比对
  4. 如果相同就比较刚才找到的底层数组的 key 值,如果 key 相同,取出 value
  5. 如果高八位 hash 值在此 bucket 没有,或者有,但是 key 不相同,就去链表中下一个溢出 bucket 中查找,直到查找到链表的末尾
  6. 如果查找不到,也不会返回空值,而是返回相应类型的 0 值。

# 3.11.4 扩容

哈希表就是以空间换时间,访问速度是直接跟填充因子相关的,所以当哈希表太满之后就需要进行扩容。

如果扩容前的哈希表大小为 2B 扩容之后的大小为 2 (B+1),每次扩容都变为原来大小的两倍,哈希表大小始终为 2 的指数倍,则有 (hash mod 2B) 等价于 (hash & (2B-1))。这样可以简化运算,避免了取余操作。

触发扩容的条件?

  1. 负载因子 (负载因子 = 键数量 /bucket 数量) > 6.5 时,也即平均每个 bucket 存储的键值对达到 6.5 个。
  2. 溢出桶(overflow)数量 > 2^15 时,也即 overflow 数量超过 32768 时。

什么是增量扩容呢?

如果负载因子 > 6.5 时,进行增量扩容。这时会新建一个桶(bucket),新的 bucket 长度是原来的 2 倍,然后旧桶数据搬迁到新桶。每个旧桶的键值对都会分流到两个新桶中

主要是缩短 map 容器的响应时间。假如我们直接将 map 用作某个响应实时性要求非常高的 web 应用存储,如果不采用增量扩容,当 map 里面存储的元素很多之后,扩容时系统就会卡往,导致较长一段时间内无法响应请求。不过增量扩容本质上还是将总的扩容时间分摊到了每一次哈希操作上面。

什么是等量扩容?它的触发条件是什么?进行等量扩容后的优势是什么?

等量扩容,就是创建和旧桶数目一样多的新桶,然后把原来的键值对迁移到新桶中,重新做一遍类似增量扩容的搬迁动作。

触发条件:负载因子没超标,溢出桶较多。这个较多的评判标准为:

  • 如果常规桶数目不大于 2^15,那么使用的溢出桶数目超过常规桶就算是多了;
  • 如果常规桶数目大于 215,那么使用溢出桶数目一旦超过 215 就算多了。

这样做的目的是把松散的键值对重新排列一次,能够存储的更加紧凑,进而减少溢出桶的使用,以使 bucket 的使用率更高,进而保证更快的存取。

# 4. 常用语句及关键字

接下来我们了解一下关于 go 语言语句的基本内容。

# 4.1 条件语句

和 c 语言类似,相关的条件语句如下表所示:

语句 描述
if 语句 if 语句 由一个布尔表达式后紧跟一个或多个语句组成。
if…else 语句 if 语句 后可以使用可选的 else 语句,else 语句中的表达式在布尔表达式为 false 时执行。
switch 语句 switch 语句用于基于不同条件执行不同动作。
select 语句 select 语句类似于 switch 语句,但是 select 会随机执行一个可运行的 case。如果没有 case 可运行,它将阻塞,直到有 case 可运行。
  • if 语句
    语法如下:
1
2
3
4
if 布尔表达式 {
/* 在布尔表达式为 true 时执行 */
}
123
  • if-else 语句
1
2
3
4
5
6
if 布尔表达式 {
/* 在布尔表达式为 true 时执行 */
} else {
/* 在布尔表达式为 false 时执行 */
}
12345
  • switch 语句
    其中的变量 v 可以是任何类型, val1val2 可以是同类型的任意值,类型不局限为常量或者整数,或者最终结果为相同类型的表达式。
1
2
3
4
5
6
7
8
9
switch v {
case val1:
...
case val2:
...
default:
...
}
12345678
  • select 语句
    select 是 Go 中的一个控制结构,类似于用于通信的 switch 语句。每个 case 必须是一个通信操作,要么是发送要么是接收。它将会随机执行一个可运行的 case。如果没有 case 可运行,它将阻塞,直到有 case 可运行。一个默认的子句应该总是可运行的。
1
2
3
4
5
6
7
8
9
10
select {
case communication clause :
statement(s);
case communication clause :
statement(s);
/* 你可以定义任意数量的 case */
default : /* 可选 */
statement(s);
}
123456789

注意:

  • 每个 case 必须都是一个通信
  • 所有 channel 表达式都会被求值,所有被发送的表达式都会被求值
  • 如果任意某一个通信都可以,它就执行,其他就忽略
  • 如果有多个 case 都可以运行,select 就会随机挑选一个来执行。
  • 如果没有一个 case 可以被运行:如果有 default 子句,就执行 default 子句,select 将被阻塞,直到某个通信可以运行,从而避免饥饿问题。

# 4.2 循环语句

# 4.2.1 循环处理语句

go 中时使用 for 实现循环的,共有三种形式:

语法
和 c 语言中的 for 相同 for init; condition; post {}
和 c 语言中的 while 相同 for condition{}
和 c 语言中的 for(;;) 相同 for{}

除此以外,for 循环还可以直接使用 range 对 slice、map、数组以及字符串等进行迭代循环,格式如下:

1
2
3
4
for key, value := range oldmap {
newmap[key] = value
}
123

# 4.2.1 循环控制语句

控制语句 详解
break 中断跳出循环或者 switch 语句
continue 跳过当前循环的剩余语句,然后继续下一轮循环
goto 语句 将控制转移到被标记的语句
  1. break
    break 主要用于循环语句跳出循环,和 c 语言中的使用方式是相同的。且在多重循环的时候还可以使用 label 标出想要 break 的循环。
    实例代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
a := 0
for a<5 {
fmt.Printf("%d\n", a)
a++
if a==2 {
break;
}
}
/* output
0
1
2
*/
12345678910111213
  1. continue
    Go 语言的 continue 语句 有点像 break 语句。但是 continue 不是跳出循环,而是跳过当前循环执行下一次循环语句。在多重循环中,可以用标号 label 标出想 continue 的循环。
    实例代码如下:
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
    // 不使用标记
fmt.Println("---- continue ---- ")
for i := 1; i <= 3; i++ {
fmt.Printf("i: %d\n", i)
for i2 := 11; i2 <= 13; i2++ {
fmt.Printf("i2: %d\n", i2)
continue
}
}

/* output
i: 1
i2: 11
i2: 12
i2: 13
i: 2
i2: 11
i2: 12
i2: 13
i: 3
i2: 11
i2: 12
i2: 13
*/

// 使用标记
fmt.Println("---- continue label ----")
re:
for i := 1; i <= 3; i++ {
fmt.Printf("i: %d", i)
for i2 := 11; i2 <= 13; i2++ {
fmt.Printf("i2: %d\n", i2)
continue re
}
}

/* output
i: 1
i2: 11
i: 2
i2: 11
i: 3
i2: 11
*/
1234567891011121314151617181920212223242526272829303132333435363738394041424344
  1. goto
    goto 语句主要是无条件转移到过程中指定的行。goto 语句通常和条件语句配合使用,可用来实现条件转移、构成循环以及跳出循环体等功能。但是并不主张使用 goto 语句,以免造成程序流程混乱。
    示例代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var a int = 0
LOOP: for a<5 {
if a == 2 {
a = a+1
goto LOOP
}
fmt.Printf("%d\n", a)
a++
}

/*
output:
0
1
2
3
4
*/
123456789101112131415161718

以上代码中的 LOOP 就是一个标签,当运行到 goto 语句的时候,此时执行流就会跳转到 LOOP 标志的哪一行上。

# 4.3 关键字

我们这一部分直接列表供大家了解 go 中的关键字如下:

关键字 用法
import 导入相应的包文件
package 创建包文件,用于标记该文件归属哪个包
chan channal,通道
var 变量控制,用于简短声明定义变量(:= 符号只能在函数内部使用,不能全局使用)
const 常量声明,任何时候 const 和 var 都可以同时出现
func 定义函数和方法
interface 接口,是一种具有一组方法的类型,这些方法定义了 interface 的行为
map 哈希表
struct 定义结构体
type 声明类型,取别名
for for 是 go 中唯一的循环结构,上文中已经介绍过它的用法
break 中止,跳出循环
continue 继续下一轮循环
select 选择流程,可以同时等待多个通道操作
switch 多分枝选择,上文中已经详细介绍过它的用法
case 和 switch 配套使用
default 用于选择结构的默认选型
defer 用于资源释放,会在函数返回之前进行调用
if 分支选择
else 和 if 配套使用
go 通过 go func() 来开启一个 goroutine
goto 跳转至标志点的代码块,不推荐使用
fallthrouth
range 用于遍历 slice 类型数据
return 用于标注函数返回值

# 关于我

Brath 是一个热爱技术的 Java 程序猿,公众号「InterviewCoder」定期分享有趣有料的精品原创文章!

InterviewCoder

非常感谢各位人才能看到这里,原创不易,文章如果有帮助可以关注、点赞、分享或评论,这都是对我的莫大支持!

Docker环境部署Reids镜像作为从节点

InterviewCoder

# Docker 环境部署 Reids 镜像作为从节点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 拉取镜像
docker pull redis

# 配置文件
本地:/mydata/redis/data/redis.conf 提前准备好
主要配置
bind 127.0.0.1 #注释掉这部分,使redis可以外部访问
daemonize no#用守护线程的方式启动
requirepass 你的密码#给redis设置密码
appendonly yes#redis持久化  默认是no
tcp-keepalive 300 #防止出现远程主机强迫关闭了一个现有的连接的错误 默认是300

# 创建实例并启动
docker run -p 6380:6379 --name redis -v /mydata/redis/data/redis.conf:/etc/redis/redis.conf -v /mydata/redis/data:/data -d redis redis-server /etc/redis/redis.conf --appendonly yes

# 关于我

Brath 是一个热爱技术的 Java 程序猿,公众号「InterviewCoder」定期分享有趣有料的精品原创文章!

InterviewCoder

非常感谢各位人才能看到这里,原创不易,文章如果有帮助可以关注、点赞、分享或评论,这都是对我的莫大支持!

【Go】Go语言与Java语言对比

InterviewCoder

# 【Go】Go 语言与 Java 语言对比

# Go 与 Java

# 零.GoApi 文档和中文社区网址

Go 的中文 api 文档:https://studygolang.com/pkgdoc

Go 中文社区网站:https://studygolang.com/

# 一。关于 Java

# 1.Java 的用途

1
2
3
4
5
6
7
	首先我们来回顾下Java的主要用途和应用场景:
1
用途一:服务器后端系统开发(web后端、微服务后端支付系统、业务系统、管理后台,各种后台交互的接口服务)。

用途二:大数据框架的底层实现和Java的API支持。(Hadoop)。

用途三:其它中间件的底层开发。(Tomcat、RocketMq、Hbase、Kafka(部分)、SpringCloud,Dubbo...)。

# 2.Java 的优势和特点

1
2
3
4
5
6
7
8
9
	那我们看Java语言有什么优势和特点呢?
1
*.做服务端系统性能高。

*.有虚拟机,跨平台。

*.功能强大,支持的类库多,生态圈类库多,开发框架和工具更易找。

*.市场占有率高,约60%的中国程序员都是做Java相关的工作。

# 二。关于 Go

# 1.Go 的出生原因

1
2
3
Go语言是Google内部公司大佬开发的,主要起因于Google公司有大量的C程序项目,但是开发起来效率太低,维护成本高,于是就开发了Go语言来提高效率,而且性能只是差一点。

(Go是2007年开始研发,2009推出发布)

# 2. 宏观看 Go 与 Java 的差异

1
2
3
4
5
6
7
8
9
10
11
	接着,我们来看一下Go语言与Java的差异之处:
1
*.无虚拟机,不跨平台(这里的平台指操作系统)(可以运行多个平台,每个平台打不同的二进制程序包),需要打包编译成对应服务器操作系统版本(windows/linux)的可执行程序(比如windows是exe)。(注:说go跨平台的是指32位和64位相同操作系统之间的跨平台)

*.因为Go程序直接打包成操作系统可执行的文件,没有虚拟机在中间转换的一层,所以理论上执行效率会更高(理论上更高,实际情况需具体分析)。

*.相比Java的语言和代码编写风格,Go更简洁,可以用更少的代码实现同样的功能。

*.Go语言底层也是C实现的,又做了高并发的设计(Java出生时(1995)还没有多核cpu,所以他的并发支持后来添加上去的,Go(2009)出生时已经有了多核cpu的电脑,它在设计语言时就考虑了充分利用多核cpu(英特尔2005首次推出多核)的性能),所以性能高,高并发的支持(高并发支持其中指的一个就是充分利用多核cpu的性能资源,比如go程序默认使用所有cpu(除非自己设置使用多少))也好。

*.天然的适用一些特定系统的开发,比如区块链类系统(如以太坊底层系统、以太坊上层应用程序),云计算和容器(Docker,K8s底层都是go开发的)开发的(大公司自研运维管理项目也大多是用go做底层的开发),网络编程(类似于java的Netty)。

# 3.Go 和 Java 的语言类型区别

计算机编程语言按照运行的方式可以分为编译型编程语言和解释型编译语言。

我来举一个例子,你要教别人一门沟通交流的语言,比如英语。

编译型的教的方式就是录 (这里的录相当于计算机中把程序编译成二进制可执行文件) 一个视频课程,语音课程,把每一句英语发音录下来,这样学生学的时候只要播放你的录音,然后跟着读就行,你只需要录制一次,学生就可以无数次听。

解释性的教的方式就是你亲自到学生家里给他补习,你当面教他,你读 (读相当于每次执行都重新用解释器解释一遍) 一句他学一句,

这样的话,你想要教他一句你必须就得先读一句,每次教都得重新一遍一遍的读。

这两种教学方式还有一个差别,你录 (编译) 视频语音教他,你录的英语他就只能学英语,空间环境一变,他现在要去日本,要学日语,你的视频语音教程因为已经录好了,是英语类型 (英语类型类比操作系统类型) 的,所以,你就得再录一套日语的语音教程。

而现场教他,你也会日语的话,你只需要读 (读相当于解释器解释) 日语给他听,让他学就行了,是不用考虑语言环境 (操作系统类型环境) 不同的问题的。

现在我们再来看编程语言,我们的程序执行有两种方式,一种是编译成操作系统可执行的二进制可执行程序,这样相当于编译一次,之后每次执行都不用再编译了,但是因为不同操作系统对于二进制文件的执行规范不同,不同的操作系统你要编译成不同的可执行文件。

解释型语言就是多了一个解释器,解释器我们可以类比为一个老师,你执行一行代码我们类比为学一句话的读音,解释器解释一句,就是老师先读一句,你跟着才能读一句,也就是解释器每解释一行代码为可运行的代码,操作系统执行一行代码,这样的话每次执行都需要解释器重新解释一遍,执行几次就得解释几次。

Go 是编译型的语言,运行在不同的平台需要打包成不同操作系统类型下的可执行文件。

Java 是半编译,半解释型语言。编译是指他的代码都会编译成 class 类型的文件,class 类型的文件只需要编译一次,可以在不同的操作系统的 Java 虚拟机上执行 ,半解释是指在 Java 虚拟机中,他还是需要一句一句的将 class 的二进制代码解释成对应操作系统可执行的代码。

# 4.Go 语言目前的主要应用场景

1
2
3
4
5
6
7
8
9
10
*.和Java一样,Go语言最多的应用场景就是服务器后端系统的开发,包括Web后端,微服务后端接口。

*.Go非常适用需要高性能高并发的网络编程,这里的网络编程是指不需要界面,底层只是用Socket相互传输数据的系统,类似于Java中Netty的用途。

*.一些云计算容器,比如Docker,K8s,底层就是Go语言开发的,也可以用做底层自研运维项目的开发。

*.一些游戏系统的开发,可以用Go语言。

*.区块链的一些底层软件和一些应用软件。(区块链程序的第一开发语言)

# 5. 现在市场上都有哪些公司在使用 Go 语言?

我们不讲虚的,直接 BOSS 直聘看哪些公司招,招的是干什么系统开发的。

这是腾讯的一个岗位。

在这里插入图片描述

看看岗位描述,是做互联网保险 产品的业务系统开发,业务系统是啥意思,和 JAVA 后端业务系统一样啊,说明腾讯的一部分项目已经用 Go 来开发业务系统了, 至少他这个保险团队是这样的。

在这里插入图片描述

再看小米也是:

在这里插入图片描述

也是后端,这是要和 JAVA 抢饭碗。。。

在这里插入图片描述

再看一个常见的,Go 非常适合开发运维管理系统,这个估计是开发维护阿里内部的自动化运维项目的,也就是说他们的运维支持可能是他们自己用 Go 语言写的项目。(实在不理解你就想下他们自己自研开发了一个类似于 Jenkins 和 Docker 之类的环境和代码流程发布的项目)

在这里插入图片描述

再来看一个字节跳动的,也是开发内部流程自动部署自动运维程序的

在这里插入图片描述

再看华为的,好像 Java 架构师的要求啊,微服务,缓存,消息中间件,数据库。。。

在这里插入图片描述

这里不多看,自己看看去吧,大多数你能知道的大公司都有用 go 语言尝试的新部门,新项目,市场占有率虽然比 Java 少,但是岗位实际上蛮多的。自己可以去 BOSS 上详细查查。

# 三.Go 和 Java 微观对比

# 1.GoPath 和 Java 的 ClassPath

我们先来看看关于 Java 的 classpath:

在我们的开发环境中,一个 web 程序 (war 包) 有一个 classpath, 这个 classpath 在 IDEA 的开发工具中目录体现为 src 目录和 resource 目录,实际上在真正的 war 包中他定位的是指 WEB-INF 下的 classes 文件夹下的资源 (比如 class 文件)。

我们编译后的文件都放在 classpath (类路径) 下。我们多个项目程序会有多个 classpath 目录。

在 Go 语言中,GoPath 在同一系统上的同一用户,一般规定只有一个,无论这个用户创建多少个 go 项目,都只有一个 GoPath, 并且这些项目都放在 GoPath 下的 src 目录下。

GoPath 下有三个目录:

1.bin (用于存放项目编译后的可执行文件)

2.pkg (用于存放类库文件,比如.a 结尾的包模块)

3.src (用于存放项目代码源文件)

注意:当我们在 windows 上开发 Go 程序时,需要新建一个文件夹 (文件夹名任意) 作为 GOPATH 的文件目录,在其中新建三个文件夹分别是:bin,pkg,src。如果是在集成开发工具上开发的话,需要在设置中把 GOPATH 路径设置为你自定义的那个文件夹,之后产生的文件和相关内容都会在其中。

如果是在 linux 上想跑测试开发执行 go 程序,需要在 /etc/profile 添加名为 GOPATH 的环境变量,目录设置好自己新建的。

例如:全局用户设置 GOPATH 环境变量

1
2
3
4
5
vi /etc/profile
#添加如下 目录可以灵活修改
export GOPATH=/pub/go/gopath
//立即刷新环境变量生效
source /etc/profile

单用户设置 GOPATH 环境变量

1
2
3
4
5
6
vi   ~/.bash_profile

#添加如下 目录可以自己灵活修改
export GOPATH=/home/user/local/soft/go/gopath
//立即刷新环境变量生效
source vi ~/.bash_profile

注意:这是在 linux 上开发 go 程序才需要的,如果只是生产运行程序的话是不需要任何东西的,直接运行二进制可执行程序包即可,他所有的依赖全部打进包中了。

如果是在 windows 下的 cmd,dos 窗口运行相关的 go 命令和程序,则需要在 windows 的【此电脑】–>【右键】–>【属性】–>【高级系统设置】–>【环境变量】-【新建一个系统变量】–>【变量名为 GOPATH,路径为你自己指定的自定义文件夹】(如果是在 IDEA 中开发,不需要在此配置环境变量,只需要在 IDEA 中配置好 GOPATH 的目录设置即可)

# 2.Go 的开发环境搭建

(配置环境变量 GOPATH 参考上一节内容)

我们要开发 Go 的程序,需要如下两样东西:

1.Go SDK

GO 中文社区 SDK 下载地址:https://studygolang.com/dl

go1.14 (最新的)

我们用 1.14 版就可以,因为 1.13 后才完全支持 Module 功能。

有两种安装模式,一种是压缩包解压的方式,一种是图形化安装。

推荐使用 windows 图形安装傻瓜式安装,windows 图形安装下载这个

https://studygolang.com/dl/golang/go1.14.6.windows-amd64.msi

  1. Go 的集成软件开发环境

    参考三 (4) 中的 go 集成开发环境选择。

# 3.Go 与 Java 的文件结构对比

# 1).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
//主程序必须是写成main包名
package main

//导入别的类库
import "fmt"

//全局常量定义
const num = 10

//全局变量定义
var name string = "li_ming"

//类型定义
type P struct {

}

//初始化函数
func init() {

}

//main函数:程序入口
func main() {
fmt.Printf("Hello World!!!");
}

# 2).Java 文件结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//包名
package my_package;

//导入包中的类
import java.io.*;

public Class MainTest{
//main方法:程序入口
public void static main(String[] args) {

}
}
//people类
Class People {
//成员变量
public String name;
public int age;

//成员方法
public void doSomething() {

}

}

# 4.Go 与 Java 的集成开发环境

# 1).Go 的集成开发环境

1
2
3
4
5
6
7
最常用的有三种:

Visual Studio Code(VS Code) 微软开发的一款Go语言开发工具。

LiteIDE 是国人开发的Go语言开发工具。

GoLand 这个非常好用,和Java中的IDEA是一家公司。(推荐使用)

# 2).Java 的集成开发环境

1
2
3
MyEclipse,Eclipse(已过时)。

IntelliJ IDEA(大多数用这个)。

# 5.Go 和 Java 常用包的对比

1
2
3
4
5
6
7
8
9
10
11
12
Go中文API文档地址:
https://studygolang.com/pkgdoc
12
Go Java

IO流操作: bufio/os java.lang.io
字符串操作: strings java.lang.String
容器 container(heap/list/ring) java.lang.Collection
锁 sync juc
时间 time java.time/java.lang.Date
算数操作 math java.math
底层Unsafe unsafe unsafe类

# 6.Go 的常用基础数据类型和 Java 的基础数据类型对比

# 1).go 中的常用基础数据类型有:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
1.布尔型:关键字【bool】: true   false
2.有符号整形:头一位是代表正负
int 默认整形 4或8字节 32位或64位
int8 1字节 8位
int16 2字节 16位
int32 4字节 32位
in64 8字节 64位
【int是32还是64位取决于操作系统的位数,现在电脑一般都是64位的了,所以一般都是64位】
3.无符号整形
uint 4或8字节 32位或64位
uint8 1字节 8位
uint16 2字节 16位
uint32 4字节 32位
uint64 8字节 64位
4.浮点型
注:go语言没有float类型,只有float32和float64。
float32 32位浮点数
float64 64位浮点数
5.字符串
string
6. byte 等同uint8,只是类似于一个别名的东西
rune 等同int32 只是一个别名,强调表示编码概念对应的数字

# 2).go 中派生数据类型有:

1
2
3
4
5
6
7
8
9
10
注:这里简单列举一下
指针 Pointer
数组 Array[]
结构体 struct
进程管道: channel
函数 func
切片 slice
接口 interface
哈希 map
123456789

# 3).Java 中的基础数据类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
byte

short

int

long

float

double

boolean

char

# 7.Go 和 Java 的变量对比

# 1).go 的变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main

import(
//包含print函数
"fmt"
)
func main() {
//var 变量名 变量类型 = 变量值
var name string = "li_ming"
//方法内部可以直接使用 【 变量名 := 变量值 】 赋值,方法外不可以
name2:="xiao_hong"
fmt.Println("name = ",name)
fmt.Println("name2 = ",name2)
}

# 2).Java 的变量

1
2
3
4
5
6
7
8
9
public class MyTest {
public static void main(String[] args) {
//变量类型 变量名 = 变量值
String name = "li_ming";
int i = 10;
System.out.println("name ="+name);
System.out.println("i ="+i);
}
}

# 8.Go 和 Java 的常量对比

# 1).go 的常量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
go中的常量和java中的常量含义有一个本质的区别:
go中的常量是指在编译期间就能确定的量(数据),
而java中的常量是指被赋值一次后就不能修改的量(数据)。
所以两者不一样,因为Java中的常量也是JVM跑起来后赋值的,只不过不允许更改;
go的常量在编译后就确实是什么数值了。
12345
package main

import(
//包含print函数
"fmt"
)
func main() {
//const 常量名 常量类型 = 常量值 显示推断类型
const name string = "const_li_ming"
//隐式推断类型
const name2 ="const_xiao_hong"
fmt.Println("name = ",name)
fmt.Println("name2 = ",name2)
}

# 2).Java 的常量

1
2
3
4
5
6
7
public class MyTest {
//【访问修饰符】 【静态修饰符】final修饰符 常量类型 常量名 = 常量值;
public static final String TAG = "A"; //一般设置为static静态
public static void main(String[] args) {
System.out.println("tag= "+TAG);
}
}

# 9.Go 与 Java 的赋值对比

# 1).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
Go方法内的赋值符号可以用  := ,也可以用 =,方法外只能用 = 。
例如:
12
package main

import(
//包含print函数
"fmt"
)

//方法外只能用 = 赋值
var my_name string = "my_name"
var my_name2 = "my_name2"
//my_name3:="my_name3" 不在方法内,错误

func main() {
fmt.Println("name = ",my_name)
fmt.Println("name2 = ",my_name2)
}
12345678910111213141516
Go支持多变量同时赋值:
1
package main

import(
//包含print函数
"fmt"
)

func main() {
//多变量同时赋值
var name,name2 = "li_ming","xiao_hong"
fmt.Println("name = ",name)
fmt.Println("name2 = ",name2)
}
12345678910111213
Go的丢弃赋值
1
package main

import(
//包含print函数
"fmt"
)

func main() {
//丢弃赋值 把 1和2丢弃 只取3
//在必须一次取两个以上的值的场景下,又不想要其中一个值的时候使用,比如从map中取key,value
var _,_,num = 1,2,3
fmt.Println("num = ",num)
}

# 2).java 的赋值

1
2
3
4
5
6
7
8
9
public class MyTest {
public static void main(String[] args) {
//直接用 = 赋值
String name = "li_ming";
int i = 10;
System.out.println("name ="+name);
System.out.println("i ="+i);
}
}

# 10.Go 与 Java 的注释

1
2
3
4
5
6
7
8
9
Go中的注释写法和Java中的基本一样。
例如:
//单行注释,两者相同
/*
Go的多行注释
*/
/**
Java多行注释
*/

# 11.Go 和 Java 的访问权限设置区别

首先我们来回忆一下,Java 的权限访问修饰符有哪些?

public 全局可见

protected 继承相关的类可见

default 同包可见

private 私有的,本类可见

关于 Java 中的访问权限修饰符,是用于修饰变量,方法,类的,被修饰的对象被不同的访问权限修饰符修饰后,其它程序代码要想访问它,必须在规定的访问范围内才可以,比如同包,同类,父子类,全局均可访问。

那么,Go 中的访问权限设置又有什么区别呢?

要理解这个问题,首先我们要来看一下一个 Go 程序的程序文件组织结构是什么样子的?

一个可运行的编译后的 Go 程序,必须有一个入口,程序从入口开始执行,这个入口必须是 main 包,并且从 main 包的 main 函数开始执行。

但是,为了开发的效率和管理开发任务的协调简单化,对于代码质量的可复用,可扩展等特性的要求,我们一般采用面向对象的,文件分模块式的开发。

比如,我是一个游戏程序,我的 main 函数启动后,首先要启动 UI 界面,那么关于 UI 界面相关的代码我们一般会专门分出一个模块去开发,然后这个模块有很多个程序文件,这里关于 UI 模块比如有 3 个文件,a.go,b.go,c.go,那么我们在实际当中会建一个以 ui 为名的包文件夹,然后把 a.go,b.go,c.go 全部放到 ui 这个包文件夹下,然后因为这个包没有 main 包,没有 main 函数,所以它打出来的一个程序文件就是以.a 结尾的工具包,类似于 Java 中的 jar 包,工具包文件名为 ui.a。

参考如下:

----com.add.mygame.ui

------------------------------------a.go

------------------------------------b.go

------------------------------------c.go

a.go 文件如下示例:

1
2
3
4
5
6
7
8
9
10
11
12
//这里的ui,也就是package后面的名称尽量和包文件夹的名称一致,不一致也可以
package ui

//相关方法和业务

func main() {

}
//启动游戏UI
func StartGameUI() {

}

这里需要注意一个点,在程序中的 package 后面的 ui 包名可以和文件夹 com.mashibing.mygame.ui 中最后一层的 ui 文件夹名称不一致,

我们一般按规范写是要求写一致的,不一致时的区别如下:

我们把 ui.a 打包完毕后,我们就可以在别的程序中用 import 导入这个包模块 ,然后使用其中的内容了。

上面两个 ui 不同之处在于,在我们 import 的代码后面,需要写的模块名称是在 ${gopath}/src/ 下的文件夹名,也就是 com.mashibing.mygame.ui 中的 ui。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
//游戏主程序
package main

//这里的ui是com.mashibing.mygame.ui的最后一层文件夹名
import "ui"

//相关方法和业务

func main() {
//这里的ui不是文件夹名,而是之前a.go程序中package后面写的包名
ui.StartGameUI()
}

在这里插入图片描述

在这里插入图片描述

接下来进入主题,我们的 go 语言关于访问修饰符的是指的限制什么权限,以及如何实现?

我们之前可以看出来,实战中的 go 程序是有一个 main 程序 import 很多其它包模块,每个模块实现对应的功能,最后统一在 main 程序中组合来完成整个软件程序,那么有一些其它模块的函数和变量,我只想在本程序文件中调用,不想被其它程序 import 能调用到,如何实现?

import 后是否能调用对应包中的对象 (变量,结构体,函数之类的) 就是 go 关于访问权限的定义,import 后,可以访问,说明是开启了访问权限,不可以访问,是说明关闭了其它程序访问的权限。

在 go 中,为了遵循实现简洁,快速的原则,用默认的规范来规定访问权限设置。

默认规范是:某种类型(包括变量,结构体,函数,类型等)的名称定义首字母大写就是在其它包可以访问,首字母非大写,就是只能在自己的程序中访问。

这样我们就能理解为什么导入 fmt 包后,他的 PrintF 函数的首字母 P 是大写的。

参照如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package ui

import "fmt"

func main() {
//这里的P是大写
//所有调用别的包下的函数,都是首字母大写
fmt.Printf("aa")
}
//这里的Person的首字母P也是表示外部程序导入该包后可以使用此Person类
type Person struct{

}
//这里的D同上
var Data string = "li_ming"

# 12.Go 与 Java 程序文件的后缀名对比

1
2
3
4
5
6
7
8
9
10
11
Java的编译文件是.class结尾,多个.class打成的一个可执行文件是.jar结尾,.jar不能直接在windows和linux上执行,得用java命令在JVM中执行。

Go语言的程序文件后缀为.go,有main包main函数的,.go文件打包成二进制对应操作系统的可执行程序,如windows上的.exe结尾的可执行程序。

Java的类库会以.jar结尾,Go语言非main包没有main函数的程序编译打包会打成一个类库,以.a结尾,也就是说Go语言的类库以.a结尾。

Go的类库如下:
包名.a
my_util.a
注:my_util是最顶层文件夹名,里面包含着一个个程序文件。
12345678910

# 13.Go 与 Java 选择结构的对比

# 1).if

1
Go中的if和Java中的if使用相同,只不过是把小括号给去掉了。       

示例 1:

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

import (
"fmt"
)
func main() {
/*
单分支结构语法格式如下:
if 条件判断 {
//代码块
}
*/

var num int

fmt.Printf("请输入数字")
fmt.Scan(&num)

if num > 10 {
fmt.Println("您输入的数字大于10")
}
}

示例 2:

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
package main

import (
"fmt"
)
func main() {
/*
if else分支结构语法格式如下:
if 条件判断 {
//代码块
} else {
//代码快2
}
*/

var num int

fmt.Printf("请输入数字")
fmt.Scan(&num)

if num > 10 {
fmt.Println("您输入的数字大于10")
} else {
fmt.Println("您输入的数字不大于10")
}
}

示例 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
package main

import (
"fmt"
)
func main() {
/*
if else分支结构语法格式如下:
if 条件判断 {
//代码块
} else if 条件判断{
//代码块2
} else {
//代码块3
}
*/

var num int

fmt.Printf("请输入数字")
fmt.Scan(&num)

if num > 10 {
fmt.Println("您输入的数字大于10")
} else if num == 10{
fmt.Println("您输入的数字等于10")
} else {
fmt.Println("您输入的数字小于10")
}
}

# 2).switch

示例 1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package main

import (
"fmt"
)
func main() {
var a = "li_ming"
switch a {
case "li_ming":
fmt.Println("Hello!LiMing")
case "xiao_hong":
fmt.Println("Hello!XiaoHong")
default:
fmt.Println("No!")
}
}

示例 2:一分支多值

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

import (
"fmt"
)
func main() {

var name = "li_ming"
var name2 = "xiao_hong"
switch name {
//li_ming或xiao_hong 均进入此
case "li_ming", "xiao_hong":
fmt.Println("li_ming and xiao_hong")
}

switch name2 {
case "li_ming", "xiao_hong":
fmt.Println("li_ming and xiao_hong")
}
}g

示例 3:switch 表达式

1
2
3
4
5
6
7
8
9
10
11
12
package main

import (
"fmt"
)
func main() {
var num int = 11
switch{
case num > 10 && num < 20:
fmt.Println(num)
}
}

示例 4:fallthrough 下面的 case 全部执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main

import (
"fmt"
)
func main() {
var num = 11
switch {
case num == 11:
fmt.Println("==11")
fallthrough
case num < 10:
fmt.Println("<12")
}
}

# 14.Go 与 Java 循环结构的对比

# 1).for 循环

示例 1:省略小括号

1
2
3
4
5
6
7
8
9
10
11
package main

import (
"fmt"
)

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

示例 2:和 while 相同,break,continue 同 java

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

import (
"fmt"
)

func main() {
i := 0
//省略另外两项,相当于java中的while
for i < 3 {
i++
}
//break用法相同
for i == 3 {
fmt.Println(i)
break
}
}

示例 3:死循环,三项均省略

1
2
3
4
5
6
7
8
9
10
11
12
package main

func main() {
for {

}

for true {

}

}

示例 4:嵌套循环和 java 也一样,不演示了

示例 5: range 循环

1
2
3
4
5
6
7
8
9
10
package main

import "fmt"

func main() {
var data [10]int = [10]int{1,2,3,4,5,6,7,8,9,10}
for i, num := range data {
fmt.Println(i,num)
}
}

# 2).goto

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

import "fmt"

func main() {

//goto可以用在任何地方,但是不能跨函数使用
fmt.Println("start")

//go to的作用是跳转,中间的语句不执行,无条件跳转
goto my_location //goto是关键字, my_location可以自定义,他叫标签

fmt.Println("over")
my_location:
fmt.Println("location")


# 15.Go 与 Java 的数组对比

1)go 的一维数组

1
var 数组名 [数组长度]数组类型  = [数组长度]数组类型{元素1,元素2...}

示例 1:

1
2
3
4
5
6
7
8
9
10
11
12
13
package main

import "fmt"
//全局
var my_arr [6]int
var my_arr_1 [3]int = [3]int{1,2,3}
func main() {
//方法内:
this_arr := [2]int{1, 2}
fmt.Println(my_arr)
fmt.Println(my_arr_1)
fmt.Println(this_arr)
}

2)二维数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package main

import "fmt"
//全局
var my_arr [4][6]int
var my_arr_1 [2][3]int = [...][3]int{{1, 2, 3}, {4, 5, 6}}
func main() {
//方法内:
this_arr := [2][3]int{{1, 2, 3}, {8, 8, 8}}
// 第 2 纬度不能用 "..."。
this_arr2 := [...][2]int{{1, 1}, {2, 2}, {3, 3}}
fmt.Println(my_arr)
fmt.Println(my_arr_1)
fmt.Println(this_arr)
fmt.Println(this_arr2)
}

# 16.Go 有指针概念,Java 没有指针概念

1
2
3
4
5
6
7
8
9
10
11
12
Go中有指针的概念,Java中没有指针的概念。
指针简单的说就是存储一个【变量地址】的【变量】。
12
Go中使用指针的方法
//*+变量类型 = 对应变量类型的指针类型,&+变量名 = 获取变量引用地址
var 指针变量名 *指针变量类型 = &变量名
例如:
var my_point *int = &num
//通过&+指针变量 = 修改原来的变量真实值
&指针变量名 = 修改的变量值
例如:
&my_point = 100;

示例:

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"

func main() {
// 声明实际变量
var name string="li_ming"
// 声明指针变量
var name_point *string
// 指针变量的存储地址
name_point = &name

//直接访问变量地址
fmt.Println("name 变量的地址是:", &name )

// 指针变量的存储地址
fmt.Println("name_point变量储存的指针地址:", name_point )

// 使用指针访问值
fmt.Println("*name_point 变量的值:", *name_point )
}

输出结果:

1
2
3
name 变量的地址是: 0x10ae40f0
name_point变量储存的指针地址: 0x10ae40f0
*name_point 变量的值: li_ming

# 17.Go 语言的中 new,make 和 Java 中的 new 对象有什么区别?

首先,Java 中的 new 关键字代表创建关于某一个类的一个新的对象。

如:

1
List list = new ArrayList(); 

Go 中的创建一个 struct 结构体的对象,是不需要用 new 关键字的,参考【20】中有代码示例讲解如何创建结构体对象。

Go 中 new 的概念是和内存相关的,我们可以通过 new 来为基础数据类型申请一块内存地址空间,然后把这个把这个内存地址空间赋值给

一个指针变量上。(new 主要就是为基础数据类型申请内存空间的,当我们需要一个基础数据类型的指针变量,并且在初始化这个基础指针变量时,不能确定他的初始值,此时我们才需要用 new 去内存中申请一块空间,并把这空间绑定到对应的指针上,之后可以用该指针为这块内存空间写值。new 关键字在实际开发中很少使用,和 java 很多处用 new 的情况大不相同)

参考如下示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
package main

import "fmt"

func main() {
var num *int
//此处num是nil
fmt.Println(num)
//此处会报空指针异常,因为num为nil,没有申请内存空间,所以不能为nil赋值
*num = 1
fmt.Println(*num)
}

改为如下代码即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
package main

import "fmt"

func main() {
//在内存中申请一块地址,并把内存地址存入num
var num = new(int)
//此处num的值是申请出来的内存空间地址值,一个十六进制的数字
fmt.Println(num)
//正常
*num = 1
fmt.Println(*num)
}

接下来我们来看一个 go 中的 make 是做什么用的?

go 中的 make 是用来创建 slice (切片),map (映射表),chan (线程通信管道) 这三个类型的对象的,返回的就是对应类型的对象,他的作用就相当于 Java 中 new 一个 ArrayList,new 一个 HashMap 时候的 new 的作用,只不过是 go 语法规定用 make 来创建 slice (切片),map (映射表),chan (线程通信管道)。

示例代码如下:

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
package main

import "fmt"

func main() {

//make只能为map,channel,slice申请分配内存,只有这三种,没有第四种
//所有通过make创建的这三种类型都是引用类型,传递参数时虽然是引用值传递,
//但是对方法内引用变量参数的修改可以影响到外部的引用变量
//1.通过make创建map对象 如下代码类似于Java中 Map<String,Integer> myMap = new HashMap<>();
//在这里make就是申请分配map的内存,和java中创建map的new一样
myMap := make(map[string]int)
myMap["li_ming"] = 20

//2.通过make创建channel,make函数内可以有一个参数,也可以有两个参数,有两个参数时第二个参数
//为通道的缓存队列的长度
//2.1) 只有一个参数,通道的缓存队列长度此时为0,也就是无缓存。
//创建一个传输int类型数据的通道
myChan := make(chan int)
fmt.Println(myChan)
//2.2) 有两个参数,第二个参数2代表此时代表缓存队列的长度为2
//创建一个传输int类型数据的通道,缓存为2
mychan2 := make(chan int,2)
fmt.Println(mychan2)
//此处暂时不做通道缓存队列数多少有何区别的讲解

//3.通过make创建slice切片
//有两种方式,一种是两个参数,一种是三个参数
//我们只有在创建一个空的切片时才会使用make
//如果通过一个已有的数组创建切片往往是下面的形式
//创建一个底层数组
myArr := []int{1,2,3,4,5}
//如果通过一个数组创建切片,往往是用 原始数组变量名[切片起始位置:切片结束位置] 创建一个切片
mySlice1 := myArr[2:4]
fmt.Println(mySlice1)
//我们如果是想创建一个空的slice,则用make创建切片
//如下形式 make(int[],num1,num2)
//num1 = 切片的长度(默认分配内存空间的元素个数)
//num2 = 切片的容量(解释:底层数组的长度/切片的容量,超过底层数组长度append新元素时会创建一个新的底层数组,
//不超过则会使用原来的底层数组)

//代表底层数组的长度是4,默认给底层数组的前两个元素分配内存空间
//切片指向前两个元素的地址,如果append新元素,在元素数小于4时都会
//在原来的底层数组的最后一个元素新分配空间和赋值,
//append超过4个元素时,因为原数组大小不可变,也也存储不下了,
//所以会新创建一个新的底层数组,切片指向新的底层数组
mySliceEmpty := make([]int,2,4)
fmt.Println(mySliceEmpty)

//两个参数,代表切片的长度和切片的容量(底层数组长度)均为第二个参数那个值
mySliceEmpty2 := make([]int,5)
fmt.Println(mySliceEmpty2)
}

# 18.Go 相关的数据容器和 Java 的集合框架对比

1
2
3
4
5
6
7
8
Go中有的数据结构:数组,切片,map,双向链表,环形链表,堆
Go自己的类库中没有set,没有集合(List),但是第三方库有实现。
Java中有: Map,Set,List,Queue,Stack,数组
Java中没有切片的概念。
Go中的数组打印格式是[1,2,3,4,5]
Go中的切片打印格式是[[1,2,3]]
Go中切片的概念:切片是数组的一个子集,就是数组截取某一段。
Go的map和Java的map大致相同

# 19.Go 中的函数,Go 的方法和 Java 中的方法对比

# 1).Go 中的函数定义

1
2
3
4
Go中返回值可以有多个,不像Java中多个值得封装到实体或map返回
//注:【】内的返回值可不写,无返回值直接把返回值部分全部去掉即可。
func 函数名(变量1 变量类型,变量2 变量2类型...)【(返回值1 类型1,返回值2 类型2...)】 { //注意:这个方法的右中括号必须和func写在同一行才行,否则报错,不能按c语言中的换行写
123

示例 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
package main

import "fmt"

func main() {
//定义局部变量
var a int = 100
var b int = 200
var result int

//调用函数并返回最大值
result = max(a, b)

fmt.Println( "最大值是 :", result )
}

/* 函数返回两个数的最大值 */
func max(num1, num2 int) int {
/* 定义局部变量 */
var result int

if (num1 > num2) {
result = num1
} else {
result = num2
}
return result
}

示例 2:返回多个值

1
2
3
4
5
6
7
8
9
10
11
12
13
package main

import "fmt"

func main() {
a, b := swap("li_ming", "xiao_hong")
fmt.Println(a, b)
}

func swap(x, y string) (string, string) {
//返回多个值
return y, x
}

注意点:函数的参数:基础类型是按值传递,复杂类型是按引用传递

示例 3: 函数的参数:变长参数传递

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

import "fmt"

func main() {
manyArgs(1,2,"2","3","4")
manyArgs(1,2,"5","5","5")
dataStr := []string{"11","11","11"}
//传数组也可以,加三个点
manyArgs(1,2,dataStr...)
}

//可变参数必须放在最后面
func manyArgs(a int,b int ,str ...string ){
for i,s := range str {
fmt.Println(i,s)
}
}

注意点:函数的返回值:如果有返回值,返回值的类型必须写,返回值得变量名根据情况可写可不写。

示例 4: defer:推迟执行 (类似于 java 中的 finally)

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

import "fmt"

func main() {
testMyFunc();
}


func testDefer1() {
fmt.Println("print defer1")
}
func testDefer2() {
fmt.Println("print defer2")
}

func testMyFunc() {
//defer会在方法返回前执行,有点像java中的finally
//defer写在任意位置均可,多个defer的话按照逆序依次执行
defer testDefer2()
defer testDefer1()
fmt.Println("print my func")
}

示例 5 :丢弃返回值

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

import "fmt"

func main() {
//方式一丢弃:丢弃num1和str
_,num2,_:= testFun(1,2,"3");
fmt.Println(num2)
//方式二丢弃:
_,num3,_:= testFun(1,3,"4");
fmt.Println(num3)
}

func testFun(num1,num2 int,str string) (n1 int,n2 int,s1 string){
n1 = num1
n2 = num2
s1 = str
return
}
func testFun2(num1,num2 int,str string) (int,int,string){
return num1,num2,str
}

# 2).Java 中的方法定义

1
2
3
4
访问修饰符   返回值类型   方法名(参数1类型  参数1,参数2类型 参数2...) {

return 返回值;
}

示例:

1
2
3
4
public Integer doSomething(String name,Integer age) {

return 20;
}

# 20.Go 的内置函数和 Java 的默认导入包 java.lang.*

为了在 Java 中快速开发,Java 语言的创造者把一些常用的类和接口都放到到 java.lang 包下,lang 包下的特点就是不用写 import 语句导入包就可以用里面的程序代码。

Go 中也有类似的功能,叫做 Go 的内置函数,Go 的内置函数是指不用导入任何包,直接就可以通过函数名进行调用的函数。

Go 中的内置函数有:

1
2
3
4
5
6
7
8
9
10
11
close          关闭channel

len 求长度

make 创建slice,map,chan对象

append 追加元素到切片(slice)中

panic 抛出异常,终止程序

recover 尝试恢复异常,必须写在defer相关的代码块中

参考示例代码 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
32
33
34
35
36
37
38
package main

import "fmt"

func main() {

array := [5]int{1,2,3,4,5}
str := "myName"
//获取字符串长度
fmt.Println(len(str))
//获取数组长度
fmt.Println(len(array))
//获取切片长度
fmt.Println(len(array[1:]))

//make创建channel示例
intChan := make(chan int,1)
//make创建map示例
myMap := make(map[string]interface{})
//make创建切片
mySlice := make([]int,5,10)

fmt.Println(intChan)
fmt.Println(myMap)
fmt.Println(mySlice)

//关闭管道
close(intChan)
//为切片添加元素
array2 := append(array[:],6)
//输出
fmt.Println(array2)

//new案例
num := new(int)
fmt.Println(num)

}

参考示例代码 2:panic 和 recover 的使用

他们用于抛出异常和尝试捕获恢复异常

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
func func1() {
fmt.Println("1")
}

func func2() {
// 刚刚打开某资源
defer func() {
err := recover()
fmt.Println(err)
fmt.Println("释放资源..")
}()
panic("抛出异常")
fmt.Println(2")
}

func func3() {
fmt.Println("3")
}

func main() {
func1()
func2()
func3()
}

Java 中的 java.lang 包下具体有什么在这里就不赘述了,请参考 JavaAPI 文档:

1
JavaAPI文档导航:https://www.oracle.com/cn/java/technologies/java-se-api-doc.html

# 21.Go 的标准格式化输出库 fmt 和 java 的输出打印库对比

Java 的标准输出流工具类是 java.lang 包下的 System 类,具体是其静态成员变量 PrintStream 类。

他有静态三个成员变量:

分别是 PrintStream 类型的 out,in,err

我们常见 System.out.println (), 实际上调用的就是 PrintStream 类对象的 println 方法。


Go 中的格式化输出输入库是 fmt 模块。

fmt 在 Go 中提供了输入和输出的功能,类型 Java 中的 Scanner 和 PrintStream (println)。

它的使用方法如下:

1
2
3
4
5
Print:   原样输出到控制台,不做格式控制。
Println: 输出到控制台并换行
Printf : 格式化输出(按特定标识符指定格式替换)
Sprintf:格式化字符串并把字符串返回,不输出,有点类似于Java中的拼接字符串然后返回。
Fprintf:来格式化并输出到 io.Writers 而不是 os.Stdout

详细占位符号如下:

代码示例如下:

# 22.Go 的面向对象相关知识

# 1. 封装属性 (结构体)

Go 中有一个数据类型是 Struct, 它在面向对象的概念中相当于 Java 的类,可以封装属性和封装方法,首先看封装属性如下示例:

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
package main

import "fmt"

//示例
type People struct {
name string
age int
sex bool
}
func main(){

//示例1:
var l1 People
l1.name = "li_ming"
l1.age = 22
l1.sex = false
//li_ming
fmt.Println(l1.name)

//示例2
var l2 *People = new(People)
l2.name = "xiao_hong"
l2.age = 33
l2.sex = true
//xiao_hong xiao_hong
fmt.Println(l2.name,(*l2).name)

//示例3:
var l3 *People = &People{ name:"li_Ming",age:25,sex:true}
//li_Ming li_Ming
fmt.Println(l3.name,(*l3).name)
}

# 2. 封装方法 (方法接收器)

如果想为某个 Struct 类型添加一个方法,参考如下说明和代码:

go 的方法和 Java 中的方法对比,go 的函数和 go 方法的不同

Go 中的函数是不需要用结构体的对象来调用的,可以直接调用

Go 中的方法是必须用一个具体的结构体对象来调用的,有点像 Java 的某个类的对象调用其方法

我们可以把指定的函数绑定到对应的结构体上,使该函数成为这个结构体的方法,然后这个结构体的对象就可以通过。来调用这个方法了

绑定的形式是:在 func 和方法名之间写一个 (当前对象变量名 当前结构体类型),这个叫方法的接受器,其中当前对象的变量名就是当前结构体调用该方法的对象的引用,相当于 java 中的 this 对象。

参考如下示例为 Student 学生添加一个 learn 学习的方法

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

import "fmt"

type Student struct {
num int //学号
name string //姓名
class int //班级
sex bool //性别
}

//给Student添加一个方法
//这里的(stu Student)中的stu相当于java方法中的this对象
//stu是一个方法的接收器,接收是哪个对象调用了当方法
func (stu Student) learn() {
fmt.Printf("%s学生正在学习",stu.name)
}

func main() {
stu := Student{1,"li_ming",10,true}
stu.learn()
}

方法的接收器也可以是指针类型的

参考如下案例:

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"

type Student struct {
num int //学号
name string //姓名
class int //班级
sex bool //性别
}

//这里方法的接收器也可以是指针类型
func (stu *Student) learn() {
fmt.Printf("%s学生正在学习",stu.name)
}

func main() {
//指针类型
stu := &Student{1,"li_ming",10,true}
stu.learn()
}

注意有一种情况,当一个对象为 nil 空时,它调用方法时,接收器接受的对于自身的引用也是 nil,需要我们做一些健壮性的不为 nil 才做的判断处理。

# 3.Go 的继承 (结构体嵌入)

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
package main

import "fmt"

//电脑
type Computer struct {
screen string //电脑屏幕
keyboard string //键盘
}

//计算方法
func (cp Computer) compute(num1,num2 int) int{
return num1+num2;
}

//笔记本电脑
type NoteBookComputer struct{
Computer
wireless_network_adapter string //无线网卡
}
func main() {
var cp1 NoteBookComputer = NoteBookComputer{}
cp1.screen = "高清屏"
cp1.keyboard = "防水键盘"
cp1.wireless_network_adapter = "新一代无线网卡"
fmt.Println(cp1)
fmt.Println(cp1.compute(1,2))
}

需要注意的是,Go 中可以嵌入多个结构体,但是多个结构体不能有相同的方法,如果有参数和方法名完全相同的方法,在编译的时候就会报错。所以 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
package main

import "fmt"

func main() {
man := Man{}
fmt.Println(man)
//下面的代码编译会报错
//man.doEat()

}
type Man struct {
FatherA
FatherB
}

func (p FatherA) doEat() {
fmt.Printf("FatherA eat")
}
func (t FatherB) doEat() {
fmt.Printf("FatherB eat")
}


type FatherB struct {

}

type FatherA struct {

}

# 4.Go 的多态 (接口)

接下来我们讲 Go 中如何通过父类接口指向具体实现类对象,实现多态:

go 语言中的接口是非侵入式接口。

java 语言中的接口是侵入式接口。

侵入式接口是指需要显示的在类中写明实现哪些接口。

非侵入式接口是指不要显示的在类中写明要实现哪些接口,只需要方法名同名,参数一致即可。

参考如下代码示例:接口与多态

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
package main

import "fmt"

//动物接口
type Animal interface{
eat() //吃饭接口方法
sleep() //睡觉接口方法
}
//小猫
type Cat struct {

}
//小狗
type Dog struct {

}
//小猫吃方法
func (cat Cat) eat() {
fmt.Println("小猫在吃饭")
}
//小猫睡方法
func (cat Cat) sleep(){
fmt.Println("小猫在睡觉")
}
//小狗在吃饭
func (dog Dog) eat(){
fmt.Println("小狗在吃饭")
}
//小狗在睡觉
func (dog Dog) sleep(){
fmt.Println("小狗在睡觉")
}

func main() {
var cat Animal = Cat{}
var dog Animal = Dog{}
cat.eat()
cat.sleep()
dog.eat()
dog.sleep()
}

接口可以内嵌接口

参考如下代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package main
//内嵌接口
//学习接口,内嵌听和看学习接口
type Learn interface {
LearnByHear
LearnByLook
}
//通过听学习接口
type LearnByHear interface {
hear()
}
//通过看学习
type LearnByLook interface {
look()
}

#

# 23.Go 语言中线程的实现和 Java 语言中线程的实现

go 中的线程相关的概念是 Goroutines (并发),是使用 go 关键字开启。

Java 中的线程是通过 Thread 类开启的。

在 go 语言中,一个线程就是一个 Goroutines,主函数就是(主) main Goroutines。

使用 go 语句来开启一个新的 Goroutines

比如:

普通方法执行

myFunction()

开启一个 Goroutines 来执行方法

go myFunction()

java 中是

new Thread(()->{

// 新线程逻辑代码

}).start();

参考下面的代码示例:

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
package main

import (
"fmt"
)

//并发开启新线程goroutine测试

//我的方法
func myFunction() {
fmt.Println("Hello!!!")
}
//并发执行方法
func goroutineTestFunc() {
fmt.Println("Hello!!! Start Goroutine!!!")
}


func main() {
/*
myFunction()
//go goroutineTestFunc()
//此时因为主线程有时候结束的快,goroutineTestFunc方法得不到输出,由此可以看出是开启了新的线程。
*/
//打开第二段执行
/*
go goroutineTestFunc()
time.Sleep(10*time.Second)//睡一段时间 10秒
myFunction()
*/
}

线程间的通信:

java 线程间通信有很多种方式:

比如最原始的 wait/notify

到使用 juc 下高并发线程同步容器,Go 和 Java 关于 Socket 编程的对比同步队列

到 CountDownLatch 等一系列工具类

甚至是分布式系统不同机器之间的消息中间件,单机的 disruptor 等等。

Go 语言不同,线程间主要的通信方式是 Channel。

Channel 是实现 go 语言多个线程(goroutines)之间通信的一个机制。

Channel 是一个线程间传输数据的管道,创建 Channel 必须声明管道内的数据类型是什么

下面我们创建一个传输 int 类型数据的 Channel

代码示例:

1
2
3
4
5
6
7
8
package main

import "fmt"

func main() {
ch := make(chan int)
fmt.Println(ch)
}

channel 是引用类型,函数传参数时是引用传递而不是值拷贝的传递。

channel 的空值和别的应用类型一样是 nil。

== 可以比较两个 Channel 之间传输的数据类型是否相等。

channel 是一个管道,他可以收数据和发数据。

具体参照下面代码示例:

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
package main

import (
"fmt"
"time"
)
//channel发送数据和接受数据用 <-表示,是发送还是接受取决于chan在 <-左边还是右边
//创建一个传输字符串数据类型的管道
var chanStr = make(chan string)
func main() {
fmt.Println("main goroutine print Hello ")
//默认channel是没有缓存的,阻塞的,也就是说,发送端发送后直到接受端接受到才会施放阻塞往下面走。
//同样接收端如果先开启,直到接收到数据才会停止阻塞往下走
//开启新线程发送数据
go startNewGoroutineOne()
//从管道中接收读取数据
go startNewGoroutineTwo()
//主线程等待,要不直接结束了
time.Sleep(100*time.Second)
}

func startNewGoroutineOne() {
fmt.Println("send channel print Hello ")
//管道发送数据
chanStr <- "Hello!!!"
}

func startNewGoroutineTwo(){
fmt.Println("receive channel print Hello ")
strVar := <-chanStr
fmt.Println(strVar)
}

无缓存的 channel 可以起到一个多线程间线程数据同步锁安全的作用。

缓存的 channel 创建方式是

make (chan string, 缓存个数)

缓存个数是指直到多个数据没有消费或者接受后才进行阻塞。

类似于 java 中的 synchronized 和 lock

可以保证多线程并发下的数据一致性问题。

首先我们看一个线程不安全的代码示例:

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
package main

import (
"fmt"
"time"
)

//多线程并发下的不安全问题
//金额
var moneyA int =1000
//添加金额
func subtractMoney(subMoney int) {
time.Sleep(3*time.Second)
moneyA-=subMoney
}

//查询金额
func getMoney() int {
return moneyA;
}


func main() {

//添加查询金额
go func() {
if(getMoney()>200) {
subtractMoney(200)
fmt.Printf("200元扣款成功,剩下:%d元\n",getMoney())
}
}()

//添加查询金额
go func() {
if(getMoney()>900) {
subtractMoney(900)
fmt.Printf("900元扣款成功,剩下:%d元\n",getMoney())
}
}()
//正常逻辑,只够扣款一单,可以多线程环境下结果钱扣多了
time.Sleep(5*time.Second)
fmt.Println(getMoney())
}

缓存为 1 的 channel 可以作为锁使用:

示例代码如下:

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
package main

import (
"fmt"
"time"
)

//多线程并发下使用channel改造
//金额
var moneyA = 1000
//减少金额管道
var synchLock = make(chan int,1)

//添加金额
func subtractMoney(subMoney int) {
time.Sleep(3*time.Second)
moneyA-=subMoney
}

//查询金额
func getMoney() int {
return moneyA;
}


func main() {

//添加查询金额
go func() {
synchLock<-10
if(getMoney()>200) {
subtractMoney(200)
fmt.Printf("200元扣款成功,剩下:%d元\n",getMoney())
}
<-synchLock
}()

//添加查询金额
go func() {
synchLock<-10
if(getMoney()>900) {
subtractMoney(900)
fmt.Printf("900元扣款成功,剩下:%d元\n",getMoney())
}
synchLock<-10
}()
//这样类似于java中的Lock锁,不会扣多
time.Sleep(5*time.Second)
fmt.Println(getMoney())
}

go 也有互斥锁

类似于 java 中的 Lock 接口

参考如下示例代码:

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
package main

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

//多线程并发下使用channel改造
//金额
var moneyA = 1000

var lock sync.Mutex;
//添加金额
func subtractMoney(subMoney int) {
lock.Lock()
time.Sleep(3*time.Second)
moneyA-=subMoney
lock.Unlock()
}

//查询金额
func getMoney() int {
lock.Lock()
result := moneyA
lock.Unlock()
return result;
}


func main() {
//添加查询金额
go func() {
if(getMoney()>200) {
subtractMoney(200)
fmt.Printf("200元扣款成功,剩下:%d元\n",getMoney())
}else {
fmt.Println("余额不足,无法扣款")
}
}()

//添加查询金额
go func() {
if(getMoney()>900) {
subtractMoney(900)
fmt.Printf("900元扣款成功,剩下:%d元\n",getMoney())
}else {
fmt.Println("余额不足,无法扣款")
}
}()
//正常
time.Sleep(5*time.Second)
fmt.Println(getMoney())
}

# 24.Go 中的反射与 Java 中的反射对比

整体概述:反射是一个通用的概念,是指在程序运行期间获取到变量或者对象,结构体的元信息,比如类型信息,并且能够取出其中变量的值,调用对应的方法。

首先我们先来回顾一下 Java 语言用到反射的场景有哪些?

1. 比如说我们的方法参数不能确定是什么类型,是 Object 类型,我们就可以通过反射在运行期间获取其真实的类型,然后做对应的逻辑处理。

2. 比如动态代理,我们需要在程序运行时,动态的加载一个类,创建一个类,使用一个类。

3. 比如在想要强行破解获取程序中被 private 的成员。

4.Java 的各种框架中用的非常多,框架中用反射来判断用户自定义的类是什么类型,然后做区别处理。

Go 中的反射大概也是相同的,比如,go 中有一个类型 interface,interface 类型相当于 Java 中的 Object 类,当以 interface 作为参数类型时,可以给这个参数传递任意类型的变量。

例如示例 1:

1
2
3
4
5
6
7
8
9
10
11
12
13
package main

import "fmt"

func main() {
testAllType(1);
testAllType("Go");
}

//interface{}代表任意类型
func testAllType (data interface{}){
fmt.Println(data)
}

那么第一种应用场景就出现了,当我们在 go 中想实现一个函数 / 方法,这个函数 / 方法的参数类型在编写程序的时候不能确认,在运行时会有各种不同的类型传入这个通用的函数 / 方法中,我们需要对不同类型的参数做不同的处理,那么我们就得能获取到参数是什么类型的,然后根据这个类型信息做业务逻辑判断。

反射我们需要调用 reflect 包模块,使用 reflect.typeOf () 可以获取参数的类型信息对象,再根据类型信息对象的 kind 方法,获取到具体类型,详细参考下面代码。

例如示例 2:

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
package main

import (
"fmt"
"reflect"
)

func main() {
handleType(1)
handleType(true)
}



func handleType(data interface{}) {
//获取类型对象
d := reflect.TypeOf(data)
//kind方法是获取类型
fmt.Println(d.Kind())
switch d.Kind() {
case reflect.Invalid:
//无效类型逻辑处理
fmt.Println("无效类型")
case reflect.Int,reflect.Int8,reflect.Int16,reflect.Int32,reflect.Int64:
fmt.Println("整形")
case reflect.Bool:
fmt.Println("bool类型")
}

}
因为传入进来的都是interface类型,所以我们需要用的时候要区分类型,然后取出其中真正类型的值。

反射取出值得方法就是先通过 reflect.ValueOf () 获取参数值对象,然后再通过不同的具体方法获取到值对象,比如 int 和 bool

示例 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
package main

import (
"fmt"
"reflect"
)

func main() {
handleValue(1)
handleValue(true)
}



func handleValue(data interface{}) {
//获取类型对象
d := reflect.ValueOf(data)
//kind方法是获取类型
fmt.Println(d.Kind())
switch d.Kind() {
case reflect.Invalid:
//无效类型逻辑处理
fmt.Println("无效类型")
case reflect.Int,reflect.Int8,reflect.Int16,reflect.Int32,reflect.Int64:
//取出值
var myNum = d.Int()
fmt.Println(myNum)
case reflect.Bool:
//取出bool值
var myBool = d.Bool()
fmt.Println(myBool)
}
}

结构体中的属性和方法怎么获取呢?

获取结构体属性的个数是先 ValueOf 获取结构体值对象 v 后,用 v.NumField () 获取该结构体有几个属性,通过 v.Field (i) 来获取对应位置的属性的元类型。

示例代码 4:

后续反射还有几个 api 和代码示例和具体应用场景,正在补。。。

# 25. 变量作用域的区别

Go 语言的变量作用域和 Java 中的一样,遵循最近原则,逐渐往外层找。

这个比较简单,就不做过多赘述了。

# 26.Go 语言和 Java 语言字符串操作的区别

# 27.Go 语言和 Java 语言 IO 操作的区别

# 28.Go 语言中有匿名函数,有闭包,Java 中没有 (高阶函数用法)

函数也是一种类型,它可以作为一个参数进行传递,也可以作为一个返回值传递。

Go 中可以定义一个匿名函数,并把这个函数赋值给一个变量

示例 1: 匿名函数赋值给变量

1
2
3
4
5
6
7
8
9
10
11
12
13
package main

import "fmt"

//定义一个匿名函数并赋值给myFun变量
var myFun = func(x,y int) int {
return x+y
}

func main() {
//调用myFun
fmt.Println(myFun(1,2))
}

输出结果:

1
3

Go 的函数内部是无法再声明一个有名字的函数的,Go 的函数内部只能声明匿名函数。

示例 2:

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 "fmt"

func main() {
myFunc3()
}


func myFun1() {
/*此处报错,函数内部不能声明带有名称的函数
func myFunc2() {

}
*/
}

func myFunc3() {
//函数内部可以声明一个匿名函数,并把这个匿名函数赋值给f变量
var f = func() {
fmt.Println("Hi,boy!")
}
//调用f
f()
//如果不想赋值给变量,那就必须在最后加上(),表示立即执行
func() {
fmt.Println("Hello,girl!")
}()//有参数可以写在这个小括号中
}

输出:

1
2
Hi,boy!
Hello,girl!

Go 中有闭包的功能。(闭包是一个通用的编程概念,一些语言有,一些没有,javascript 中就有这个概念,Java 中没有)

闭包,通俗易懂的讲,就是你有一个 A 函数,A 函数有一个 a 参数,然后在 A 函数内部再定义或者调用或者写一个 B 函数,这个 B 函数叫做闭包函数。B 函数内部的代码可以访问它外部的 A 函数的 a 参数,正常 A 函数调用返回完毕,a 参数就不能用了,可是闭包函数 B 函数仍然可以访问这个 a 参数,B 函数能不受 A 函数的调用生命周期限制可以随时访问其中的 a 参数,这个能访问的状态叫做已经做了闭包,闭包闭的是把 a 参数封闭到了 B 函数中,不受 A 函数的限制。

也就是说,我们用程序实现一个闭包的功能,实质上就是写一个让外层的函数参数或者函数内变量封闭绑定到内层函数的功能。

接下来我们看代码示例:

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

import "fmt"

//我们来看看实现闭包
func main() {
var f = f1(100)
f(100) //print 200
f(100) //print 300
f(100) //print 400
}

func f1(x int) func(int){
//此时即使f1函数执行完毕,x也不会消失
//x在func(y int)这个函数内一直存在并且叠加,
//这里把x的值封闭到func(y int)这个返回函数中,使其函数一直能使用x的值
//就叫做闭包,把x变量封闭到fun(y int)这个函数包内。
return func(y int){
x+=y
fmt.Printf("x=%d\n",x)
}
}

输出:

1
2
3
x=200
x=300
x=400

做下闭包的总结,如何实现一个闭包:

1. 定义一个 A 函数,此函数返回一个匿名函数。(定义一个返回匿名函数的 A 函数)

2. 把在 A 函数的 b 参数或 A 函数代码块中的 b 变量,放入匿名函数中,进行操作。

3. 这样我们调用 A 函数返回一个函数,这个函数不断的调用就可以一直使用之前 b 参数,b 变量,并且 b 值不会刷新,有点像在匿名函数外部自定义了一个 b 的成员变量(成员变量取自 Java 中类的相关概念)

# 29.Go 中的 map 和 Java 中的 HashMap

Go 中的 map 也是一个存储 key-value,键值对的这么一种数据结构。

我们来看下如何使用:

如何创建一个 map?(map 是引用类型,默认值是 nil,必须用 make 为其创建才能使用)

创建一个 map 必须要用 make,否则会是 nil

格式为: make (map [key 类型] value 类型) (下面有代码示例)

往 Go 中的 map 赋值添加元素用 【 map 变量名称 [key] = value 】 的方式

示例 1:创建 map 以及添加元素

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package main

import "fmt"

func main() {
//创建一个map必须要用make,否则会是nil
//格式为: make(map[key类型]value类型)
//Java中: Map<String,Integer> myMap = new HashMap<>();
myMap := make(map[string]int)
//往Go中的map赋值添加元素用 【 map变量名称[key] = value 】 的方式
//区别于Java中的: myMap.put("li_age",20);
myMap["li_age"] = 20
myMap["hong_age"] = 30
//打印一下map
fmt.Println(myMap)
}

我们从 map 中取值得格式为: 【 mapValue := map 变量名 [key]】

当我们填写的 key 在 map 中找不到时返回对应的 value 默认值,int 是 0,引用类型是 nil

当我们的 key 取不到对应的值,而 value 的类型是一个 int 类型,我们如何判断这个 0 是实际值还是默认值呢

此时我们需要同时取两个值

通过 map 的 key 取出两个值,第二个参数为 bool 类型,false 为该值不存在,true 为成功取到值

参考下面:

示例 2:从 map 中取值

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
package main

import "fmt"

func main() {
//创建一个map必须要用make,否则会是nil
//格式为: make(map[key类型]value类型)
//Java中: Map<String,Integer> myMap = new HashMap<>();
myMap := make(map[string]int)
//往Go中的map赋值添加元素用 【 map变量名称[key] = value 】 的方式
//区别于Java中的: myMap.put("li_age",20);
myMap["li_age"] = 20
myMap["hong_age"] = 30
//打印一下map
fmt.Println(myMap)
//不存在的值
fmt.Println(myMap["no"])

//当我们的key取不到对应的值,而value的类型是一个int类型,我们如何判断这个0是实际值还是默认值呢
//此时我们需要同时取两个值
//通过map的key取出两个值,第二个参数为bool类型,false为该值不存在,true为成功取到值
value,existsValue := myMap["no"]
if !existsValue {
fmt.Println("此值不存在")
} else {
fmt.Printf("value = %d",value)
}
}

Go 中因为返回值可以是两个,所以的 map 遍历很简单,不像 java 还得弄一个 Iterator 对象再逐个获取,它一次两个都能取出来,用 for 搭配 range 即可实现。

示例 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
package main

import "fmt"

func main() {
myMap := make(map[string]int)
myMap["num1"] = 1
myMap["num2"] = 2
myMap["num3"] = 3
myMap["num4"] = 4
myMap["num5"] = 5
myMap["num6"] = 6
//遍历key,value
for key,value := range myMap {
fmt.Println(key,value)
}
//写一个参数的时候只取key
for key := range myMap {
fmt.Println(key)
}
//如果只想取value,就需要用到之前的_标识符进行占位丢弃
for _,value := range myMap {
fmt.Println(value)
}
}

删除函数:用内置函数 delete 删除

示例 4:删除 map 元素

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

import "fmt"

func main() {
myMap := make(map[string]int)
myMap["num1"] = 1
myMap["num2"] = 2
myMap["num3"] = 3
myMap["num4"] = 4
myMap["num5"] = 5
myMap["num6"] = 6
//第二个参数为删除的key
delete(myMap,"num6")
//此时已经没有值了,默认值为0
fmt.Println(myMap["num6"])
}

在 Java 中有一些复杂的 Map 类型,比如:

1
2
Map<String,Map<String,Object>> data = new HashMap<>();
1

实际上,在 Go 语言中,也有复杂的类型,我们举几个代码示例

示例 5:

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
package main

import "fmt"

func main() {
//由map组成的切片
//第一部分 make[] 声明切片
//第二部分 map[string]int 声明该切片内部装的单个类型是map
//第三部分 参数 5 表示该切片的长度和容量都是5
//长度是用索引能取到第几个元素,索引不能超过长度-1,分配长度后都是默认值,int是0,引用类型是nil
//容量至少比长度大,能索引到几个+未来可添加元素个数(目前没有任何东西,看不见)= 切片容量
//make([]切片类型,切片长度,切片容量)
//make([]切片类型,切片长度和容量等同)
slice := make([]map[string]int,5,10)
slice0 := make([]map[string]int,0,10)
//我们看看打印的东西
fmt.Println("slice=",slice)
fmt.Println("slice=0",slice0)

///* 先看这段
//因为有5个长度,所以初始化了5个map,但是map没有通过make申请内容空间,所以报错nil map
//slice[0]["age"] = 10;//报错
//下面不报错
slice[0] = make(map[string]int,10)
slice[0]["age"] = 19
fmt.Println(slice[0]["age"])
//*/
}

输出结果:

1
2
3
4
slice= [map[] map[] map[] map[] map[]]
slice=0 []
19
123

接下来继续看代码:

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
package main

import "fmt"

func main() {
//由map组成的切片
//第一部分 make[] 声明切片
//第二部分 map[string]int 声明该切片内部装的单个类型是map
//第三部分 参数 5 表示该切片的长度和容量都是5
//长度是用索引能取到第几个元素,索引不能超过长度-1,分配长度后都是默认值,int是0,引用类型是nil
//append元素到切片时,是添加到最末尾的位置,当元素未超过容量时,都是用的同一个底层数组
//超过容量时会返回一个新的数组
//make([]切片类型,切片长度,切片容量)
//make([]切片类型,切片长度和容量等同)
slice := make([]map[string]int,5,10)
slice0 := make([]map[string]int,0,10)
//我们看看打印的东西
fmt.Println("slice=",slice)
fmt.Println("slice=0",slice0)

/* 先看这段
//因为有5个长度,所以初始化了5个map,但是map没有通过make申请内容空间,所以报错nil map
//slice[0]["age"] = 10;//报错
//下面不报错
slice[0] = make(map[string]int,10)
slice[0]["age"] = 19
fmt.Println(slice[0]["age"])
*/

}

输出:

1
panic: assignment to entry in nil map

看下面这个报错:

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"

func main() {
//由map组成的切片
//第一部分 make[] 声明切片
//第二部分 map[string]int 声明该切片内部装的单个类型是map
//第三部分 参数 5 表示该切片的长度和容量都是5
//长度是用索引能取到第几个元素,索引不能超过长度-1,分配长度后都是默认值,int是0,引用类型是nil
//append元素到切片时,是添加到最末尾的位置,当元素未超过容量时,都是用的同一个底层数组
//超过容量时会返回一个新的数组
//make([]切片类型,切片长度,切片容量)
//make([]切片类型,切片长度和容量等同)
slice := make([]map[string]int,5,10)
slice0 := make([]map[string]int,0,10)
//我们看看打印的东西
fmt.Println("slice=",slice)
fmt.Println("slice=0",slice0)

/* 先看这段
//因为有5个长度,所以初始化了5个map,但是map没有通过make申请内容空间,所以报错nil map
//slice[0]["age"] = 10;//报错
//下面不报错
slice[0] = make(map[string]int,10)
slice[0]["age"] = 19
fmt.Println(slice[0]["age"])
*/
///*
//因为初始化了0个长度,所以索引取不到值,报index out of range
slice0[0]["age"] = 10;

//*/
}

输出:

1
slice= [mappanic: runtime error: index out of range

接下来我们看一个:类似于 Java 中常用的 map 类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package main

import "fmt"

func main() {
//类似于Java中的Map<String,HashMap<String,Object>>
var myMap = make(map[string]map[string]interface{},10)
fmt.Println(myMap)
//记得make
myMap["li_ming_id_123"] = make(map[string]interface{},5)
myMap["li_ming_id_123"]["school"] = "清华大学"

fmt.Println(myMap)
}

输出:

1
2
map[]
map[li_ming_id_123:map[school:清华大学]]

# 30.Go 中的 time 时间包模块和 Java 中的时间 API 使用区别

Go 中关于时间处理的操作在 time 包中

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
32
33
34
35
36
37
38
package main

import (
"fmt"
"time"
)

func main() {
//获取当前时间
now := time.Now()
//获取当前年份
year := now.Year()
//获取当前月份
month := now.Month()
//获取当前 日期
day := now.Day()
//获取当前小时
hour := now.Hour()
//获取当前分钟
min := now.Minute()
//获取当前秒
second :=now.Second()

//获取当前时间戳,和其它编程语言一样,自1970年算起
timestamp := now.Unix()
//纳秒时间戳
ntimestamp := now.UnixNano()


fmt.Println("year=",year)
fmt.Println("month=",month)
fmt.Println("day=",day)
fmt.Println("hour=",hour)
fmt.Println("min=",min)
fmt.Println("second=",second)
fmt.Println("timestamp=",timestamp)
fmt.Println("ntimestamp=",ntimestamp)
}

2. 格式化时间

Go 的时间格式化和其它语言不太一样,它比较特殊,取了 go 的出生日期作为参数标准

参考如下代码示例:

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

import (
"fmt"
"time"
)

func main() {
//获取当前时间
now := time.Now()

//2006-01-02 15:04:05这个数值是一个标准写死的,只要改格式符号即可
fmt.Println(now.Format("2006-01-02 15:04:05"))
fmt.Println(now.Format("2006/01/02 15:04:05"))
fmt.Println(now.Format("2006/01/02"))//年月日
fmt.Println(now.Format("15:04:05"))//时分秒
}

# 31.Go 和 Java 关于 Socket 编程的对比

# 32. 聊聊 Go 语言如何连接 Mysql 数据库

# 33. 聊聊 Go 语言如何使用 Redis

# 34.Go 中的依赖管理–Module, 对比 Java 的 maven

# 35.Go 的协程高并发支持与 Java 的区别

# 36.Go 的性能调优和 Java 的性能调优

# 37.Go 的测试 API 与 Java 的单元测试

# 38. 自定义类型 Type

# 39.Go 的参数值传递与引用传递

接下来我们讲一下 Go 中的参数传递原理。

关于参数传递是一个什么概念呢,参数传递相关的知识是在研究当调用一个函数时,把外部的一个变量传入函数内,在函数内修改这个参数是否会对外部的参数变量的值有影响。参数传递用在的一个地方是函数的参数传递。(还有方法的接收器参数传递)

比如李明今天没有写作业,到了学校后匆匆忙忙的找小红要作业本 (小红的作业本为方法调用处传入的参数),想要抄一抄补上,所以李明有一个抄作业的任务 (抄作业的任务为函数),那么他有两个选择可以完成抄作业的任务。

第一个是直接拿过来小红的作业本开始抄,这在函数中叫做引用传递,因为如果小明抄的时候不小心桌子上的水打翻了,弄湿了小红的作业本,小红的作业本就真湿了,没法交了。

第二个是用打印机把小红的作业打印一份,然后拿着打印的那份抄,这叫做值传递,也就是说我拷贝一份值来用,那么我在抄作业 (任务函数内) 无论怎么弄湿小红的作业本,小红真正的自己的作业本也不受到影响。

在编程语言的函数中,如果是值传递,则是一个拷贝,在方法内部修改该参数值无法对其本身造成影响,如果是引用传递的概念,则可以改变其对象本身的值。

在 Go 语言中只有值传递,也是是说,无论如何 Go 的参数传递的都是一个拷贝。

重点来了:

Go 中的值传递有两种类型,如下:

1. 第一种值传递是具体的类型对象值传递,可能是 int,string,struct 之类的。

1
在此时,如果我们要自定义一个struct类型,传入参数中,可能遇到一个坑,因为是值传递,所以会拷贝一个struct对象,如果这个对象占内存比较大,而且这个函数调用频繁,会大量的拷贝消耗性能资源。

2. 第二种传递是叫指针参数类型的值传递,此时参数是一个指针类型,到具体的方法中,我们的参数也要用指针类型的参数接受,但是此时 Go 语言的内部做了一个黑箱操作。

举例 (下面还有完整可执行代码示例,先文字和伪代码举例):

我们有一个类型为 Boy 的结构体,还有一个方法 Mod

1
2
3
func Mod(b  *Boy){

}

这个 Mod 方法的参数是一个指针类型的 Boy 对象,

我们要调用的时候应该这样传参数:

1
2
3
var boy = Boy{} 
//用&取boy对象的指针地址,然后传入Mod方法
Mod(&boy)

我们看看下面的代码示例:

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
package main

import "fmt"

// Boy 结构体
type Boy struct {
name string
age int
}


func Mod(b *Boy) {
//这个是获取调用方法传入的参数的地址值
fmt.Printf("b的值(之前boy的地址)是%p\n",b)
//这个是获取本函数中 b这个指针变量的地址
fmt.Printf("b这个指针自己的地址是=%p\n",&b)
//打印值
//这里自动转换使指针可以直接点出来属性
fmt.Println(b.name,b.age)
}

func main() {
boy := Boy{"li_ming",20}
fmt.Printf("main函数中的boy地址是:%p\n",&boy)
//将boy的地址 放入Mod函数的参数中
Mod(&boy)
//注意!!!下面有黑箱操作:
/* //在&boy并放入Mod传递的过程中实际上做了如下黑箱操作
b := new(Boy) //创建一个名为b的类型为Boy的指针变量
b = &boy //把boy的地址存入b这个指针变量内
//接着把b放入func Mod(b *Boy)的参数中,然后,开始执行Mod方法。
fmt.Println(b.name,b.age)
fmt.Printf("b的地址是:%p\n",&b)
fmt.Printf("b的值是:%p\n",b)

//输出结果
//main函数中的boy地址是:0x10aec0c0
//li_ming 20
//b的地址是:0x10ae40f8
//b的值是:0x10aec0c0
*/

/*//以下代码无用,是指为了加深理解new,可以试试输出结果
boy2 := new(Boy)
fmt.Printf("main函数中new的boy2地址是:%p\n",boy2)
boy2.name = "xiaohong"
boy2.age = 18
Mod(boy2)
*/
}

所以,Go 中的参数传递所有的都是值传递,

只不过值传递中,值可以是指针类型,是创建了一个新的指针存储原来参数 (这个参数是原对象的地址) 的值。

所以你用原对象的地址改它的属性,是有点类似于引用类型传递的效果的。

为啥说指针类型也是值传递,因为他还是创建了一个新的指针对象,值传递就是拷贝,拷贝就得创建对象,只不过这个新的指针变量存储的值是原来的参数对象的地址。

最后总结一下:

1.Go 的参数传递都是值传递。

2. 指针类型的值传递可以改变原来对象的值。

3.make 和 new 从底层原理上创建的所有对象都是指针对象,所以 make 和 new 创建出来的 slice,map,chan 或者其它任何对象都是指针传递,改变值后都可以使原来的对象属性发生变化。

# 40. 结构体转 JSON

# 41.Go 如何搭建 HTTP-Server

# 42.Go 如何搭建 HTTP-Client

# 43.Go 如何设置使用的 CPU 个数

Go 语言天生支持高并发,其中一个体现就是如果你的 Go 程序不设置并发时使用的最大 cpu 核数的话,在高并发情况下 Go 会自动把所有 CPU 都用上,跑满。

1
2
3
4
5
6
拓展阅读:
我们简单理解一下cpu(懂得可以跳过)
举例:比如有一个专门做财务的公司(计算机),他们的赚钱业务很简单(计算机工作),就是帮别人做算术题(计算机工作的具体任务),加减乘除之类的算术题,现在公司有4个员工(物理意义上的4个cpu核数),有4本算数书(4个进程),每本书有10道题(线程),一共有40道算术题要算(40个线程任务),于是4个人一起干活,在同一时间,有4道算术题被计算,最后大致上每个人算了10道算数题。
第二天,有8本算术书(8个进程),他们为了快速完成任务,规定一人(每个人是一个物理cpu核数)管2本算数书(单物理cpu内部实际上是管理的两个不同的算数书,也就是相当于有两个不同的逻辑cpu),为的就是如果第一本做烦了可以换着做第2本,混合着做,最后都做完就可以。

cpu是进行最终二进制计算的核心计算器,cpu核数是有两个概念,一个是真实世界的物理硬件核数,比如4核cpu,就是有4个物理硬件内核,然而我们在生产环境的linux服务器上top的时候,出现的cpu个数实际上是逻辑cpu数,有可能linux服务器只有4核物理cpu,可是每个物理cpu分为两个逻辑cpu,这个时候我们在linux上top看的时候就是有8个cpu信息行数据。

我们回顾一下 Java,Java 运行时我们一般管理的都是线程数,而所有的 java 线程均在 JVM 这个虚拟机进程中,于是在高并发情况下,当 cpu 资源充足时,我们需要根据 cpu 的逻辑核数来确定我们的线程池线程数 (在高并发环境下一定要设置优化线程数啊!!!线程池就能设置线程数!!!),比如我们是 4 个物理 cpu, 每个双核逻辑,一共逻辑八核 cpu, 此时,比如我们要做并发定时任务,这台服务器没有其它程序,8 个 cpu 全都给我们自己用,那么我们的线程数最少也要设置成 8,再细化,我们得根据程序执行的任务分别在 cpu 计算 (正常处理程序业务逻辑) 的耗时和 cpuIO 耗时(IO 耗时比如查 mysql 数据库数据),假如我们定时跑批任务一个任务计算用时 0.2 秒,查数据库 0.8 秒 (自己可以写程序监测),那么可以参考如下公式:

总任务耗时 /cpu 耗时 = 多少个线程 (每个逻辑 cpu)

我们算出每个逻辑 cpu 要跑多少个线程后再乘以逻辑 cpu 的个数,就能算出来了

如下:

(0.2+0.8)/0.2=5 个线程 (每个逻辑 cpu)

5*8 = 40

于是我们在线程池的时候应该这么写:

1
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(40);

至于公式为什么要这么写,是因为 IO 操作的时候,cpu 是空闲的,也就是说,0.8 秒数据库操作的时候,cpu 都是空闲的,那么我们就多开几个线程让 cpu 在这 0.8 秒的时候工作,开几个呢,要等 0.8 秒,一个任务 cpu 要计算 0.2 秒,0.8/0.2=4 (个),可是这个逻辑 cpu 还有一个主线程在那 0.8 秒上等着结果呢,所以是 4+1=5(个)线程。

上述我们回顾了 Java 中的线程数和 CPU 核数相关,接下来我们来看 Go 语言。

我们下面来仔细讲讲 Go 中的 goroutine (实际是协程),是如何天然的支持高并发的,它与 Java 中的线程 Thread 又有什么区别,为什么它比线程能更好的支持高并发。

# 44. 初始化结构体,匿名结构体,结构体指针 (再讲)

# 45. 方法中的值接收和指针接收的区别 (方法进阶细节讲解)

我们之前讲了如果给一个类型绑定上一个接受者,就可以为这个类型添加一个这个类型独有的函数,只有这个类型对象自己能调用的函数,这个特殊的函数叫方法。

现在,我们讲一下方法关于传递接受者(自身引用)的进阶玩法。

Go 语言中的参数传递

# 46. 基于包模块的 Init 函数

# 47.Go 语言中的初始化依赖项

# 48.slice 相关知识点

slice 的中文意思是切片。

要想理解切片,我们首先要理解数组。

数组是一个长度不能变化的容器,存储同一数据类型的数据。

比如:int 数组

1
[1,2,3,4,5,6]

切片是对数组中一截,一小段,一个子集的地址的截取,切片存储的是它指向的底层数组中的一小截数据的地址,切片中不存数据,创建切片也不会把数组中的数据 copy 一份,切片只是存储着数组中一部分连续的数据的地址,切片的每一个元素实际上都指向具体的数组的中一个元素。

切片内部包含三个元素:

1. 底层数组(它指向的是哪一个数组)

我们要理解底层数组是什么,先举例:

[1,2,3] 这是一个 int 数组,其中元素 1 的地址是 0x0001, 元素 2 的地址是 0x0002,元素 3 的地址是 0x0003。

那么如果我们创建一个通过数组 [1,2,3] 创建一个切片 x。

这个 x 里面存储的并不是拷贝的另外一份新的 [1,2,3]。

切片 x 实际上是这样子的:

[0x0001,0x0002,0x0003]

当我们取出 x [0] 的时候,它操作的实际上是 0x0001 这个地址的元素,而这个地址实际上就是数组 [1,2,3] 中的 1 的地址。

也就是说,当我们修改了数组 [1,2,3] 中的 1 后,比如 0x0001 = 5 , 切片 x 中的 0 元素的取值自动也不一样了,因为 0x0001 地址上存储的 值已经被改成 5 了,而 x [0] 实际上还是 0x0001, 所以此时取出 x [0], 得到的就是 5。

切片存储的每一个元素实际上是它指向的底层数组的每一个元素的地址。

也就是说切片是一个引用类型,它不存储元素,不拷贝元素,它存储数组元素的引用,通过修改切片会修改原来数组的值。

2. 切片的长度

这个切片中有有几个元素,指向了数组中的几个连续的元素。

3. 切片的容量

从切片在底层数组的起始下标 (切片的首个元素) 到底层数组的最后一个元素,一共有几个元素,切片的容量就是几。

例如:(下面先用伪代码示例,后面有具体可执行代码)

原数组:a = [1,2,3,4,5,6,7,8]

切片: b = a [2:5] 从数组 a 的下标为 2 的开始,也就是具体数值是 3 开始,截取到下标为 5,下标为 5 的是 6,因为切片截取是左开右闭,所以切片中包括下标为 2 的数值 3,不包含下标为 5 的数值 6。

切片存储的地址指向的数据是:[3,4,5]

因为 3,4,5 有三个数,所以切片的长度是 3。

因为从切片的起始元素 3 到底层数组的末尾元素 8 之间有 6 个元素,所以切片的容量是 6。

修改切片实际上是修改切片指向的底层数组中的值。

# 49.Go 中类似于函数指针的功能

Go 中要实现函数指针非常简单。

因为 Go 中的函数也是一种类型。

所以我们只要声明一个变量,把某一个函数赋值给这个变量,就能实现函数指针的效果。

如下代码示例:

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

import "fmt"

//加法
func myAddFun(x,y int) int {
return x+y
}
//减法
func mySubFun(x,y int ) int{
return x-y;
}
//函数变量(类似于函数指针)
var myPointFun func(x,y int) int

func main() {
//加法函数赋值给该函数变量,相当于函数指针指向加法函数
myPointFun = myAddFun
fmt.Printf("a+b = %d\n",myPointFun(10,20))
//减法函数赋值给该函数变量,相当于函数指针指向减法函数
myPointFun = mySubFun
fmt.Printf("a-b = %d\n",myPointFun(100,50))
}

输出:

1
2
3
a+b = 30
a-b = 50
12

# 50.Go 有没有注解

原生的 Go 语言的 SDK 是不支持注解功能的,但是有一些其它的第三方机构为了实现自己的某些功能需求编写了一些自定义的注解。

# 51.Go 不能做大数据相关的开发

因为大数据的一些底层都是 Java 开发的,用 Java 实现接口开发功能非常方便快捷,对于 Go 语言的支持包比较少,另外就是一些算法库像 numpy,pandas 和一些机器学习,深度学习算法库 Python 支持的比较好,对于 Go 的支持很不好。

# 52.Go 没有泛型

Go 中不支持泛型 (明确)

# 53.Go 如何产生随机数 (随机数和种子)

# 54.Go 如何打类似于 (java jar 那种依赖包).a 的工具依赖包 (有了 Module 后不用这个了)

Go 中也有很多通过命令来完成辅助开发的工具,就像 Java 中 jdk 中的 java javac javap 等指令那种命令工具。

比如有 go build xxx 命令,go clean xxx 命令, go run xxx 命令…

Java 中打 jar 包可以通过 IDEA 集成开发环境图形界面化直接打包,也可以使用 jar 命令在命令行操作中 (使用不同的参数) 进行打包。

与 java jar 命令打包对应的 Go 的命令是 go install, 这个 go install 也类似于 maven 中的 install, 它会把打成的.a 后缀名结尾的工具包文件

放入 ${GOPATH} /pkg 下。

具体使用如下示例:

注意:使用 go install 之前必须在操作系统的环境变量中定义 ${GOPATH} 这个环境变量

1. 查看我们当前的操作系统中环境变量有没有定义 GOPATH。

2. 查看 ${GOPATH} 目录下是否有 src,pkg,bin 目录,并且保证我们的代码是在 src 下的。

3. 打开一个命令行窗口,比如 windows 是 cmd 打开一个 dos 窗口。

4. 我们在最开始之前已经把 go 的安装包下的包含 Go 操作指令的 bin 目录配置在了 PATH 环境变量中,所以此时我们可以不用管目录直接使用 go install 命令。

  1. 例如目录结构如下:
1
com.mashibing.gotest

-------------------------mygopackge

MyUtil.go

1
记住一点,此时MyUtil中不能是main包,也不能有main函数,否则打不出来.a结尾的依赖包。

此时编写执行命令:

1
go  install com/mashibing/gotest/mygopackge

此指令运行时,首先会去找 ${GOPATH} 目录

然后把后面的 com/mashibing/gotest/mygopackge 拼接上去

也就是 ${GOPATH}/com/mashibing/gotest/mygopackge

然后会把 ${GOPATH}/com/mashibing/gotest/mygopackge 下的所有.go 文件,比如 MyUtil.go 全部打包压缩进 mygopackge.a 文件

最后会把 mygopackage.a 放入 G O P A T H /p k g / {GOPATH}/pkg/GOPAT**H/pkg/{标示操作系统的一个名字 (这个不重要)}/com/mashibing/gotest/ 下。

最终.a 文件存储的结构是这样的:

${GOPATH}/pkg/com/mashibing/gotest/mygopackge.a

# 55.Go 中的依赖管理 Module 使用

# 1. 什么是 GoModule?(Go 中 Module 和包的区别?)

首先我们要理解一下 Go 的 Module 是一个什么概念?

我先简单的说一下,Go 中的 Module 是 GoSDK1.11 后提出的一个新的类似于包的机制,叫做模块,在 1.13 版本后成熟使用,GoSDK 中 Module 功能是和相当于一个包的增强版,一个模块类型的项目在根目录下必须有一个 go.mod 文件,这个模块项目内部可以有很多个不同的包,每个包下有不同的代码,我们下载依赖的时候是把这个模块下载下来 (模块以压缩包 (比如 zip) 的形式存储在 G O P A T H /p k g /m o d /c a c h e / 下,源码文件也会在 {GOPATH}/pkg/mod/cache/ 下,源码文件也会在 GOPAT**H/pkg/mod/cache / 下,源码文件也会在 {GOPATH}/pkg/mod/ 下)。

我们导入模块的时候只需要引入一次,使用模块中不同的包的时候可以通过 import 模块下不同的包名,来引入不同包的功能。

比如下面的结构

-----------com.mashibing.module

-----------------------package1

--------------test1.go

------------------------package2

-------------test2.go

然后我们只需要在 go.mod 中引入这一个模块,就能在 import 的时候任意引入 package1 或 package2。

# 2. 为什么要使用 GoModule?

# 1). 团队协作开发中对于包的版本号的管理

在没有 Module 之前,我们都是把自己写的 Go 程序打成包,然后别的程序引用的话引入这个包。

可是在开发中这些包的版本有个明显的不能管理的问题。

比如我怎么知道这个包是今天开发的最新版还是明天开发的,我在团队协同开发中怎么把别人写的最新版本的包更新到我的项目中。

# 2)便于开发中的依赖包管理

其次,我们在开发中下载了别人的项目,怎么快速的观察有哪些依赖包,如何快速的把所有依赖包从仓库中下载下来,都是一个问题,

这两个问题就可以通过观察项目根目录下的 go.mod 文件的依赖模块列表和执行 go mod download 命令快速从第三方模块仓库中下载依赖包来完成。

# 3). 隔离管理不同类别的项目

有了 Module 后,我们可以把我们自己的项目和系统的项目隔离管理,我们的项目不用必须放在 ${GOPATH}/src 下了

# 3. 哪些项目能使用 GoModule?

一个 GoModule 项目要想引入其它依赖模块,需要在根目录下的 go.mod 中添加对应的依赖模块地址。

注意:!!!重点来了!!!

GoModule 只能引用同样是 Module 类型的项目,经常用于引用内部自己的项目。

像 maven 仓库一样引用开源模块的依赖也是一个特别常用的场景。

不过我们需要修改代理地址访问国内的第三方 GoModule 提供商。

https://goproxy.cn/ 是一个国内的可访问的 GoModule 依赖仓库,类似于 Java 中 maven 中央仓库的概念。

# 4.GoModule 的版本问题?

我们使用 Go module 之前,首先要保证我们的 Go SDK 是在 1.13 以及以上版本。(Go1.11 以上就可以使用 Module, 但是需要设置一些开启等,1.13 后默认开启)

因为在 1.13 版本上官方正式推荐使用,成为稳定版了。

Go 也有代码仓库,比如可以使用 github 作为 go 项目的代码仓库,Go 语言本身就提供了一个指令 go get 来从指定的仓库中

拉取依赖包和代码,不过 go get 这个指令在开启模块功能和关闭模块功能下用法不一样,下面有开启模块下的用法。

# 5.GoModule 和 Java 中 Maven 的区别?

Go 中的 Module 和 Java 中的 Maven 不同:

首先,Module 是官方的 SDK 包自带的,它并非像 maven 一样还得安装 maven 插件之类的。

关于中央依赖仓库,Go 和 Java 中的概念是类似的,都是国内的第三方提供的。

# 6. 如何开启 GoModule?(GO111MODULE)

具体我们如何使用 Module 呢?

我们首先要检查我们的 GoSDK 版本是 1.11 还是 1.13 之上。

如果是 1.11 的话我们需要设置一个操作系统的中的环境变量,用于开启 Module 功能,这个是否开启的环境变量名是 GO111MODULE,

他有三种状态:

第一个是 on 开启状态,在此状态开启下项目不会再去 ${GOPATH} 下寻找依赖包。

第二个是 off 不开启 Module 功能,在此状态下项目会去 ${GOPATH} 下寻找依赖包。

第三个是 auto 自动检测状态,在此状态下会判断项目是否在 G O P A T H /s r c 外,如果在外面,会判断项目根目录下是否有 g o . m o d 文件,如果均有则开启 M o d u l e 功能,如果缺任何一个则会从 {GOPATH}/src 外,如果在外面,会判断项目根目录下是否有 go.mod 文件,如果均有则开启 Module 功能,如果缺任何一个则会从 GOPAT**H/src 外,如果在外面,会判断项目根目录下是否有 g**o.mod 文件,如果均有则开启 Modul**e 功能,如果缺任何一个则会从 {GOPATH} 下寻找依赖包。

GoSDK1.13 版本后 GO111MODULE 的默认值是 auto,所以 1.13 版本后不用修改该变量。

注意:在使用模块的时候, GOPATH 是无意义的,不过它还是会把下载的依赖储存在 ${GOPATH}/src/mod 中,也会把 go install 的结果放在 ${GOPATH}/bin 中。

windows

1
set GO111MODULE=on

linux

1
export GO111MODULE=on

# 7.GoModule 的真实使用场景 1:

接下来我们代入具体的使用场景:

今天,小明要接手一个新的 Go 项目,他通过 GoLand 中的 git 工具,从公司的 git 仓库中下载了一个 Go 的项目。(下载到他电脑的非 ${GOPATH}/src 目录,比如下载到他电脑的任意一个自己的工作空间)

此时他要做的是:

1). 先打开项目根目录下的 go.mod 文件看看里面依赖了什么工具包。(这个就是随便了解一下项目)

2).Go 的中央模块仓库是 Go 的官网提供的,在国外是 https://proxy.golang.org 这个地址,可是在国内无法访问。

我们在国内需要使用如下的中央模块仓库地址:https://goproxy.cn

我们 Go 中的 SDK 默认是去找国外的中央模块仓库的,如何修改成国内的呢?

我们知道,所有的下载拉取行为脚本实际上是从 go download 这个脚本代码中实现的,而在这个脚本中的源码实现里,肯定有一个代码是写的是取出操作系统中的一个环境变量,这个环境变量存储着一个地址,这个地址代表了去哪个中央模块仓库拉取。

在 GoSDK 中的默认实现里,这个操作系统的环境变量叫做 GOPROXY,在脚本中为其赋予了一个默认值,就是国外的 proxy.golang.org 这个值。

我们要想修改,只需要在当前电脑修改该环境变量的值即可:

(注意,这个变量值不带 https, 这只是一个变量,程序会自动拼接 https)

windows

1
set GOPROXY=goproxy.cn

linux

1
export GOPROXY=goproxy.cn

3). 切换到项目的根目录,也就是有 go.mod 的那层目录,打开命令行窗口。

执行 download 指令 (下载模块项目到 ${GOPATH}/pkg/mod 下)

1
go  mod  download

4). 如果不报错,代表已经下载好了,可以使用了,此时在项目根目录会生成一个 go.sum 文件。

一会再讲解 sum 文件。

5). 此时可以进行开发了。

# 8.GoModule 的真实使用场景 2:

场景 2:我们如何用命令创建一个 Module 的项目,(开发工具也能手动创建)。

切换到项目根目录,执行如下指令:

1
go  mod init 模块名(模块名可不写)

然后会在根目录下生成一个 go.mod 文件

我们看看这个 go.mod 文件长啥样?

1
2
3
4
// 刚才init指令后的模块名参数被写在module后了
module 模块名
//表示使用GoSDK的哪个版本
go 1.14

修改 go.mod 文件中的依赖即可。

我们有两种方式下载和更新依赖:

1. 修改 go.mod 文件,然后执行 go mod down 把模块依赖下载到自己 ${GOPATH}/pkg/mod 下,这里面装的是所有下载的 module 缓存依赖文件,其中有 zip 的包,也有源码,在一个项目文件夹下的不同文件夹下放着,还有版本号文件夹区分,每个版本都是一个文件夹。

2. 直接在命令行使用 go get package@version 更新或者下载依赖模块,升级或者降级模块的版本。(这里是开启模块后的 go get 指令用法)

例如:

1
go get  github.com/gin-contrib/sessions@v0.0.1

这个指令执行过后,会自动修改 go.mod 中的文件内容,不需要我们手动修改 go.mod 文件中的内容。

# 9.go.mod 文件详解

接下来我们讲讲核心配置文件 go.mod

go.mod 内容如下:

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
//表示本项目的module模块名称是什么,别的模块依赖此模块的时候写这个名字
module test
//表示使用GoSDK的哪个版本
go 1.14
//require中声明的是需要依赖的包和包版本号
require (
//格式如下: 需要import导入的模块名 版本号
// 需要import导入的模块名2 版本号2
// ... ...
github.com/gin-contrib/sessions v0.0.1
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/gin-gonic/gin v1.4.0
github.com/go-redis/redis v6.15.6+incompatible
github.com/go-sql-driver/mysql v1.4.1
github.com/golang/protobuf v1.3.2 // indirect
github.com/jinzhu/gorm v1.9.11
github.com/json-iterator/go v1.1.7 // indirect
github.com/kr/pretty v0.1.0 // indirect
github.com/mattn/go-isatty v0.0.10 // indirect
github.com/sirupsen/logrus v1.2.0
github.com/ugorji/go v1.1.7 // indirect
)
//replace写法如下,表示如果项目中有引入前面的依赖模块,改为引用=>后面的依赖模块,
//可以用于golang的国外地址访问改为指向国内的github地址,当然你在上面require直接写github就不用在这里repalce了
replace (
golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a => github.com/golang/crypto v0.0.0-20190313024323-a1f597ede03a
)
//忽略依赖模块,表示在该项目中无论如何都使用不了该依赖模块,可以用于限制使用某个有bug版本的模块
exclude(
github.com/ugorji/go v1.1.7
)

注:go.mod 提供了 module, require、replace 和 exclude 四个命令

module 语句指定包的名字(路径)
require 语句指定的依赖项模块
replace 语句可以替换依赖项模块
exclude 语句可以忽略依赖项模块

上面 github.com/ugorji/go v1.1.7 //indirect 有 indirect 和非 indirect

indirect 代表此模块是间接引用的,中间隔了几个项目

这个不用特殊写,可以注释写便于识别和开发

# 10.GoModule 有哪些命令?如何使用?

Go 有如下关于 Module 的命令:

1
2
3
4
5
6
7
8
9
//go mod   命令:
download //下载依赖模块到${GOPATH}/pkg/mod
edit //一系列参数指令用于操作go.mod文件,参数太多,具体下面有例子
graph //输出显示每一个模块依赖了哪些模块
init //在一个非module项目的根目录下创建一个go.mod文件使其变为一个module管理的项目
tidy //根据项目实际使用的依赖包修改(删除和添加)go.mod中的文本内容
vendor //在项目根目录创建一个vender文件夹 然后把${GOPATH}/pkg/mod下载的本项目需要的依赖模块拷贝到本项目的vender目录下
verify //校验${GOPATH}/pkg/mod中的依赖模块下载到本地后是否被修改或者篡改过
why //一个说明文档的功能,用于说明一些包之间的为什么要这么依赖。(没啥用)
# 0). init 和 download

我们之前在案例中讲了 init,download 指令,这里不再赘述

# 1).go mod edit

是指在命令行用指令通过不同的参数修改 go.mod 文件,这个指令必须得写参数才能正确执行,不能空执行 go mod edit

参数 1 :-fmt

1
go mod edit -fmt

格式化 go.mod 文件,只是格式规范一下,不做其它任何内容上的修改。

其它任何 edit 指令执行完毕后都会自动执行 - fmt 格式化操作。

这个使用场景就是我们如果不想做任何操作,就想试试 edit 指令,就只需要跟上 - fmt 就行,因为单独不加任何参数

只有 go mod edit 后面不跟参数是无法执行的。

我们如何升级降级依赖模块的版本,或者说添加新的依赖和移除旧的依赖呢

参数 2: -require=path@version /-droprequire=path flags

添加一个依赖

1
go mod  edit -require=github.com/gin-contrib/sessions@v0.0.1

删除一个依赖

1
go mod edit -droprequire=github.com/gin-contrib/sessions@v0.0.1

这两个和 go get package@version 功能差不多,但是官方文档更推荐使用 go get 来完成添加和修改依赖(go get 后的 package 和上面的 path 一个含义,都是模块全路径名)

参数 3:-exclude=path@version and -dropexclude=path@version

排除某个版本某个模块的使用,必须有该模块才可以写这个进行排除。

1
go mod edit -exclude=github.com/gin-contrib/sessions@v0.0.1

删除排除

1
go mod edit -dropexclude=github.com/gin-contrib/sessions@v0.0.1

简单来说,执行这两个是为了我们在开发中避免使用到不应该使用的包

… 还有好几个,基本很少用,省略了

# 2).go mod graph

命令用法: 输出每一个模块依赖了哪些模块 无参数,直接使用 ,在项目根目录下命令行执行

1
go mod graph

比如:

模块 1 依赖了模块 a

模块 1 依赖了模块 b

模块 1 依赖了模块 c

模块 2 依赖了模块 x

模块 2 依赖了模块 z

如下是具体例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
C:\${GOPAHT}\file\project>go mod graph
file\project github.com/edgexfoundry/go-mod-bootstrap@v0.0.35
github.com/edgexfoundry/go-mod-bootstrap@v0.0.35 github.com/BurntSushi/toml@v0.3.1
github.com/edgexfoundry/go-mod-bootstrap@v0.0.35 github.com/edgexfoundry/go-mod-configuration@v0.0.3
github.com/edgexfoundry/go-mod-bootstrap@v0.0.35 github.com/edgexfoundry/go-mod-core-contracts@v0.1.34
github.com/edgexfoundry/go-mod-bootstrap@v0.0.35 github.com/edgexfoundry/go-mod-registry@v0.1.17
github.com/edgexfoundry/go-mod-bootstrap@v0.0.35 github.com/edgexfoundry/go-mod-secrets@v0.0.17
github.com/edgexfoundry/go-mod-bootstrap@v0.0.35 github.com/gorilla/mux@v1.7.1
github.com/edgexfoundry/go-mod-bootstrap@v0.0.35 github.com/pelletier/go-toml@v1.2.0
github.com/edgexfoundry/go-mod-bootstrap@v0.0.35 github.com/stretchr/testify@v1.5.1
github.com/edgexfoundry/go-mod-bootstrap@v0.0.35 gopkg.in/yaml.v2@v2.2.8
github.com/edgexfoundry/go-mod-configuration@v0.0.3 github.com/cenkalti/backoff@v2.2.1+incompatible
github.com/edgexfoundry/go-mod-configuration@v0.0.3 github.com/hashicorp/consul/api@v1.1.0
# 3).go mod tidy

根据实际项目使用到的依赖模块,在 go.mod 中添加或者删除文本引用

有一个参数可选项 -v 输出在 go.mod 文件中删除的引用模块信息

比如我们项目用到一个模块,go.mod 中没写,执行后 go.mod 中就会添加上该模块的文本引用。

如果我们在 go.mod 中引用了一个模块,检测在真实项目中并没有使用,则会在 go.mod 中删除该文本引用。

使用如下:

1
go  mod  tidy -v

输出:

1
unused github.com/edgexfoundry/go-mod-bootstrap

输出表示检测项目没有使用到该模块,然后从 go.mod 中把该包的引用文字给删除了。

# 4).go mod vender

该指令会在项目中建立一个 vender 目录,然后把 ${GOPATG}/pkg/mod 中下载的依赖拷贝到项目的 vender 目录中,方便管理和方便在 idea 中引用依赖。 -v 参数可以在控制台输出相关的结果信息

1
go mod vender -v 
# 5).go mod verify

验证下载到 ${GOPATH}/pkg/mod 中的依赖模块有没有被修改或者篡改。

结果会输出是否被修改过

1
go mod verify

比如输出:

1
all modules verified

这个是所有模块已经验证,代表没有被修改,如果被修改,会提示哪些被修改。

# 6).go mod why

这个没啥用,说白了就是一个解释文档,输入参数和依赖他说明哪些包为啥要依赖这些包,不用看它,用处不大。

# 11.go.sum 详细讲解

# 1).go.sum 什么时候会更新或者新建生成?

当我们通过 go mod download 下载完依赖模块或者 go get package@version 更新了依赖包的时候

,会检查根目录下有没有一个叫 go.sum 的文件,没有的话则创建一个并写入内容,有的话会更新 go.sum 中的内容。

# 2).go.sum 是用来做什么的?

go.sum 的作用是用来校验你下载的依赖模块是否是官方仓库提供的,对应的正确的版本的,并且中途没有被黑客篡改的。

go.sum 主要是起安全作用和保证依赖的版本肯定是官方的提供的那个版本,版本确认具体是确认你下载的那个模块版本里面的代码的和官方提供的模块的那个版本的代码完全相同,一字不差。

通过 go.sum 保证安全性是很有必要的,因为如果你的电脑被黑客攻击了,黑客可以截取你对外发送的文件,也可以修改发送给你的文件,那么就会产生一个问题:

本来的路径应该是这样的: 第三方模块依赖库 ------------> 你的电脑

结果中间有黑客会变成这样:

第三方模块依赖库 --------> 黑客修改了依赖库中的代码,植入病毒代码,并重新打成模块发送给你 ---------> 你以为是官方的版本

结果黑客就把病毒代码植入到了你的项目中,你的项目就不安全了,面临着数据全部泄露的风险。

# 3).go.sum 是如何实现校验机制的?它包含什么内容?

说到校验安全机制,有一种常规的玩法就是使用不可逆加密算法,不可逆加密算法是指将 a 文本通过算法加密成 b 文本后,b 文本永远也不能反着计算出 a 文本。

不可加密算法的具体是怎么应用的呢?它是如何起作用的?

我们在这里先讲一个不可逆的加密算法 SHA-256 算法。

SHA-256 算法的功能就是将一个任意长度的字符串转换成一个固定长度为 64 的字符串,比如:

4e07408562bedb8b60ce05c1decfe3ad16b72230967de01f640b7e4729b49fce

这里从 4e07 代表四个字符串,按此算,这个加密后的字符串为 64 个。

为什么是 64 个呢?

因为 64 个字符串每两个字符为一组,比如 4e 是一组,07 是一组,也就是说有 32 组,每一组是一个十六进制的数值,一个十六进制的数值也就是两个字符用计算机中的 8 个字节内存空间存储,也就是一个十六进制的数字,有两个字符串,占 8 个字节,一个字节等同 8 位 (bit)(位只能存储 0 和 1 两个值),也就是说:

32(32 个十六进制数,每个十六进制数用两个字符表示)*8 字节 = 256 位。

仔细看名字,SHA 代表是算法的加密方式类型,256 代表的是他这个是 256 位的版本。

具体原理实现是 SHA 内部定义了一系列固定数值的表,然后加密的时候无论是需要加密多少文字,它都按照一定的规则从需要加密的文字中按一定规则抽取其中的缩略一部分,然后拿缩略的一部分和 SHA 内部的固定数值表进行固定的 hash 映射和算术操作,这个 hash 映射和算术操作的顺序是固定写死的,公共数据表是写死的,这个写死的顺序和公共数据表就是这个算法的具体内容本质。

这样的话,因为抽取的是缩略的内容,所以我们可以把输出结果固定在 64 个字符,256 位。

因为是缩略的内容, 所以我们不可能通过缩略的内容反推出完整的结果。

但是,相同的文本按照这个算法加密出来的 64 个字符肯定是相同的,同时,只要改变原需要加密文本的一个字符,也会造成加密出来的 64 个字符大不相同。

我们用 SHA-256 通常是这么用的:

A 方 要 发送信息给 B 方

B 方 要确定信息是 A 方发送的,没有经过篡改

此时 A 和 B 同时约定一个密码字符串,比如 abc。

这个 abc 只有 A 方和 B 方知道。

A 方把 需要传输的文本拼接上 abc,然后通过 SHA-256 加密算出一个值,把原文本和算出的值全部发送给 B。

B 方 拿出原文本,拼接上 abc,进行 SHA256 计算,看看结果是否和传输过来的 A 传输的值一样,如果一样,代表中间没有被篡改。

为什么呢?

因为如果有一个黑客 C 想要篡改,他就得同时篡改原文本和算出的签名值。

可是 C 不知道密码是 abc,它也就不能把 abc 拼接到原文后,所以它算出来的签名和 B 算出来的签名肯定不一致。

所以 B 如果自己算出的签名值与接收到的签名值不一致,B 就知道不是 A 发过来的,就能校验发送端的源头是否是官方安全的了。


接下来我们讲一下 go.sum 的验证机制。

首先说下 go.sum 中存储的内容,这个文件存储的每一行都是如下格式

模块名 版本号 hash 签名值

示例:

1
2
3
github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=   

github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=

这里的 hash 签名值是拿当前模块当前版本号内的所有代码字符串计算出来的一个值,就是通过上面讲解的 SHA-256 计算的。

所以哪怕是这个模块中的代码有一个字变了,计算出来的 hash 值也不相同。

第三方模块库在每发布一个新的模块版本后,会按照 SHA-256 计算出对应版本的 hash 值,然后提供给外部获取用于检验安全性。

当我们 go mod download 和 go get package@version 后 会更新 go.mod 中的模块路径和版本。

然后会更新或者创建根目录下 go.sum 文件中的模块名 版本号 和 hash 值。

在 go.sum 中的 hash 值是在下载和更新依赖包的时候,同时获取官方提供的版本号得来的。

也就是说,基本上 go.sum 中的文件都是从官网(外国)(中国是第三方模块仓库)上获得的正品版本号,这个版本号是仓库方自己算的,你只是获取到了存储到你自己的 go.sum 中。

具体如何获取版本号有个小知识点:

1
go module机制在下载和更新依赖的时候会取出操作系统中名为`GOSUMDB`的环境变量中的值,这个服务器地址值代表了从哪个第三方仓库获取对应的正品版本号。

重点来了,当你在 go build 打包创建 go 项目的时候,go build 的内部指令会去拿你本地的模块文件进行 SHA-256 计算,然后拿到一个计算出来的结果值,之后它会拿此值和 go.sum 中的正确的从官网拉取的值进行对比,如果不一样,说明这个模块包不是官方发布的,也就是你本地的模块包和官方发布的模块包中的代码肯定有差异。

# 四。专门详解 Go 并发编程相关知识

# 1.Go 为什么天然支持高并发,纤程比线程的优势是什么?

Go 语言在设计的时候就考虑了充分利用计算机的多核处理器,具体表现为,Go 中开启一个并发的任务以操作系统的线程资源调度为单位的,而是 Go 的创造者们自己写了一套管理多个任务的机制,在这个机制下,每一个并发的任务线程叫做纤程,这个纤程的作用等同一个线程,也是并发执行的,只不过纤程是在应用程序管理的,懂底层的可以讲是在用户态的一个线程,而 Java 中调度的线程是属于操作系统,也就是操作系统内核态的线程。

用户态的纤程归属于用户编写的软件管理和调度,优点是可以根据情况灵活实现堆栈的内存分配,最优化其中的运行资源配置。

内核态的线程归属于操作系统调度和管理,他底层是有 windows 或者 linux 操作系统底层的代码管理的,那么他就不灵活,每个线程分配的资源可能造成浪费,创建的线程数肯定也有一定的限制。

Go 的创造可以为自己的语言和任务灵活配置资源,Linux 和 windows 操作系统的代码是通用的,总不能为你这个语言修改源代码把。

在实际程序运行中,一个操作系统的内核态线程可能管理着好几个甚至数十个纤程 (根据实际情况和设置不同而不同),所以省去了线程时间片上下文切换的时间。

同时因为内部机制灵活,所以执行效率高,占用内存也少。

这就是 Go 语言的并发优势的核心所在。

# 2. 并发和并行的区别?

并发是指的一个角色在一段时间内通过来回切换处理了多个任务。

并行是指两个或者多个角色同时处理自己的任务。

举例:

并发:在一个小时内,你写了 10 分钟语文作业,又写了 10 分钟数学,之后又写了 10 分中英语作业,然后再从语文 10 分钟,数学 10 分钟,英文 10 分钟又来一次。

这个叫做你并发的写语文数学英语作业。

你一个一段时间(一个小时内)通过切换(一会写数学,一会写语文。。。),处理了多个任务(写了三门课的作业)

并行:你和小明同时写自己的作业。你们俩同时运行的状态叫做并行运作状态,强调的是你们两个人同时在处理任务 (做作业)。

你和小明 (两个以上的角色) 同时写作业 (处理自己的任务)。

在计算机中,比如有 4 个 cpu,4 个 cpu 同时工作,叫做这 4 个 cpu 并行执行任务,每个 cpu 通过时间片机制上下文切换处理 100 个小任务,叫做每个 cpu 并发的处理 100 个任务。

# 3.Go 是如何用 Channel 进行协程间数据通信数据同步的?

go 中的线程相关的概念是 Goroutines (并发),是使用 go 关键字开启。

Java 中的线程是通过 Thread 类开启的。

在 go 语言中,一个线程就是一个 Goroutines,主函数就是(主) main Goroutines。

使用 go 语句来开启一个新的 Goroutines

比如:

普通方法执行

myFunction()

开启一个 Goroutines 来执行方法

go myFunction()

java 中是

new Thread(()->{

// 新线程逻辑代码

}).start();

参考下面的代码示例:

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
package main

import (
"fmt"
)

//并发开启新线程goroutine测试

//我的方法
func myFunction() {
fmt.Println("Hello!!!")
}
//并发执行方法
func goroutineTestFunc() {
fmt.Println("Hello!!! Start Goroutine!!!")
}


func main() {
/*
myFunction()
//go goroutineTestFunc()
//此时因为主线程有时候结束的快,goroutineTestFunc方法得不到输出,由此可以看出是开启了新的线程。
*/
//打开第二段执行
/*
go goroutineTestFunc()
time.Sleep(10*time.Second)//睡一段时间 10秒
myFunction()
*/
}

线程间的通信:

java 线程间通信有很多种方式:

比如最原始的 wait/notify

到使用 juc 下高并发线程同步容器,同步队列

到 CountDownLatch 等一系列工具类

甚至是分布式系统不同机器之间的消息中间件,单机的 disruptor 等等。

Go 语言不同,线程间主要的通信方式是 Channel。

Channel 是实现 go 语言多个线程(goroutines)之间通信的一个机制。

Channel 是一个线程间传输数据的管道,创建 Channel 必须声明管道内的数据类型是什么

下面我们创建一个传输 int 类型数据的 Channel

代码示例:

1
2
3
4
5
6
7
8
package main

import "fmt"

func main() {
ch := make(chan int)
fmt.Println(ch)
}

channel 是引用类型,函数传参数时是引用传递而不是值拷贝的传递。

channel 的空值和别的应用类型一样是 nil。

== 可以比较两个 Channel 之间传输的数据类型是否相等。

channel 是一个管道,他可以收数据和发数据。

具体参照下面代码示例:

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
package main

import (
"fmt"
"time"
)
//channel发送数据和接受数据用 <-表示,是发送还是接受取决于chan在 <-左边还是右边
//创建一个传输字符串数据类型的管道
var chanStr = make(chan string)
func main() {
fmt.Println("main goroutine print Hello ")
//默认channel是没有缓存的,阻塞的,也就是说,发送端发送后直到接受端接受到才会施放阻塞往下面走。
//同样接收端如果先开启,直到接收到数据才会停止阻塞往下走
//开启新线程发送数据
go startNewGoroutineOne()
//从管道中接收读取数据
go startNewGoroutineTwo()
//主线程等待,要不直接结束了
time.Sleep(100*time.Second)
}

func startNewGoroutineOne() {
fmt.Println("send channel print Hello ")
//管道发送数据
chanStr <- "Hello!!!"
}

func startNewGoroutineTwo(){
fmt.Println("receive channel print Hello ")
strVar := <-chanStr
fmt.Println(strVar)
}

无缓存的 channel 可以起到一个多线程间线程数据同步锁安全的作用。

缓存的 channel 创建方式是

make (chan string, 缓存个数)

缓存个数是指直到多个数据没有消费或者接受后才进行阻塞。

类似于 java 中的 synchronized 和 lock

可以保证多线程并发下的数据一致性问题。

首先我们看一个线程不安全的代码示例:

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
package main

import (
"fmt"
"time"
)

//多线程并发下的不安全问题
//金额
var moneyA int =1000
//添加金额
func subtractMoney(subMoney int) {
time.Sleep(3*time.Second)
moneyA-=subMoney
}

//查询金额
func getMoney() int {
return moneyA;
}


func main() {

//添加查询金额
go func() {
if(getMoney()>200) {
subtractMoney(200)
fmt.Printf("200元扣款成功,剩下:%d元\n",getMoney())
}
}()

//添加查询金额
go func() {
if(getMoney()>900) {
subtractMoney(900)
fmt.Printf("900元扣款成功,剩下:%d元\n",getMoney())
}
}()
//正常逻辑,只够扣款一单,可以多线程环境下结果钱扣多了
time.Sleep(5*time.Second)
fmt.Println(getMoney())
}

缓存为 1 的 channel 可以作为锁使用:

示例代码如下:

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
package main

import (
"fmt"
"time"
)

//多线程并发下使用channel改造
//金额
var moneyA = 1000
//减少金额管道
var synchLock = make(chan int,1)

//添加金额
func subtractMoney(subMoney int) {
time.Sleep(3*time.Second)
moneyA-=subMoney
}

//查询金额
func getMoney() int {
return moneyA;
}


func main() {

//添加查询金额
go func() {
synchLock<-10
if(getMoney()>200) {
subtractMoney(200)
fmt.Printf("200元扣款成功,剩下:%d元\n",getMoney())
}
<-synchLock
}()

//添加查询金额
go func() {
synchLock<-10
if(getMoney()>900) {
subtractMoney(900)
fmt.Printf("900元扣款成功,剩下:%d元\n",getMoney())
}
synchLock<-10
}()
//这样类似于java中的Lock锁,不会扣多
time.Sleep(5*time.Second)
fmt.Println(getMoney())
}

# 4.Go 中的 Goroutine 使用和 GMP 模型?

Go 中的线程 (实际是纤程) goroutine 的底层管理和调度是在 runtime 包中自己实现的,其中遵循了 GMP 模型。

G 就是一个 goroutine,包括它自身的一些元信息。

M 是指操作系统内核态的线程的一个虚拟表示,一个 M 就是操作系统内核态的一个线程。

P 是一个组列表,P 管理着多个 goroutines,P 还有一些用于组管理的元数据信息。

# 5.Go 的 select 怎么用?

Go 中的 select 是专门用于支持更好的使用管道 (channel) 的。

我们之前虽然讲了能从管道中读取数据,但是这有一个缺陷,就是我们在一个 Goroutine 中不能同时处理读取多个 channel,因为在一个 Goroutine 中,一个 channel 阻塞后就无法继续运行了,所以无法在一个 Goroutine 处理多个 channel, 而 select 很好的解决了这个问题。

select 相当于 Java 中 Netty 框架的多路复用器的功能。

举例代码示例:

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

import "fmt"

func main() {
//创建一个缓存为1的chan
myChan := make(chan int,1)
for i:=1;i<=100;i++{
//select 的用法是,从上到下依次判断case 是否可执行,如果可执行,则执行完毕跳出select,如果不能执行,尝试下一个执行
//这里的可执行是指的不阻塞,也就是说,select从上到下开始挑选一个不阻塞的case执行,执行完毕后跳出,
//如果所有case都阻塞,则执行default
//如下输出结果,i=奇数的时候走case myChan<-i:,把奇数放入mychan
//走偶数的时候因为myChan中有数据了,则把上一个奇数打印出来。
//所以结果是 1 3 5 7 ...
select {
case data := <-myChan:
fmt.Println(data)
case myChan<-i:
default:
fmt.Println("default !!!")
}
}

}

# 6.Go 中的互斥锁 (类似于 Java 中的 ReentrantLock)

先按线程不安全的数据错误的代码示例:

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
package main

import (
"fmt"
"sync"
)

//全局变量
var num int

var wait sync.WaitGroup

func main() {
wait.Add(5)
go myAdd()
go myAdd()
go myAdd()
go myAdd()
go myAdd()
wait.Wait()
//预期值等于5万,可是因为线程不安全错误,小于5万
fmt.Printf("num = %d\n",num)
}


func myAdd() {
defer wait.Done()
for i:=0 ;i<10000;i++ {
num+=1
}
}

打印输出结果:

1
num = 38626  

互斥锁示例代码如下:

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"
)

//全局变量
var num int

var wait sync.WaitGroup

var lock sync.Mutex
func main() {
wait.Add(5)
go myAdd()
go myAdd()
go myAdd()
go myAdd()
go myAdd()
wait.Wait()
//预期值等于5万,可是因为线程不安全错误,小于5万
fmt.Printf("num = %d\n",num)
}


func myAdd() {
defer wait.Done()
for i:=0 ;i<10000;i++ {
lock.Lock()
num+=1
lock.Unlock()
}
}

# 7.Go 中的读写锁 (类似于 Java 中的 ReentrantReadWriteLock)

读写锁用于读多写少的情况,多个线程并发读不上锁,写的时候才上锁互斥

读写锁示例代码如下:

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
package main

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


//金额
var moneyA = 1000
//读写锁
var rwLock sync.RWMutex;
var wait sync.WaitGroup
//添加金额
func subtractMoney(subMoney int) {
rwLock.Lock()
time.Sleep(3*time.Second)
moneyA-=subMoney
rwLock.Unlock()
}

//查询金额
func getMoney() int {
rwLock.RLock()
result := moneyA
rwLock.RUnlock()
return result;
}


func main() {
wait.Add(2)
//添加查询金额
go func() {
defer wait.Done()
if(getMoney()>200) {
subtractMoney(200)
fmt.Printf("200元扣款成功,剩下:%d元\n",getMoney())
}else {
fmt.Println("余额不足,无法扣款")
}
}()

//添加查询金额
go func() {
defer wait.Done()
if(getMoney()>900) {
subtractMoney(900)
fmt.Printf("900元扣款成功,剩下:%d元\n",getMoney())
}else {
fmt.Println("余额不足,无法扣款")
}
}()
wait.Wait()
fmt.Println(getMoney())
}

# 8.Go 中的并发安全 Map (类似于 CurrentHashMap)

Go 中自己通过 make 创建的 map 不是线程安全的,具体体现在多线程添加值和修改值下会报如下错误:

1
2
fatal error : concurrent map writes
1

这个错类似于 java 中多线程读写线程不安全的容器时报的错。

Go 为了解决这个问题,专门给我们提供了一个并发安全的 map,这个并发安全的 map 不用通过 make 创建,拿来即可用,并且他提供了一些不同于普通 map 的操作方法。

参考如下代码示例:

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
package main

import (
"fmt"
"sync"
)

//创建一个sync包下的线程安全map对象
var myConcurrentMap = sync.Map{}
//遍历数据用的
var myRangeMap = sync.Map{}

func main() {
//存储数据
myConcurrentMap.Store(1,"li_ming")
//取出数据
name,ok := myConcurrentMap.Load(1)
if(!ok) {
fmt.Println("不存在")
return
}
//打印值 li_ming
fmt.Println(name)
//该key有值,则ok为true,返回它原来存在的值,不做任何操作;该key无值,则执行添加操作,ok为false,返回新添加的值
name2, ok2 := myConcurrentMap.LoadOrStore(1,"xiao_hong")
//因为key=1存在,所以打印是 li_ming true
fmt.Println(name2,ok2)
name3, ok3 := myConcurrentMap.LoadOrStore(2,"xiao_hong")
//因为key=2不存在,所以打印是 xiao_hong false
fmt.Println(name3,ok3)
//标记删除值
myConcurrentMap.Delete(1)
//取出数据
//name4,ok4 := myConcurrentMap.Load(1)
//if(!ok4) {
// fmt.Println("name4=不存在")
// return
//}
//fmt.Println(name4)

//遍历数据
rangeFunc()
}
//遍历
func rangeFunc(){
myRangeMap.Store(1,"xiao_ming")
myRangeMap.Store(2,"xiao_li")
myRangeMap.Store(3,"xiao_ke")
myRangeMap.Store(4,"xiao_lei")

myRangeMap.Range(func(k, v interface{}) bool {
fmt.Println("data_key_value = :",k,v)
//return true代表继续遍历下一个,return false代表结束遍历操作
return true
})

}

# 9.Go 中的 AtomicXXX 原子操作类 (类似于 Java 中的 AtocmicInteger 之类的)

Go 中的 atomic 包里面的功能和 Java 中的 Atomic 一样,原子操作类,原理也是 cas, 甚至提供了 cas 的 api 函数,这里不做过多讲解,

简单举一个代码示例,因为方法太多,详细的请参考 api 文档中的 atomic 包:

1
2
3
4
5
6
7
8
9
package main

import "sync/atomic"

func main() {
//简单举例
var num int64 = 20
atomic.AddInt64(&num,1)
}

# 10.Go 中的 WaitGroup (类似于 Java 中的 CountDownLatch)

现在让我们看一个需求,比如我们开启三个并发任务,然后三个并发任务执行处理完毕后我们才让主线程继续往下面走。

这时候肯定不能用睡眠了,因为不知道睡眠多长时间。

这是 Go 中的 sync 包提供了一个 WaitGroup 的工具,他基本上和 Java 中的 CountDownLatch 的功能一致。

接下来让我们看代码示例:

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
package main

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

//获取类似于CountDownLatch的对象
var wait sync.WaitGroup

func main() {
//设置计数器任务为3,当3个任务全部done后,wait.Wait()才会松开阻塞
wait.Add(3)
go myFun1()
go myFun2()
go myFun3()
//阻塞
wait.Wait()
}


func myFun1() {
//计数器减1
defer wait.Done()
//睡眠五秒
time.Sleep(time.Second*5)
fmt.Println("fun1执行完毕")
}

func myFun2() {
//计数器减1
defer wait.Done()
//睡眠五秒
time.Sleep(time.Second*5)
fmt.Println("fun2执行完毕")

}
func myFun3() {
//计数器减1
defer wait.Done()
//睡眠五秒
time.Sleep(time.Second*5)
fmt.Println("fun3执行完毕")
}
码如下:

```go
package main

import (
"fmt"
"sync"
)

//全局变量
var num int

var wait sync.WaitGroup

var lock sync.Mutex
func main() {
wait.Add(5)
go myAdd()
go myAdd()
go myAdd()
go myAdd()
go myAdd()
wait.Wait()
//预期值等于5万,可是因为线程不安全错误,小于5万
fmt.Printf("num = %d\n",num)
}


func myAdd() {
defer wait.Done()
for i:=0 ;i<10000;i++ {
lock.Lock()
num+=1
lock.Unlock()
}
}

# 7.Go 中的读写锁 (类似于 Java 中的 ReentrantReadWriteLock)

读写锁用于读多写少的情况,多个线程并发读不上锁,写的时候才上锁互斥

读写锁示例代码如下:

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
package main

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


//金额
var moneyA = 1000
//读写锁
var rwLock sync.RWMutex;
var wait sync.WaitGroup
//添加金额
func subtractMoney(subMoney int) {
rwLock.Lock()
time.Sleep(3*time.Second)
moneyA-=subMoney
rwLock.Unlock()
}

//查询金额
func getMoney() int {
rwLock.RLock()
result := moneyA
rwLock.RUnlock()
return result;
}


func main() {
wait.Add(2)
//添加查询金额
go func() {
defer wait.Done()
if(getMoney()>200) {
subtractMoney(200)
fmt.Printf("200元扣款成功,剩下:%d元\n",getMoney())
}else {
fmt.Println("余额不足,无法扣款")
}
}()

//添加查询金额
go func() {
defer wait.Done()
if(getMoney()>900) {
subtractMoney(900)
fmt.Printf("900元扣款成功,剩下:%d元\n",getMoney())
}else {
fmt.Println("余额不足,无法扣款")
}
}()
wait.Wait()
fmt.Println(getMoney())
}

# 8.Go 中的并发安全 Map (类似于 CurrentHashMap)

Go 中自己通过 make 创建的 map 不是线程安全的,具体体现在多线程添加值和修改值下会报如下错误:

1
fatal error : concurrent map writes

这个错类似于 java 中多线程读写线程不安全的容器时报的错。

Go 为了解决这个问题,专门给我们提供了一个并发安全的 map,这个并发安全的 map 不用通过 make 创建,拿来即可用,并且他提供了一些不同于普通 map 的操作方法。

参考如下代码示例:

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
package main

import (
"fmt"
"sync"
)

//创建一个sync包下的线程安全map对象
var myConcurrentMap = sync.Map{}
//遍历数据用的
var myRangeMap = sync.Map{}

func main() {
//存储数据
myConcurrentMap.Store(1,"li_ming")
//取出数据
name,ok := myConcurrentMap.Load(1)
if(!ok) {
fmt.Println("不存在")
return
}
//打印值 li_ming
fmt.Println(name)
//该key有值,则ok为true,返回它原来存在的值,不做任何操作;该key无值,则执行添加操作,ok为false,返回新添加的值
name2, ok2 := myConcurrentMap.LoadOrStore(1,"xiao_hong")
//因为key=1存在,所以打印是 li_ming true
fmt.Println(name2,ok2)
name3, ok3 := myConcurrentMap.LoadOrStore(2,"xiao_hong")
//因为key=2不存在,所以打印是 xiao_hong false
fmt.Println(name3,ok3)
//标记删除值
myConcurrentMap.Delete(1)
//取出数据
//name4,ok4 := myConcurrentMap.Load(1)
//if(!ok4) {
// fmt.Println("name4=不存在")
// return
//}
//fmt.Println(name4)

//遍历数据
rangeFunc()
}
//遍历
func rangeFunc(){
myRangeMap.Store(1,"xiao_ming")
myRangeMap.Store(2,"xiao_li")
myRangeMap.Store(3,"xiao_ke")
myRangeMap.Store(4,"xiao_lei")

myRangeMap.Range(func(k, v interface{}) bool {
fmt.Println("data_key_value = :",k,v)
//return true代表继续遍历下一个,return false代表结束遍历操作
return true
})

}

# 9.Go 中的 AtomicXXX 原子操作类 (类似于 Java 中的 AtocmicInteger 之类的)

Go 中的 atomic 包里面的功能和 Java 中的 Atomic 一样,原子操作类,原理也是 cas, 甚至提供了 cas 的 api 函数,这里不做过多讲解,

简单举一个代码示例,因为方法太多,详细的请参考 api 文档中的 atomic 包:

1
2
3
4
5
6
7
8
9
package main

import "sync/atomic"

func main() {
//简单举例
var num int64 = 20
atomic.AddInt64(&num,1)
}

# 10.Go 中的 WaitGroup (类似于 Java 中的 CountDownLatch)

现在让我们看一个需求,比如我们开启三个并发任务,然后三个并发任务执行处理完毕后我们才让主线程继续往下面走。

这时候肯定不能用睡眠了,因为不知道睡眠多长时间。

这是 Go 中的 sync 包提供了一个 WaitGroup 的工具,他基本上和 Java 中的 CountDownLatch 的功能一致。

接下来让我们看代码示例:

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
package main

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

//获取类似于CountDownLatch的对象
var wait sync.WaitGroup

func main() {
//设置计数器任务为3,当3个任务全部done后,wait.Wait()才会松开阻塞
wait.Add(3)
go myFun1()
go myFun2()
go myFun3()
//阻塞
wait.Wait()
}


func myFun1() {
//计数器减1
defer wait.Done()
//睡眠五秒
time.Sleep(time.Second*5)
fmt.Println("fun1执行完毕")
}

func myFun2() {
//计数器减1
defer wait.Done()
//睡眠五秒
time.Sleep(time.Second*5)
fmt.Println("fun2执行完毕")

}
func myFun3() {
//计数器减1
defer wait.Done()
//睡眠五秒
time.Sleep(time.Second*5)
fmt.Println("fun3执行完毕")
}

# 关于我

Brath 是一个热爱技术的 Java 程序猿,公众号「InterviewCoder」定期分享有趣有料的精品原创文章!

InterviewCoder

非常感谢各位人才能看到这里,原创不易,文章如果有帮助可以关注、点赞、分享或评论,这都是对我的莫大支持!

docker简单部署rocketMQ

InterviewCoder

# docker 简单部署 rocketMQ

1. 创建 namesrv 服务
拉取镜像

1
docker pull rocketmqinc/rocketmq

# 创建 namesrv 数据存储路径

1
mkdir -p  /docker/rocketmq/data/namesrv/logs   /docker/rocketmq/data/namesrv/store

# 构建 namesrv 容器

1
2
3
4
5
6
7
8
9
docker run -d \
--restart=always \
--name rmqnamesrv \
-p 9876:9876 \
-v /docker/rocketmq/data/namesrv/logs:/root/logs \
-v /docker/rocketmq/data/namesrv/store:/root/store \
-e "MAX_POSSIBLE_HEAP=100000000" \
rocketmqinc/rocketmq \
sh mqnamesrv

# 2. 创建 broker 节点

# 创建 broker 数据存储路径

1
mkdir -p  /docker/rocketmq/data/broker/logs   /docker/rocketmq/data/broker/store /docker/rocketmq/conf

# 创建配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
vi /docker/rocketmq/conf/broker.conf
# 所属集群名称,如果节点较多可以配置多个
brokerClusterName = DefaultCluster
#broker名称,master和slave使用相同的名称,表明他们的主从关系
brokerName = broker-a
#0表示Master,大于0表示不同的slave
brokerId = 0
#表示几点做消息删除动作,默认是凌晨4点
deleteWhen = 04
#在磁盘上保留消息的时长,单位是小时
fileReservedTime = 48
#有三个值:SYNC_MASTER,ASYNC_MASTER,SLAVE;同步和异步表示Master和Slave之间同步数据的机制;
brokerRole = ASYNC_MASTER
#刷盘策略,取值为:ASYNC_FLUSH,SYNC_FLUSH表示同步刷盘和异步刷盘;SYNC_FLUSH消息写入磁盘后才返回成功状态,ASYNC_FLUSH不需要;
flushDiskType = ASYNC_FLUSH
# 磁盘使用达到95%之后,生产者再写入消息会报错 CODE: 14 DESC: service not available now, maybe disk full
diskMaxUsedSpaceRatio=95

namesrvAddr=【IP】:9876
brokerIP1 = 【IP】

# 构建 broker 容器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
docker run -d  \
--restart=always \
--name rmqbroker \
--link rmqnamesrv:namesrv \
-p 10911:10911 \
-p 10912:10912 \
-p 10909:10909 \
-v /docker/rocketmq/data/broker/logs:/root/logs \
-v /docker/rocketmq/data/broker/store:/root/store \
-v /docker/rocketmq/conf/broker.conf:/opt/rocketmq-4.4.0/conf/broker.conf \
-e "NAMESRV_ADDR= 【你的IP地址】:9876" \
-e "MAX_POSSIBLE_HEAP=200000000" \
rocketmqinc/rocketmq \
sh mqbroker -c /opt/rocketmq-4.4.0/conf/broker.conf


# 3. 创建 rockermq-console 服务

# 拉取镜像

1
docker pull pangliang/rocketmq-console-ng

# 构建 rockermq-console 容器

1
2
3
4
5
6
7
8
docker run -d \
--restart=always \
--name rmqadmin \
-e "JAVA_OPTS=-Drocketmq.namesrv.addr=【你的IP地址】:9876 \
-Dcom.rocketmq.sendMessageWithVIPChannel=false" \
-p 9877:8080 \
pangliang/rocketmq-console-ng

# 关闭防火墙

1
systemctl stop firewalld.service

# 开放指定端口

1
2
3
4
5
6
firewall-cmd --permanent --zone=public --add-port=9876/tcp
firewall-cmd --permanent --zone=public --add-port=10911/tcp
firewall-cmd --permanent --zone=public --add-port=10912/tcp
# 立即生效
firewall-cmd --reload

# 访问控制台

网页访问 http://【IP】:9999 / 查看控制台信息

在这里插入图片描述

# 关于我

Brath 是一个热爱技术的 Java 程序猿,公众号「InterviewCoder」定期分享有趣有料的精品原创文章!

InterviewCoder

非常感谢各位人才能看到这里,原创不易,文章如果有帮助可以关注、点赞、分享或评论,这都是对我的莫大支持!

Linux服务器下部署vue 2.0项目

InterviewCoder

# 一、安装 VUE 环境

1.wget 指定进行下载,下载后文件默认存在根目录下的 root 包下面;

1
wget https://nodejs.org/dist/v16.14.0/node-v16.14.0-linux-x64.tar.xz

解压缩

1
2
3
xz -d node-v16.14.0-linux-x64.tar.xz  #将xz格式解压为tar

tar -xvf node-v16.14.0-linux-x64.tar #将tar解压为文件

重命名文件夹

1
2
mv node-v16.14.0-linux-x64 nodeJS-V16.14.0

配置 node.js 环境变量

1
vim /etc/profile

进入编辑,在文件最下面加上如下字段

1
2
export NODEJS_HOME=/root/nodeJS-V16.14.0
export PATH=$NODEJS_HOME/bin:$PATH

个人比较喜欢用 Xftp 将 profile 文件传输到 windows 系统下进行配置后再覆盖回去

配置完成后使用 source 命令重新执行刚修改的初始化文件,使之立即生效,而不必注销并重新登录

1
source /etc/profile

检查环境变量是否配置成功

1
node -v

安装 WebPackage

1
npm install webpack -g --registry=https://registry.npm.taobao.org

# 二、Nginx 环境搭建配置

wget 指定进行下载,下载后文件默认存在根目录下的 root 下面;

1
1.wget https://nginx.org/download/nginx-1.21.6.tar.gz

解压缩

1
tar -zxvf nginx-1.21.6.tar.gz

进入 nginx 文件夹

1
cd /root/nginx-1.21.6

检查配置

1
./configure

安装 gcc 环境,有就不需要安装了

1
yum install gcc-c++

安装 PCRE 依赖库

1
yum install -y pcre pcre-devel

安装 zlib 依赖库

1
yum install -y zlib zlib-devel

安装 OpenSSL 安全套接字层密码库

1
yum install -y openssl openssl-devel

再次执行配置检查命令

1
2
./configure

编译安装 nginx

1
make install

查找默认安装路径

1
whereis nginx

配置 nginx 环境变量

1
vim /etc/profile

进入编辑,在文件最下面加上如下字段

1
export PATH=$PATH:/usr/local/nginx/sbin

个人比较喜欢用 Xftp 将 profile 文件传输到 windows 系统下进行配置后再覆盖回去

初始化配置

1
source /etc/profile

检查环境变量是否配置成功

1
nginx -v

启动 nginx

1
nginx

查看 nginx 是否启动,远程访问服务器,跳出 nginx 欢迎界面就算配置成功了!

nginx 常用命令

1
2
3
4
5
6
7
启动服务:nginx
退出服务:nginx -s quit
强制关闭服务:nginx -s stop
重载服务:nginx -s reload  (重载服务配置文件,类似于重启,但服务不会中止)
验证配置文件:nginx -t
使用配置文件:nginx -c "配置文件路径"
使用帮助:nginx -h

# 三、发布 VUE 项目

# 打包 VUE 项目

1. 首先配置好线上环境的路径:prod.env

image-20220428084247115

1
2
3
4
5
module.exports = {
NODE_ENV: '"production"',
ENV_CONFIG: '"prod"',
MANAGEMENT_SERVICE_API: '"http://42.193.125.92:7815"',
}

2. 控制台输入 npm run build:prod

image-20220428084349346

3. 打包完会生成一个 dist 文件夹

image-20220428084359625

4. 将 dist 文件夹内容覆盖到 /usr/local/nginx/html 目录下面

image-20220428084414353

5. 修改 nginx 配置

1
/usr/local/nginx/conf/nginx.conf 编辑打开
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
worker_processes  1;

events {
worker_connections 1024;
}


http {
include mime.types;
default_type application/octet-stream;

sendfile on;

keepalive_timeout 65;

server {
listen 8099; #配置当前服务端口
server_name localhost; #配置当前服务IP

location / {
root html/dist; #配置服务根目录
index index.html index.htm; #配置服务索引页面
}

error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
}
}

6. 重启或关闭重开 Nginx

1
2
3
4
5
6
7
nginx -s reload #重启


nginx -s stop #强制停止

nginx #开启

# 关于我

Brath 是一个热爱技术的 Java 程序猿,公众号「InterviewCoder」定期分享有趣有料的精品原创文章!

InterviewCoder

非常感谢各位人才能看到这里,原创不易,文章如果有帮助可以关注、点赞、分享或评论,这都是对我的莫大支持!

Redis与Mysql | Master与Slave同步:canal教学

InterviewCoder

# 前言:

​ 作者最近在做自己的项目,使用到 Redis,需要热更新,修改 Mysql 后同步 Redis 缓存,出于对圈子的贡献,也较于当前的 canal 的博客大多数不是很详细,所以写下这篇文章,时间是 2022 年 6 月 29 日。目的是帮助更多的人,希望能为在祖国的经济发展作出小小的贡献。

​ end

# 学习 Canal 基本需要:

​ Linux 服务器,性能无大要求

​ Java 基础

​ Mysql,Redis 基础

# 俗话说,要了解一个东西,先了解他的由来:

# 一、Canal 起源

​ 阿里巴巴因为业务特性,买家集中在国外,衍生出了杭州美国异地数据同步需求,从 2010 年开始,阿里巴巴开始开发 canal,canal 是基于 Java 开发的数据库增量日志解析,提供增量数据订阅 & 消费的中间件。Canal 主要支持了 Mysql 和 Bilog 解析,解析完成后利用 canal Client 来处理获取相关数据。

了解完 canal 的起源,再来看看 canal 的核心业务依赖,也就是 mysql 的二进制日志:binary_log 简称:Binlog

# 二、Binlog

​ binlog 指二进制日志,它记录了数据库上的所有改变,并以二进制的形式保存在磁盘中,它可以用来查看数据库的变更历史、数据库增量备份和恢复、MySQL 的复制(主从数据库的复制)。

# binlog 有三种格式:

statement:基于 SQL 语句的复制(statement-based replication,SBR)
row:基于行的复制(row-based replication,RBR)
mixed:混合模式复制(mixed-based replication,MBR)

# statement:语句级别

每一条会修改数据的 sql 都会记录在 binlog 中。

​ 优点:不需要记录每一行的变化,减少了 binlog 日志量,节约了 IO,提高性能。但是注意 statement 相比于 row 能节约多少性能与日志量,取决于应用的 SQL 情况。正常同一条记录修改或者插入 row 格式所产生的日志量还小于 Statement 产生的日志量,但是考虑到如果带条件的 update 操作,以及整表删除,alter 表等操作,ROW 格式会产生大量日志,因此在考虑是否使用 ROW 格式日志时应该跟据应用的实际情况,其所产生的日志量会增加多少,以及带来的 IO 性能问题。

​ 缺点:由于记录的只是执行语句,为了这些语句在 slave 上正确运行,我们还必须记录每条语句在执行时候的一些相关信息,以保证所有语句能在 slave 得到和在 master 端执行时相同的结果。另外,一些特定的函数功能如果要在 slave 和 master 上保持一致会有很多相关问题。

# row:行数据级别

5.1.5 版本的 MySQL 才开始支持 row level 的复制,它不记录 sql 语句上下文相关信息,仅保存哪条记录被修改。

​ 优点:binlog 中可以不记录执行的 sql 语句的上下文相关的信息,仅需要记录那一条记录被修改成什么了。所以 row level 的日志会非常清楚的记下每一行数据修改的细节。而且不会出现某些特定情况下的存储过程,或 function,以及 trigger 的调用和触发无法被正确复制的问题。

​ 缺点:所有的执行的语句当记录到日志中的时候,都将以每行记录的修改来记录,这样可能会产生大量的日志内容。但是新版本的 MySQL 对 row level 模式进行了优化,并不是所有的修改都会以 row level 来记录,像遇到表结构变更的时候就会以 statement 模式来记录,如果 sql 语句确实就是 update 或者 delete 等修改数据的语句,那么还是会记录所有行的变更。

# mixed:混合级别

从 5.1.8 版本开始,MySQL 提供了 Mixed 格式,实际上就是 Statement 与 Row 的结合。

​ 在 Mixed 模式下,一般的语句修改使用 statment 格式保存 binlog,如果一些函数,statement 无法完成主从复制的操作,则采用 row 格式保存 binlog,MySQL 会根据执行的每一条具体的 sql 语句来区分对待记录的日志形式,也就是在 Statement 和 Row 之间选择一种。

# 由于 statement 和 mixed 的特殊性,通过 sql 来备份,总会有数据不一致的情况,比如:now () 函数。
# 所以绝大多数场景下使用 Row 级别,也就是行级别,这样保证我们备份的数据和出口的数据相一致。

# 三、下载和安装 Canal 工具

下载前,在 mysql 创建 canal 用户,因为 canal 服务端需要连接 mysql 数据库

1
2
3
4
5
-- 使用命令登录:mysql -u root -p
-- 创建用户 用户名:canal 密码:Canal@123456
create user 'canal'@'%' identified by 'Canal@123456';
-- 授权 *.*表示所有库
grant SELECT, REPLICATION SLAVE, REPLICATION CLIENT on *.* to 'canal'@'%' identified by 'Canal@123456';

# 改了配置文件之后,重启 MySQL,使用命令查看是否打开 binlog 模式:

在这里插入图片描述

# 查看 binlog 日志文件列表:

在这里插入图片描述

# 点此下载 Canal👇

https://ghproxy.com/https://github.com/alibaba/canal/releases/download/canal-1.1.2/canal.deployer-1.1.2.tar.gz

此链接为 github 代理提供连接,仅供参考,此处无广告意义。

image-20220629152616754

下载好后上传至 linux 服务器,创建 canal 文件夹并解压到 canal 文件夹中

image-20220629153150741

完成后会得到以上四个核心文件:bin,conf,lib,logs

需要修改一处配置文件:

​ /canal/conf/example 下的 instance.properties

image-20220629153511220

修改完成后保存退出

接下来进入 bin 目录 sh startUp.sh 启动 canal 服务端

# 至此服务端的操作基本完成

Java 客户端操作
首先引入 maven 依赖:

1
2
3
4
5
<dependency>
<groupId>com.alibaba.otter</groupId>
<artifactId>canal.client</artifactId>
<version>1.1.2</version>
</dependency>

然后创建一个 canal 项目,使用 SpringBoot 构建,如图所示,创建 canal 包:

image-20220629153956493

canal 工具类,仅供参考

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
package cn.brath.canal;
import java.awt.print.Printable;
import java.time.LocalDateTime;

import cn.brath.common.redis.service.TokenService;
import cn.brath.common.redis.util.RedisKeys;
import cn.brath.common.utils.AssertUtil;
import cn.brath.common.utils.UserTokenManager;
import cn.brath.entity.IvUser;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.otter.canal.client.CanalConnector;
import com.alibaba.otter.canal.client.CanalConnectors;
import com.alibaba.otter.canal.protocol.CanalEntry.*;
import com.alibaba.otter.canal.protocol.Message;
import com.google.protobuf.InvalidProtocolBufferException;
import lombok.Data;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.print.attribute.standard.MediaPrintableArea;
import java.net.InetSocketAddress;
import java.time.ZoneId;
import java.util.List;

@Component
@Data
public class CanalClient {

/**
* SLF4J日志
*/
private static Logger logger = LoggerFactory.getLogger(CanalClient.class);

private String host = "***.***.***.***";

private String port = "11111";

private String destination = "example";

/**
* 用户令牌业务接口
*/
private static TokenService tokenService;

@Autowired
public void TokenServiceIn(TokenService tokenService) {
CanalClient.tokenService = tokenService;
}

/**
* canal启动方法
*/
public void run() {
if (!AssertUtil.isEmptys(host, port, destination)) {
logger.error("canal客户端连接失败,当前服务端host:{},port:{},destination:{}", host, port, destination);
return;
}
CanalConnector connector = CanalConnectors.newSingleConnector(
new InetSocketAddress(host, Integer.valueOf(port)), destination, "", ""
);
int batchSize = 1000;
try {
//建立连接
connector.connect();
//目标为全部表
connector.subscribe(".*\\..*");
connector.rollback();
logger.info("canal客户端连接完成,当前服务端host:{},port:{},destination:{}", host, port, destination);
try {
while (true) {
//尝试从master那边拉去数据batchSize条记录,有多少取多少
Message message = connector.getWithoutAck(batchSize);
long batchId = message.getId();
int size = message.getEntries().size();
if (batchId == -1 || size == 0) {
Thread.sleep(1000);
} else {
logger.info("同步任务进行中,检测到修改数据,执行同步Redis");
dataHandle(message.getEntries());
}
connector.ack(batchId);
}
} catch (InterruptedException e) {
e.printStackTrace();
} catch (InvalidProtocolBufferException e) {
e.printStackTrace();
}
} finally {
connector.disconnect();
}
}

/**
* 数据处理
*
* @param entrys
*/
private void dataHandle(List<Entry> entrys) throws InvalidProtocolBufferException {
JSONObject beforeData = null;
JSONObject afterData = null;
for (Entry entry : entrys) {
if (EntryType.ROWDATA.equals(entry.getEntryType())) {
//反序列化rowdata
RowChange rowChange = RowChange.parseFrom(entry.getStoreValue());
//获取数据集
List<RowData> rowDataList = rowChange.getRowDatasList();
//获取数据遍历
for (RowData rowData : rowDataList) {
afterData = new JSONObject();
List<Column> afterColumnsList = rowData.getAfterColumnsList();
for (Column column : afterColumnsList) {
afterData.put(column.getName(), column.getValue());
}
}

//因为作者这里只做同步Redis,不考虑到操作类型,只需要覆盖相同键值数据

//写入Redis
executeRedisWarehousing(afterData);
}
}
}

/**
* 执行Redis用户数据入库
*
* @param afterData
*/
public static void executeRedisWarehousing(JSONObject afterData) {
logger.info("开始执行Redis热更新入库同步Mysql -- ");

do...

logger.info("入库完成");
}

}

# 启动类使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@SpringBootApplication
@Slf4j
public class Application {
public static void main(String[] args) {
SpringApplication.run(InterviewUserServiceApplication.class, args);
//项目启动,执行canal客户端监听
try {
new CanalClient().run();
} catch (Exception e) {
e.printStackTrace();
log.error(" canal客户端监听 启动失败,原因可能是:{}", e.getMessage());
}
}
}

接下来启动项目运行,成功连接 canal 后我们尝试修改一个 mysql 的数据,发现在客户端成功完成了与 Redis 的同步操作

image-20220629154454409

# 相关异常:

Canal 异常:

dump address /124.222.106.122:3306 has an error, retrying. caused by java.la

解决办法:重启 Mysql,删除 example 下的 dat 后缀文件后重启 canal

其他:

​ 是否开放端口 11111

​ mysql 是否连接成功,查看 logs/example/example.log

​ 服务端与客户端是否连接成功,查看当前项目日志即可

# 关于我

Brath 是一个热爱技术的 Java 程序猿,公众号「InterviewCoder」定期分享有趣有料的精品原创文章!

InterviewCoder

非常感谢各位人才能看到这里,原创不易,文章如果有帮助可以关注、点赞、分享或评论,这都是对我的莫大支持!

【ELK】使用Docker搭建ELK

InterviewCoder

# 文章目录

# 概念:

那么,ELK 到底是什么呢? “ELK” 是三个开源项目的首字母缩写,这三个项目分别是:Elasticsearch、Logstash 和 Kibana。Elasticsearch 是一个搜索和分析引擎。Logstash 是服务器端数据处理管道,能够同时从多个来源采集数据,转换数据,然后将数据发送到诸如 Elasticsearch 等 “存储库” 中。Kibana 则可以让用户在 Elasticsearch 中使用图形和图表对数据进行可视化

  • 工作流程
    在这里插入图片描述
  • 在后续 elk 引入了 beats (数据采集器) 后被称为 Elastic Stack 或者 ELK

# 安装 elk (这里通过 docker 进行安装)

# 安装 es

  • 在 dockerhub 上搜索 es在这里插入图片描述
  • 找到需要的 es 版本
    在这里插入图片描述
  • 拉取 es 镜像 docker pull elasticsearch:tag
    在这里插入图片描述
    在这里插入图片描述
  • 在 dockerhub 官网上可以看到 es 的启动命令
    在这里插入图片描述
  • 先创建自定义 docker 网络 docker network create elastic ,默认是桥接模式
    在这里插入图片描述
  • 查看创建的网络在这里插入图片描述
  • 启动 es 镜像,这里我以单机的形式启动 docker run -d --name elasticsearch --net elastic -p 9200:9200 -p 9300:9300 -e "discovery.type=single-node" elasticsearch:tag
    在这里插入图片描述
  • 启动之后访问 localhost:9200 ,有数据返回说明启动成功,如下图
    在这里插入图片描述
  • 修改 es 配置,进入容器 docker exec -it a804 /bin/sh
    在这里插入图片描述
  • config 目录下的 elasticsearch.yml 文件添加
1
2
3
http.cors.enabled: true 
http.cors.allow-origin: "*"
12
  • 修改完配置之后,退出容器并重启
    在这里插入图片描述

# 安装 kikana

  • 从 dockerhub 拉取与 es 对应版本的 kibana docker pull kibana:tag
    在这里插入图片描述
  • 启动 kibana docker run --name kib-7.6 --net elastic -d -p 5601:5601 kibana:tag
  • 启动之后访问
    在这里插入图片描述
  • 出现上图是由于 kibanakibana.yml,默认的地址是 http://elasticsearch:9200, 需要修改为 es 服务 ip
  • 进入到 es 容器里面 docker -it 容器编号 /bin/sh
    在这里插入图片描述
  • 查看 es 的容器详情 docker inspect a80402dbe9f5
  • 找到网络详情,找到 es 服务的 ip 地址
    在这里插入图片描述
  • 也可以通过 docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' a804 获取 ip
    在这里插入图片描述
  • 进入到 kibana 容器,切换到 /usr/share/kibana/config 目录
    在这里插入图片描述
  • 修改 kibana.yml 文件
    在这里插入图片描述
    在这里插入图片描述
  • 修改完 kibana.yml 之后重启 kibana 容器
  • 访问 kibana localhost:5601
    在这里插入图片描述
    在这里插入图片描述
  • 到这里 kibana 就安装成功了

# 安装 logstash

  • 从 dockerhub 拉取 logstash docker pull logstash:7.6.2
    在这里插入图片描述

# 关于我

Brath 是一个热爱技术的 Java 程序猿,公众号「InterviewCoder」定期分享有趣有料的精品原创文章!

InterviewCoder

非常感谢各位人才能看到这里,原创不易,文章如果有帮助可以关注、点赞、分享或评论,这都是对我的莫大支持!

SpringBoot+Redis实现高性能,强支持的点赞-查看收藏流程设计

InterviewCoder

# SpringBoot+Redis 实现高性能,强支持的点赞流程设计

# 前言

本文基于 SpringCloudAlibaba, 用户发起点赞、取消点赞,后先存入 Redis 中,再每隔两小时从 Redis 读取点赞数据写入数据库中做持久化存储。

点赞功能在很多系统中都有,但别看功能小,想要做好需要考虑的东西还挺多的。

点赞、取消点赞是高频次的操作,若每次都读写数据库,大量的操作会影响数据库性能,所以需要做缓存。

至于多久从 Redis 取一次数据存到数据库中,根据项目的实际情况定吧,我是暂时设了两个小时。

项目需求需要查看都谁点赞了,所以要存储每个点赞的点赞人、被点赞人,不能简单的做计数。

# 文章分四部分介绍:

  • Redis 缓存设计及实现

  • 数据库设计

  • 数据库操作

  • 开启定时任务持久化存储到数据库

# 一、Redis 缓存设计及实现

# 1.1 Redis 安装及运行

Redis 安装请自行查阅相关教程。

说下 Docker 安装运行 Redis

1
docker run -d -p 6379:6379 redis:4.0.8

如果已经安装了 Redis,打开命令行,输入启动 Redis 的命令

1
redis-server

# 1.2 Redis 与 SpringBoot 项目的整合

  1. pom.xml 中引入依赖
1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
  1. 在启动类上添加注释 @EnableCaching
1
2
3
4
5
6
7
8
9
10
11
@SpringBootApplication
@EnableDiscoveryClient
@EnableSwagger2
@EnableFeignClients(basePackages = "com.solo.coderiver.project.client")
@EnableCaching
public class UserApplication {

public static void main(String[] args) {
SpringApplication.run(UserApplication.class, args);
}
}
  1. 编写 Redis 配置类 RedisConfig
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
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;

import java.net.UnknownHostException;


@Configuration
public class RedisConfig {

@Bean
@ConditionalOnMissingBean(name = "redisTemplate")
public RedisTemplate<String, Object> redisTemplate(
RedisConnectionFactory redisConnectionFactory)
throws UnknownHostException {

Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<Object>(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);

RedisTemplate<String, Object> template = new RedisTemplate<String, Object>();
template.setConnectionFactory(redisConnectionFactory);
template.setKeySerializer(jackson2JsonRedisSerializer);
template.setValueSerializer(jackson2JsonRedisSerializer);
template.setHashKeySerializer(jackson2JsonRedisSerializer);
template.setHashValueSerializer(jackson2JsonRedisSerializer);
template.afterPropertiesSet();
return template;
}


@Bean
@ConditionalOnMissingBean(StringRedisTemplate.class)
public StringRedisTemplate stringRedisTemplate(
RedisConnectionFactory redisConnectionFactory)
throws UnknownHostException {
StringRedisTemplate template = new StringRedisTemplate();
template.setConnectionFactory(redisConnectionFactory);
return template;
}
}

至此 Redis 在 SpringBoot 项目中的配置已经完成,可以愉快的使用了。

# 1.3 Redis 的数据结构类型

Redis 可以存储键与 5 种不同数据结构类型之间的映射,这 5 种数据结构类型分别为 String(字符串)、List(列表)、Set(集合)、Hash(散列)和 Zset(有序集合)。

下面来对这 5 种数据结构类型作简单的介绍:

结构类型 结构存储的值 结构的读写能力
String 可以是字符串、整数或者浮点数 对整个字符串或者字符串的其中一部分执行操作;对象和浮点数执行自增 (increment) 或者自减 (decrement)
List 一个链表,链表上的每个节点都包含了一个字符串 从链表的两端推入或者弹出元素;根据偏移量对链表进行修剪 (trim);读取单个或者多个元素;根据值来查找或者移除元素
Set 包含字符串的无序收集器 (unorderedcollection),并且被包含的每个字符串都是独一无二的、各不相同 添加、获取、移除单个元素;检查一个元素是否存在于某个集合中;计算交集、并集、差集;从集合里卖弄随机获取元素
Hash 包含键值对的无序散列表 添加、获取、移除单个键值对;获取所有键值对
Zset 字符串成员 (member) 与浮点数分值 (score) 之间的有序映射,元素的排列顺序由分值的大小决定 添加、获取、删除单个元素;根据分值范围 (range) 或者成员来获取元素

# 1.4 点赞数据在 Redis 中的存储格式

用 Redis 存储两种数据,一种是记录点赞人、被点赞人、点赞状态的数据,另一种是每个用户被点赞了多少次,做个简单的计数。

由于需要记录点赞人和被点赞人,还有点赞状态(点赞、取消点赞),还要固定时间间隔取出 Redis 中所有点赞数据,分析了下 Redis 数据格式中 Hash 最合适。

因为 Hash 里的数据都是存在一个键里,可以通过这个键很方便的把所有的点赞数据都取出。这个键里面的数据还可以存成键值对的形式,方便存入点赞人、被点赞人和点赞状态。

设点赞人的 id 为 likedPostId ,被点赞人的 id 为 likedUserId ,点赞时状态为 1,取消点赞状态为 0。将点赞人 id 和被点赞人 id 作为键,两个 id 中间用 :: 隔开,点赞状态作为值。

所以如果用户点赞,存储的键为: likedUserId::likedPostId ,对应的值为 1 。

取消点赞,存储的键为: likedUserId::likedPostId ,对应的值为 0 。

取数据时把键用 :: 切开就得到了两个 id,也很方便。

# 1.5 操作 Redis

Redis 各种数据格式的操作方法可以看看 这篇文章 ,写的非常好。

将具体操作方法封装到了 RedisService 接口里

RedisService.java

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
import com.solo.coderiver.user.dataobject.UserLike;
import com.solo.coderiver.user.dto.LikedCountDTO;

import java.util.List;

public interface RedisService {

/**
* 点赞。状态为1
* @param likedUserId
* @param likedPostId
*/
void saveLiked2Redis(String likedUserId, String likedPostId);

/**
* 取消点赞。将状态改变为0
* @param likedUserId
* @param likedPostId
*/
void unlikeFromRedis(String likedUserId, String likedPostId);

/**
* 从Redis中删除一条点赞数据
* @param likedUserId
* @param likedPostId
*/
void deleteLikedFromRedis(String likedUserId, String likedPostId);

/**
* 该用户的点赞数加1
* @param likedUserId
*/
void incrementLikedCount(String likedUserId);

/**
* 该用户的点赞数减1
* @param likedUserId
*/
void decrementLikedCount(String likedUserId);

/**
* 获取Redis中存储的所有点赞数据
* @return
*/
List<UserLike> getLikedDataFromRedis();

/**
* 获取Redis中存储的所有点赞数量
* @return
*/
List<LikedCountDTO> getLikedCountFromRedis();

}

实现类 RedisServiceImpl.java

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
import com.solo.coderiver.user.dataobject.UserLike;
import com.solo.coderiver.user.dto.LikedCountDTO;
import com.solo.coderiver.user.enums.LikedStatusEnum;
import com.solo.coderiver.user.service.LikedService;
import com.solo.coderiver.user.service.RedisService;
import com.solo.coderiver.user.utils.RedisKeyUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.Cursor;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ScanOptions;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;

@Service
@Slf4j
public class RedisServiceImpl implements RedisService {

@Autowired
RedisTemplate redisTemplate;

@Autowired
LikedService likedService;

@Override
public void saveLiked2Redis(String likedUserId, String likedPostId) {
String key = RedisKeyUtils.getLikedKey(likedUserId, likedPostId);
redisTemplate.opsForHash().put(RedisKeyUtils.MAP_KEY_USER_LIKED, key, LikedStatusEnum.LIKE.getCode());
}

@Override
public void unlikeFromRedis(String likedUserId, String likedPostId) {
String key = RedisKeyUtils.getLikedKey(likedUserId, likedPostId);
redisTemplate.opsForHash().put(RedisKeyUtils.MAP_KEY_USER_LIKED, key, LikedStatusEnum.UNLIKE.getCode());
}

@Override
public void deleteLikedFromRedis(String likedUserId, String likedPostId) {
String key = RedisKeyUtils.getLikedKey(likedUserId, likedPostId);
redisTemplate.opsForHash().delete(RedisKeyUtils.MAP_KEY_USER_LIKED, key);
}

@Override
public void incrementLikedCount(String likedUserId) {
redisTemplate.opsForHash().increment(RedisKeyUtils.MAP_KEY_USER_LIKED_COUNT, likedUserId, 1);
}

@Override
public void decrementLikedCount(String likedUserId) {
redisTemplate.opsForHash().increment(RedisKeyUtils.MAP_KEY_USER_LIKED_COUNT, likedUserId, -1);
}

@Override
public List<UserLike> getLikedDataFromRedis() {
Cursor<Map.Entry<Object, Object>> cursor = redisTemplate.opsForHash().scan(RedisKeyUtils.MAP_KEY_USER_LIKED, ScanOptions.NONE);
List<UserLike> list = new ArrayList<>();
while (cursor.hasNext()){
Map.Entry<Object, Object> entry = cursor.next();
String key = (String) entry.getKey();
//分离出 likedUserId,likedPostId
String[] split = key.split("::");
String likedUserId = split[0];
String likedPostId = split[1];
Integer value = (Integer) entry.getValue();

//组装成 UserLike 对象
UserLike userLike = new UserLike(likedUserId, likedPostId, value);
list.add(userLike);

//存到 list 后从 Redis 中删除
redisTemplate.opsForHash().delete(RedisKeyUtils.MAP_KEY_USER_LIKED, key);
}

return list;
}

@Override
public List<LikedCountDTO> getLikedCountFromRedis() {
Cursor<Map.Entry<Object, Object>> cursor = redisTemplate.opsForHash().scan(RedisKeyUtils.MAP_KEY_USER_LIKED_COUNT, ScanOptions.NONE);
List<LikedCountDTO> list = new ArrayList<>();
while (cursor.hasNext()){
Map.Entry<Object, Object> map = cursor.next();
//将点赞数量存储在 LikedCountDT
String key = (String)map.getKey();
LikedCountDTO dto = new LikedCountDTO(key, (Integer) map.getValue());
list.add(dto);
//从Redis中删除这条记录
redisTemplate.opsForHash().delete(RedisKeyUtils.MAP_KEY_USER_LIKED_COUNT, key);
}
return list;
}
}

用到的工具类和枚举类

RedisKeyUtils, 用于根据一定规则生成 key

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class RedisKeyUtils {

//保存用户点赞数据的key
public static final String MAP_KEY_USER_LIKED = "MAP_USER_LIKED";
//保存用户被点赞数量的key
public static final String MAP_KEY_USER_LIKED_COUNT = "MAP_USER_LIKED_COUNT";

/**
* 拼接被点赞的用户id和点赞的人的id作为key。格式 222222::333333
* @param likedUserId 被点赞的人id
* @param likedPostId 点赞的人的id
* @return
*/
public static String getLikedKey(String likedUserId, String likedPostId){
StringBuilder builder = new StringBuilder();
builder.append(likedUserId);
builder.append("::");
builder.append(likedPostId);
return builder.toString();
}
}

LikedStatusEnum 用户点赞状态的枚举类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.solo.coderiver.user.enums;

import lombok.Getter;

/**
* 用户点赞的状态
*/
@Getter
public enum LikedStatusEnum {
LIKE(1, "点赞"),
UNLIKE(0, "取消点赞/未点赞"),
;

private Integer code;

private String msg;

LikedStatusEnum(Integer code, String msg) {
this.code = code;
this.msg = msg;
}
}

# 二、数据库设计

数据库表中至少要包含三个字段:被点赞用户 id,点赞用户 id,点赞状态。再加上主键 id,创建时间,修改时间就行了。

建表语句

1
2
3
4
5
6
7
8
9
10
11
create table `user_like`(
`id` int not null auto_increment,
`liked_user_id` varchar(32) not null comment '被点赞的用户id',
`liked_post_id` varchar(32) not null comment '点赞的用户id',
`status` tinyint(1) default '1' comment '点赞状态,0取消,1点赞',
`create_time` timestamp not null default current_timestamp comment '创建时间',
`update_time` timestamp not null default current_timestamp on update current_timestamp comment '修改时间',
primary key(`id`),
INDEX `liked_user_id`(`liked_user_id`),
INDEX `liked_post_id`(`liked_post_id`)
) comment '用户点赞表';

对应的对象 UserLike

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
import com.solo.coderiver.user.enums.LikedStatusEnum;
import lombok.Data;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

/**
* 用户点赞表
*/
@Entity
@Data
public class UserLike {

//主键id
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;

//被点赞的用户的id
private String likedUserId;

//点赞的用户的id
private String likedPostId;

//点赞的状态.默认未点赞
private Integer status = LikedStatusEnum.UNLIKE.getCode();

public UserLike() {
}

public UserLike(String likedUserId, String likedPostId, Integer status) {
this.likedUserId = likedUserId;
this.likedPostId = likedPostId;
this.status = status;
}
}

# 三、数据库操作

操作数据库同样封装在接口中

LikedService

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
import com.solo.coderiver.user.dataobject.UserLike;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;

import java.util.List;

public interface LikedService {

/**
* 保存点赞记录
* @param userLike
* @return
*/
UserLike save(UserLike userLike);

/**
* 批量保存或修改
* @param list
*/
List<UserLike> saveAll(List<UserLike> list);


/**
* 根据被点赞人的id查询点赞列表(即查询都谁给这个人点赞过)
* @param likedUserId 被点赞人的id
* @param pageable
* @return
*/
Page<UserLike> getLikedListByLikedUserId(String likedUserId, Pageable pageable);

/**
* 根据点赞人的id查询点赞列表(即查询这个人都给谁点赞过)
* @param likedPostId
* @param pageable
* @return
*/
Page<UserLike> getLikedListByLikedPostId(String likedPostId, Pageable pageable);

/**
* 通过被点赞人和点赞人id查询是否存在点赞记录
* @param likedUserId
* @param likedPostId
* @return
*/
UserLike getByLikedUserIdAndLikedPostId(String likedUserId, String likedPostId);

/**
* 将Redis里的点赞数据存入数据库中
*/
void transLikedFromRedis2DB();

/**
* 将Redis中的点赞数量数据存入数据库
*/
void transLikedCountFromRedis2DB();

}

LikedServiceImpl 实现类

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
import com.solo.coderiver.user.dataobject.UserInfo;
import com.solo.coderiver.user.dataobject.UserLike;
import com.solo.coderiver.user.dto.LikedCountDTO;
import com.solo.coderiver.user.enums.LikedStatusEnum;
import com.solo.coderiver.user.repository.UserLikeRepository;
import com.solo.coderiver.user.service.LikedService;
import com.solo.coderiver.user.service.RedisService;
import com.solo.coderiver.user.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

@Service
@Slf4j
public class LikedServiceImpl implements LikedService {

@Autowired
UserLikeRepository likeRepository;

@Autowired
RedisService redisService;

@Autowired
UserService userService;

@Override
@Transactional
public UserLike save(UserLike userLike) {
return likeRepository.save(userLike);
}

@Override
@Transactional
public List<UserLike> saveAll(List<UserLike> list) {
return likeRepository.saveAll(list);
}

@Override
public Page<UserLike> getLikedListByLikedUserId(String likedUserId, Pageable pageable) {
return likeRepository.findByLikedUserIdAndStatus(likedUserId, LikedStatusEnum.LIKE.getCode(), pageable);
}

@Override
public Page<UserLike> getLikedListByLikedPostId(String likedPostId, Pageable pageable) {
return likeRepository.findByLikedPostIdAndStatus(likedPostId, LikedStatusEnum.LIKE.getCode(), pageable);
}

@Override
public UserLike getByLikedUserIdAndLikedPostId(String likedUserId, String likedPostId) {
return likeRepository.findByLikedUserIdAndLikedPostId(likedUserId, likedPostId);
}

@Override
@Transactional
public void transLikedFromRedis2DB() {
List<UserLike> list = redisService.getLikedDataFromRedis();
for (UserLike like : list) {
UserLike ul = getByLikedUserIdAndLikedPostId(like.getLikedUserId(), like.getLikedPostId());
if (ul == null){
//没有记录,直接存入
save(like);
}else{
//有记录,需要更新
ul.setStatus(like.getStatus());
save(ul);
}
}
}

@Override
@Transactional
public void transLikedCountFromRedis2DB() {
List<LikedCountDTO> list = redisService.getLikedCountFromRedis();
for (LikedCountDTO dto : list) {
UserInfo user = userService.findById(dto.getId());
//点赞数量属于无关紧要的操作,出错无需抛异常
if (user != null){
Integer likeNum = user.getLikeNum() + dto.getCount();
user.setLikeNum(likeNum);
//更新点赞数量
userService.updateInfo(user);
}
}
}
}

数据库的操作就这些,主要还是增删改查。

# 四、开启定时任务持久化存储到数据库

定时任务 Quartz 很强大,就用它了。

Quartz 使用步骤:

  1. 添加依赖
1
2
3
4
5
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-quartz</artifactId>
</dependency>
复制代码
  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
package com.solo.coderiver.user.config;

import com.solo.coderiver.user.task.LikeTask;
import org.quartz.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class QuartzConfig {

private static final String LIKE_TASK_IDENTITY = "LikeTaskQuartz";

@Bean
public JobDetail quartzDetail(){
return JobBuilder.newJob(LikeTask.class).withIdentity(LIKE_TASK_IDENTITY).storeDurably().build();
}

@Bean
public Trigger quartzTrigger(){
SimpleScheduleBuilder scheduleBuilder = SimpleScheduleBuilder.simpleSchedule()
// .withIntervalInSeconds(10) //设置时间周期单位秒
.withIntervalInHours(2) //两个小时执行一次
.repeatForever();
return TriggerBuilder.newTrigger().forJob(quartzDetail())
.withIdentity(LIKE_TASK_IDENTITY)
.withSchedule(scheduleBuilder)
.build();
}
}
  1. 编写执行任务的类继承自 QuartzJobBean
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
package com.solo.coderiver.user.task;

import com.solo.coderiver.user.service.LikedService;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.time.DateUtils;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.quartz.QuartzJobBean;

import java.text.SimpleDateFormat;
import java.util.Date;

/**
* 点赞的定时任务
*/
@Slf4j
public class LikeTask extends QuartzJobBean {

@Autowired
LikedService likedService;

private SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

@Override
protected void executeInternal(JobExecutionContext jobExecutionContext) throws JobExecutionException {

log.info("LikeTask-------- {}", sdf.format(new Date()));

//将 Redis 里的点赞信息同步到数据库里
likedService.transLikedFromRedis2DB();
likedService.transLikedCountFromRedis2DB();
}
}

在定时任务中直接调用 LikedService 封装的方法完成数据同步。

# 关于我

Brath 是一个热爱技术的 Java 程序猿,公众号「InterviewCoder」定期分享有趣有料的精品原创文章!

InterviewCoder

非常感谢各位人才能看到这里,原创不易,文章如果有帮助可以关注、点赞、分享或评论,这都是对我的莫大支持!

【Activiti】Java工作流引擎 Activiti 万字详细入门

InterviewCoder

# 【Activiti】Java 工作流引擎 Activiti 万字详细入门

# Activiti7

# 一、工作流介绍

# 1.1 概念

工作流 (Workflow),就是通过计算机对业务流程自动化执行管理。它主要解决的是 “使在多个参与者之间按照某种预定义的规则自动进行传递文档、信息或任务的过程,从而实现某个预期的业务目标,或者促使此目标的实现”。

# 1.2 工作流系统

一个软件系统中具有工作流的功能,我们把它称为工作流系统,一个系统中工作流的功能是什么?就是对系统的业务流程进行自动化管理,所以工作流是建立在业务流程的基础上,所以一个软件的系统核心根本上还是系统的业务流程,工作流只是协助进行业务流程管理。即使没有工作流业务系统也可以开发运行,只不过有了工作流可以更好的管理业务流程,提高系统的可扩展性。

# 1.3 适用行业

消费品行业,制造业,电信服务业,银证险等金融服务业,物流服务业,物业服务业,物业管理,大中型进出口贸易公司,政府事业机构,研究院所及教育服务业等,特别是大的跨国企业和集团公司。

# 1.4 具体应用

1、关键业务流程:订单、报价处理、合同审核、客户电话处理、供应链管理等

2、行政管理类:出差申请、加班申请、请假申请、用车申请、各种办公用品申请、购买申请、日报周报等凡是原来手工流转处理的行政表单。

3、人事管理类:员工培训安排、绩效考评、职位变动处理、员工档案信息管理等。

4、财务相关类:付款请求、应收款处理、日常报销处理、出差报销、预算和计划申请等。

5、客户服务类:客户信息管理、客户投诉、请求处理、售后服务管理等。

6、特殊服务类:ISO 系列对应流程、质量管理对应流程、产品数据信息管理、贸易公司报关处理、物流公司货物跟踪处理等各种通过表单逐步手工流转完成的任务均可应用工作流软件自动规范地实施。

# 1.5 实现方式

在没有专门的工作流引擎之前,我们之前为了实现流程控制,通常的做法就是采用状态字段的值来跟踪流程的变化情况。这样不同角色的用户,通过状态字段的取值来决定记录是否显示。

针对有权限可以查看的记录,当前用户根据自己的角色来决定审批是否合格的操作。如果合格将状态字段设置一个值,来代表合格;当然如果不合格也需要设置一个值来代表不合格的情况。

这是一种最为原始的方式。通过状态字段虽然做到了流程控制,但是当我们的流程发生变更的时候,这种方式所编写的代码也要进行调整。

那么有没有专业的方式来实现工作流的管理呢?并且可以做到业务流程变化之后,我们的程序可以不用改变,如果可以实现这样的效果,那么我们的业务系统的适应能力就得到了极大提升。

# 二、Activiti7 概述

# 2.1 介绍

Alfresco 软件在 2010 年 5 月 17 日宣布 Activiti 业务流程管理(BPM)开源项目的正式启动,其首席架构师由业务流程管理 BPM 的专家 Tom Baeyens 担任,Tom Baeyens 就是原来 jbpm 的架构师,而 jbpm 是一个非常有名的工作流引擎,当然 activiti 也是一个工作流引擎。

Activiti 是一个工作流引擎, activiti 可以将业务系统中复杂的业务流程抽取出来,使用专门的建模语言 BPMN2.0 进行定义,业务流程按照预先定义的流程进行执行,实现了系统的流程由 activiti 进行管理,减少业务系统由于流程变更进行系统升级改造的工作量,从而提高系统的健壮性,同时也减少了系统开发维护成本。

官方网站:https://www.activiti.org/

img

经历的版本:

img

目前最新版本:Activiti7.0.0.Beta

# 2.1.1 BPM

BPM(Business Process Management),即业务流程管理,是一种规范化的构造端到端的业务流程,以持续的提高组织业务效率。常见商业管理教育如 EMBA、MBA 等均将 BPM 包含在内。

# 2.1.2 BPM 软件

BPM 软件就是根据企业中业务环境的变化,推进人与人之间、人与系统之间以及系统与系统之间的整合及调整的经营方法与解决方案的 IT 工具。

通过 BPM 软件对企业内部及外部的业务流程的整个生命周期进行建模、自动化、管理监控和优化,使企业成本降低,利润得以大幅提升。

BPM 软件在企业中应用领域广泛,凡是有业务流程的地方都可以 BPM 软件进行管理,比如企业人事办公管理、采购流程管理、公文审批流程管理、财务管理等。

# 2.1.3 BPMN

BPMN(Business Process Model AndNotation)- 业务流程模型和符号 是由 BPMI(BusinessProcess Management Initiative)开发的一套标准的业务流程建模符号,使用 BPMN 提供的符号可以创建业务流程。

2004 年 5 月发布了 BPMN1.0 规范.BPMI 于 2005 年 9 月并入 OMG(The Object Management Group 对象管理组织) 组织。OMG 于 2011 年 1 月发布 BPMN2.0 的最终版本。

具体发展历史如下:

img

BPMN 是目前被各 BPM 厂商广泛接受的 BPM 标准。Activiti 就是使用 BPMN 2.0 进行流程建模、流程执行管理,它包括很多的建模符号,比如:

Event

用一个圆圈表示,它是流程中运行过程中发生的事情。

img

活动用圆角矩形表示,一个流程由一个活动或多个活动组成

img

Bpmn 图形其实是通过 xml 表示业务流程,上边的.bpmn 文件使用文本编辑器打开:

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
<?xml version="1.0" encoding="UTF-8"?>
<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:activiti="http://activiti.org/bpmn" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:omgdc="http://www.omg.org/spec/DD/20100524/DC" xmlns:omgdi="http://www.omg.org/spec/DD/20100524/DI" typeLanguage="http://www.w3.org/2001/XMLSchema" expressionLanguage="http://www.w3.org/1999/XPath" targetNamespace="http://www.activiti.org/test">
<process id="myProcess" name="My process" isExecutable="true">
<startEvent id="startevent1" name="Start"></startEvent>
<userTask id="usertask1" name="创建请假单"></userTask>
<sequenceFlow id="flow1" sourceRef="startevent1" targetRef="usertask1"></sequenceFlow>
<userTask id="usertask2" name="部门经理审核"></userTask>
<sequenceFlow id="flow2" sourceRef="usertask1" targetRef="usertask2"></sequenceFlow>
<userTask id="usertask3" name="人事复核"></userTask>
<sequenceFlow id="flow3" sourceRef="usertask2" targetRef="usertask3"></sequenceFlow>
<endEvent id="endevent1" name="End"></endEvent>
<sequenceFlow id="flow4" sourceRef="usertask3" targetRef="endevent1"></sequenceFlow>
</process>
<bpmndi:BPMNDiagram id="BPMNDiagram_myProcess">
<bpmndi:BPMNPlane bpmnElement="myProcess" id="BPMNPlane_myProcess">
<bpmndi:BPMNShape bpmnElement="startevent1" id="BPMNShape_startevent1">
<omgdc:Bounds height="35.0" width="35.0" x="130.0" y="160.0"></omgdc:Bounds>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape bpmnElement="usertask1" id="BPMNShape_usertask1">
<omgdc:Bounds height="55.0" width="105.0" x="210.0" y="150.0"></omgdc:Bounds>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape bpmnElement="usertask2" id="BPMNShape_usertask2">
<omgdc:Bounds height="55.0" width="105.0" x="360.0" y="150.0"></omgdc:Bounds>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape bpmnElement="usertask3" id="BPMNShape_usertask3">
<omgdc:Bounds height="55.0" width="105.0" x="510.0" y="150.0"></omgdc:Bounds>
</bpmndi:BPMNShape>
<bpmndi:BPMNShape bpmnElement="endevent1" id="BPMNShape_endevent1">
<omgdc:Bounds height="35.0" width="35.0" x="660.0" y="160.0"></omgdc:Bounds>
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge bpmnElement="flow1" id="BPMNEdge_flow1">
<omgdi:waypoint x="165.0" y="177.0"></omgdi:waypoint>
<omgdi:waypoint x="210.0" y="177.0"></omgdi:waypoint>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge bpmnElement="flow2" id="BPMNEdge_flow2">
<omgdi:waypoint x="315.0" y="177.0"></omgdi:waypoint>
<omgdi:waypoint x="360.0" y="177.0"></omgdi:waypoint>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge bpmnElement="flow3" id="BPMNEdge_flow3">
<omgdi:waypoint x="465.0" y="177.0"></omgdi:waypoint>
<omgdi:waypoint x="510.0" y="177.0"></omgdi:waypoint>
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge bpmnElement="flow4" id="BPMNEdge_flow4">
<omgdi:waypoint x="615.0" y="177.0"></omgdi:waypoint>
<omgdi:waypoint x="660.0" y="177.0"></omgdi:waypoint>
</bpmndi:BPMNEdge>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</definitions>

# 2.2 使用步骤

# 部署 activiti

Activiti 是一个工作流引擎(其实就是一堆 jar 包 API),业务系统访问 (操作) activiti 的接口,就可以方便的操作流程相关数据,这样就可以把工作流环境与业务系统的环境集成在一起。

# 流程定义

使用 activiti 流程建模工具 (activity-designer) 定义业务流程 (.bpmn 文件) 。

.bpmn 文件就是业务流程定义文件,通过 xml 定义业务流程。

# 流程定义部署

activiti 部署业务流程定义(.bpmn 文件)。

使用 activiti 提供的 api 把流程定义内容存储起来,在 Activiti 执行过程中可以查询定义的内容

Activiti 执行把流程定义内容存储在数据库中

# 启动一个流程实例

流程实例也叫:ProcessInstance

启动一个流程实例表示开始一次业务流程的运行。

在员工请假流程定义部署完成后,如果张三要请假就可以启动一个流程实例,如果李四要请假也启动一个流程实例,两个流程的执行互相不影响。

# 用户查询待办任务 (Task)

因为现在系统的业务流程已经交给 activiti 管理,通过 activiti 就可以查询当前流程执行到哪了,当前用户需要办理什么任务了,这些 activiti 帮我们管理了,而不需要开发人员自己编写在 sql 语句查询。

# 用户办理任务

用户查询待办任务后,就可以办理某个任务,如果这个任务办理完成还需要其它用户办理,比如采购单创建后由部门经理审核,这个过程也是由 activiti 帮我们完成了。

# 流程结束

当任务办理完成没有下一个任务结点了,这个流程实例就完成了。

# 三、Activiti 环境

# 3.1 开发环境

Jdk1.8 或以上版本

Mysql 5 及以上的版本

Tomcat8.5

IDEA

注意:activiti 的流程定义工具插件可以安装在 IDEA 下,也可以安装在 Eclipse 工具下

# 3.2 Activiti 环境

我们使用:Activiti7.0.0.Beta1 默认支持 spring5

# 3.2.1 下载 activiti7

Activiti 下载地址:http://activiti.org/download.html ,Maven 的依赖如下:

1
2
3
4
5
6
7
8
9
10
11
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.activiti</groupId>
<artifactId>activiti-dependencies</artifactId>
<version>7.0.0.Beta1</version>
<scope>import</scope>
<type>pom</type>
</dependency>
</dependencies>
</dependencyManagement>

1) Database

activiti 运行需要有数据库的支持,支持的数据库有:h2, mysql, oracle, postgres, mssql, db2。

# 3.2.2 流程设计器 IDEA 下安装

在 IDEA 的 File 菜单中找到子菜单”Settings”, 后面我们再选择左侧的 “plugins” 菜单,如下图所示:

img

此时我们就可以搜索到 actiBPM 插件,它就是 Activiti Designer 的 IDEA 版本,我们点击 Install 安装。

安装好后,页面如下:

img

提示需要重启 idea,点击重启。

重启完成后,再次打开 Settings 下的 Plugins(插件列表),点击右侧的 Installed(已安装的插件),在列表中看到 actiBPM,就说明已经安装成功了,如下图所示:

img

后面的课程里,我们会使用这个流程设计器进行 Activiti 的流程设计。

# 3.3 Activiti 的数据库支持

Activiti 在运行时需要数据库的支持,使用 25 张表,把流程定义节点内容读取到数据库表中,以供后续使用。

# 3.3.1 Activiti 支持的数据库

activiti 支持的数据库和版本如下:

数据库类型 版本 JDBC 连接示例 说明
h2 1.3.168 jdbc:h2:tcp://localhost/activiti 默认配置的数据库
mysql 5.1.21 jdbc:mysql://localhost:3306/activiti?autoReconnect=true 使用 mysql-connector-java 驱动测试
oracle 11.2.0.1.0 jdbc:oracle:thin:@localhost:1521:xe
postgres 8.1 jdbc:postgresql://localhost:5432/activiti
db2 DB2 10.1 using db2jcc4 jdbc:db2://localhost:50000/activiti
mssql 2008 using sqljdbc4 jdbc:sqlserver://localhost:1433/activiti

# 3.3.2 在 MySQL 生成表

# 3.3.2.1 创建数据库

创建 mysql 数据库 activiti (名字任意):

CREATE DATABASE activiti DEFAULT CHARACTER SET utf8;

# 3.3.2.2 使用 java 代码生成表

# 1) 创建 java 工程

使用 idea 创建 java 的 maven 工程,取名:activiti01。

# 2) 加入 maven 依赖的坐标(jar 包)

首先需要在 java 工程中加入 ProcessEngine 所需要的 jar 包,包括:

  1. activiti-engine-7.0.0.beta1.jar
  2. activiti 依赖的 jar 包: mybatis、 alf4j、 log4j 等
  3. activiti 依赖的 spring 包
  4. mysql 数据库驱动
  5. 第三方数据连接池 dbcp
  6. 单元测试 Junit-4.12.jar

我们使用 maven 来实现项目的构建,所以应当导入这些 jar 所对应的坐标到 pom.xml 文件中。

完整的依赖内容如下:

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
<properties>
<slf4j.version>1.6.6</slf4j.version>
<log4j.version>1.2.12</log4j.version>
<activiti.version>7.0.0.Beta1</activiti.version>
</properties>
<dependencies>
<dependency>
<groupId>org.activiti</groupId>
<artifactId>activiti-engine</artifactId>
<version>${activiti.version}</version>
</dependency>
<dependency>
<groupId>org.activiti</groupId>
<artifactId>activiti-spring</artifactId>
<version>${activiti.version}</version>
</dependency>
<!-- bpmn 模型处理 -->
<dependency>
<groupId>org.activiti</groupId>
<artifactId>activiti-bpmn-model</artifactId>
<version>${activiti.version}</version>
</dependency>
<!-- bpmn 转换 -->
<dependency>
<groupId>org.activiti</groupId>
<artifactId>activiti-bpmn-converter</artifactId>
<version>${activiti.version}</version>
</dependency>
<!-- bpmn json数据转换 -->
<dependency>
<groupId>org.activiti</groupId>
<artifactId>activiti-json-converter</artifactId>
<version>${activiti.version}</version>
</dependency>
<!-- bpmn 布局 -->
<dependency>
<groupId>org.activiti</groupId>
<artifactId>activiti-bpmn-layout</artifactId>
<version>${activiti.version}</version>
</dependency>
<!-- activiti 云支持 -->
<dependency>
<groupId>org.activiti.cloud</groupId>
<artifactId>activiti-cloud-services-api</artifactId>
<version>${activiti.version}</version>
</dependency>
<!-- mysql驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.40</version>
</dependency>
<!-- mybatis -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.4.5</version>
</dependency>
<!-- 链接池 -->
<dependency>
<groupId>commons-dbcp</groupId>
<artifactId>commons-dbcp</artifactId>
<version>1.4</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
<!-- log start -->
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>${log4j.version}</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>${slf4j.version}</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>${slf4j.version}</version>
</dependency>
</dependencies>
# 3) 添加 log4j 日志配置

我们使用 log4j 日志包,可以对日志进行配置

在 resources 下创建 log4j.properties

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# Set root category priority to INFO and its only appender to CONSOLE.
#log4j.rootCategory=INFO, CONSOLE debug info warn error fatal
log4j.rootCategory=debug, CONSOLE, LOGFILE
# Set the enterprise logger category to FATAL and its only appender to CONSOLE.
log4j.logger.org.apache.axis.enterprise=FATAL, CONSOLE
# CONSOLE is set to be a ConsoleAppender using a PatternLayout.
log4j.appender.CONSOLE=org.apache.log4j.ConsoleAppender
log4j.appender.CONSOLE.layout=org.apache.log4j.PatternLayout
log4j.appender.CONSOLE.layout.ConversionPattern=%d{ISO8601} %-6r[%15.15t] %-5p %30.30c %x - %m\n
# LOGFILE is set to be a File appender using a PatternLayout.
log4j.appender.LOGFILE=org.apache.log4j.FileAppender
log4j.appender.LOGFILE.File=f:\act\activiti.log
log4j.appender.LOGFILE.Append=true
log4j.appender.LOGFILE.layout=org.apache.log4j.PatternLayout
log4j.appender.LOGFILE.layout.ConversionPattern=%d{ISO8601} %-6r[%15.15t] %-5p %30.30c %x - %m\n
# 4) 添加 activiti 配置文件

我们使用 activiti 提供的默认方式来创建 mysql 的表。

默认方式的要求是在 resources 下创建 activiti.cfg.xml 文件,注意:默认方式目录和文件名不能修改,因为 activiti 的源码中已经设置,到固定的目录读取固定文件名的文件。

1
2
3
4
5
6
7
8
9
10
11
12
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/contex
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd">
</beans>
# 5) 在 activiti.cfg.xml 中进行配置

默认方式要在在 activiti.cfg.xml 中 bean 的名字叫 processEngineConfiguration,名字不可修改

在这里有 2 中配置方式:一种是单独配置数据源,一种是不单独配置数据源

# 1、直接配置 processEngineConfiguration

processEngineConfiguration 用来创建 ProcessEngine,在创建 ProcessEngine 时会执行数据库的操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/contex
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd">
<!-- 默认id对应的值 为processEngineConfiguration -->
<!-- processEngine Activiti的流程引擎 -->
<bean id="processEngineConfiguration"
class="org.activiti.engine.impl.cfg.StandaloneProcessEngineConfiguration">
<property name="jdbcDriver" value="com.mysql.jdbc.Driver"/>
<property name="jdbcUrl" value="jdbc:mysql:///activiti"/>
<property name="jdbcUsername" value="root"/>
<property name="jdbcPassword" value="123456"/>
<!-- activiti数据库表处理策略 -->
<property name="databaseSchemaUpdate" value="true"/>
</bean>
</beans>
# 2、配置数据源后,在 processEngineConfiguration 引用

首先配置数据源

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
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/contex
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd">

<!-- 这里可以使用 链接池 dbcp-->
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource">
<property name="driverClassName" value="com.mysql.jdbc.Driver" />
<property name="url" value="jdbc:mysql:///activiti" />
<property name="username" value="root" />
<property name="password" value="123456" />
<property name="maxActive" value="3" />
<property name="maxIdle" value="1" />
</bean>

<bean id="processEngineConfiguration"
class="org.activiti.engine.impl.cfg.StandaloneProcessEngineConfiguration">
<!-- 引用数据源 上面已经设置好了-->
<property name="dataSource" ref="dataSource" />
<!-- activiti数据库表处理策略 -->
<property name="databaseSchemaUpdate" value="true"/>
</bean>
</beans>
# 6) java 类编写程序生成表

创建一个测试类,调用 activiti 的工具类,生成 acitivti 需要的数据库表。

直接使用 activiti 提供的工具类 ProcessEngines,会默认读取 classpath 下的 activiti.cfg.xml 文件,读取其中的数据库配置,创建 ProcessEngine,在创建 ProcessEngine 时会自动创建表。

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.itheima.activiti01.test;

import org.activiti.engine.ProcessEngine;
import org.activiti.engine.ProcessEngineConfiguration;
import org.junit.Test;

public class TestDemo {
/**
* 生成 activiti的数据库表
*/
@Test
public void testCreateDbTable() {
//使用classpath下的activiti.cfg.xml中的配置创建processEngine
ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
System.out.println(processEngine);
}
}

说明:
1、运行以上程序段即可完成 activiti 表创建,通过改变 activiti.cfg.xml 中 databaseSchemaUpdate 参数的值执行不同的数据表处理策略。
2 、 上 边 的 方法 getDefaultProcessEngine 方法在执行时,从 activiti.cfg.xml 中找固定的名称 processEngineConfiguration 。

在测试程序执行过程中,idea 的控制台会输出日志,说明程序正在创建数据表,类似如下,注意红线内容:

img

执行完成后我们查看数据库, 创建了 25 张表,结果如下:

img

到这,我们就完成 activiti 运行需要的数据库和表的创建。

# 3.4 表结构介绍

# 3.4.1 表的命名规则和作用

看到刚才创建的表,我们发现 Activiti 的表都以 ACT_ 开头。

第二部分是表示表的用途的两个字母标识。 用途也和服务的 API 对应。
ACT_RE :'RE’表示 repository。 这个前缀的表包含了流程定义和流程静态资源 (图片,规则,等等)。
ACT_RU:'RU’表示 runtime。 这些运行时的表,包含流程实例,任务,变量,异步任务,等运行中的数据。 Activiti 只在流程实例执行过程中保存这些数据, 在流程结束时就会删除这些记录。 这样运行时表可以一直很小速度很快。
ACT_HI:'HI’表示 history。 这些表包含历史数据,比如历史流程实例, 变量,任务等等。
ACT_GE : GE 表示 general。 通用数据, 用于不同场景下

# 3.4.2 Activiti 数据表介绍

表分类 表名 解释
一般数据
[ACT_GE_BYTEARRAY] 通用的流程定义和流程资源
[ACT_GE_PROPERTY] 系统相关属性
流程历史记录
[ACT_HI_ACTINST] 历史的流程实例
[ACT_HI_ATTACHMENT] 历史的流程附件
[ACT_HI_COMMENT] 历史的说明性信息
[ACT_HI_DETAIL] 历史的流程运行中的细节信息
[ACT_HI_IDENTITYLINK] 历史的流程运行过程中用户关系
[ACT_HI_PROCINST] 历史的流程实例
[ACT_HI_TASKINST] 历史的任务实例
[ACT_HI_VARINST] 历史的流程运行中的变量信息
流程定义表
[ACT_RE_DEPLOYMENT] 部署单元信息
[ACT_RE_MODEL] 模型信息
[ACT_RE_PROCDEF] 已部署的流程定义
运行实例表
[ACT_RU_EVENT_SUBSCR] 运行时事件
[ACT_RU_EXECUTION] 运行时流程执行实例
[ACT_RU_IDENTITYLINK] 运行时用户关系信息,存储任务节点与参与者的相关信息
[ACT_RU_JOB] 运行时作业
[ACT_RU_TASK] 运行时任务
[ACT_RU_VARIABLE] 运行时变量表

# 四、Activiti 类关系图

上面我们完成了 Activiti 数据库表的生成,java 代码中我们调用 Activiti 的工具类,下面来了解 Activiti 的类关系

# 4.1 类关系图

img

在新版本中,我们通过实验可以发现 IdentityService,FormService 两个 Serivce 都已经删除了。

所以后面我们对于这两个 Service 也不讲解了,但老版本中还是有这两个 Service,同学们需要了解一下

# 4.2 activiti.cfg.xml

activiti 的引擎配置文件,包括:ProcessEngineConfiguration 的定义、数据源定义、事务管理器等,此文件其实就是一个 spring 配置文件。

# 4.3 流程引擎配置类

流程引擎的配置类(ProcessEngineConfiguration),通过 ProcessEngineConfiguration 可以创建工作流引擎 ProceccEngine,常用的两种方法如下:

# 4.3.1 StandaloneProcessEngineConfiguration

使用 StandaloneProcessEngineConfigurationActiviti 可以单独运行,来创建 ProcessEngine,Activiti 会自己处理事务。

配置文件方式:

通常在 activiti.cfg.xml 配置文件中定义一个 id 为 processEngineConfiguration 的 bean.

方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<bean id="processEngineConfiguration"
class="org.activiti.engine.impl.cfg.StandaloneProcessEngineConfiguration">
<!--配置数据库相关的信息-->
<!--数据库驱动-->
<property name="jdbcDriver" value="com.mysql.jdbc.Driver"/>
<!--数据库链接-->
<property name="jdbcUrl" value="jdbc:mysql:///activiti"/>
<!--数据库用户名-->
<property name="jdbcUsername" value="root"/>
<!--数据库密码-->
<property name="jdbcPassword" value="123456"/>
<!--actviti数据库表在生成时的策略 true - 如果数据库中已经存在相应的表,那么直接使用,如果不存在,那么会创建-->
<property name="databaseSchemaUpdate" value="true"/>
</bean>

还可以加入连接池:

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
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/contex
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd">
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource">
<property name="driverClassName" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql:///activiti"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
<property name="maxActive" value="3"/>
<property name="maxIdle" value="1"/>
</bean>
<!--在默认方式下 bean的id 固定为 processEngineConfiguration-->
<bean id="processEngineConfiguration"
class="org.activiti.engine.impl.cfg.StandaloneProcessEngineConfiguration">
<!--引入上面配置好的 链接池-->
<property name="dataSource" ref="dataSource"/>
<!--actviti数据库表在生成时的策略 true - 如果数据库中已经存在相应的表,那么直接使用,如果不存在,那么会创建-->
<property name="databaseSchemaUpdate" value="true"/>
</bean>
</beans>

# 4.3.2 SpringProcessEngineConfiguration

通过 org.activiti.spring.SpringProcessEngineConfiguration 与 Spring 整合。

创建 spring 与 activiti 的整合配置文件:

activity-spring.cfg.xml(名称可修改)

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
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:mvc="http://www.springframework.org/schema/mvc"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.1.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc-3.1.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-3.1.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-3.1.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx-3.1.xsd ">
<!-- 工作流引擎配置bean -->
<bean id="processEngineConfiguration" class="org.activiti.spring.SpringProcessEngineConfiguration">
<!-- 数据源 -->
<property name="dataSource" ref="dataSource" />
<!-- 使用spring事务管理器 -->
<property name="transactionManager" ref="transactionManager" />
<!-- 数据库策略 -->
<property name="databaseSchemaUpdate" value="drop-create" />
<!-- activiti的定时任务关闭 -->
<property name="jobExecutorActivate" value="false" />
</bean>
<!-- 流程引擎 -->
<bean id="processEngine" class="org.activiti.spring.ProcessEngineFactoryBean">
<property name="processEngineConfiguration" ref="processEngineConfiguration" />
</bean>
<!-- 资源服务service -->
<bean id="repositoryService" factory-bean="processEngine"
factory-method="getRepositoryService" />
<!-- 流程运行service -->
<bean id="runtimeService" factory-bean="processEngine"
factory-method="getRuntimeService" />
<!-- 任务管理service -->
<bean id="taskService" factory-bean="processEngine"
factory-method="getTaskService" />
<!-- 历史管理service -->
<bean id="historyService" factory-bean="processEngine" factory-method="getHistoryService" />
<!-- 用户管理service -->
<bean id="identityService" factory-bean="processEngine" factory-method="getIdentityService" />
<!-- 引擎管理service -->
<bean id="managementService" factory-bean="processEngine" factory-method="getManagementService" />
<!-- 数据源 -->
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource">
<property name="driverClassName" value="com.mysql.jdbc.Driver" />
<property name="url" value="jdbc:mysql://localhost:3306/activiti" />
<property name="username" value="root" />
<property name="password" value="mysql" />
<property name="maxActive" value="3" />
<property name="maxIdle" value="1" />
</bean>
<!-- 事务管理器 -->
<bean id="transactionManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource" />
</bean>
<!-- 通知 -->
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<tx:attributes></tx:attributes>
<!-- 传播行为 -->
<tx:method name="save*" propagation="REQUIRED" />
<tx:method name="insert*" propagation="REQUIRED" />
<tx:method name="delete*" propagation="REQUIRED" />
<tx:method name="update*" propagation="REQUIRED" />
<tx:method name="find*" propagation="SUPPORTS" read-only="true" />
<tx:method name="get*" propagation="SUPPORTS" read-only="true" />
</tx:attributes>
</tx:advice>
<!-- 切面,根据具体项目修改切点配置 -->
<aop:config proxy-target-class="true">
<aop:advisor advice-ref="txAdvice" pointcut="execution(* com.itheima.ihrm.service.impl.*.(..))"* />
</aop:config>
</beans>

# 创建 processEngineConfiguration

1
ProcessEngineConfiguration configuration = ProcessEngineConfiguration.createProcessEngineConfigurationFromResource("activiti.cfg.xml")

上边的代码要求 activiti.cfg.xml 中必须有一个 processEngineConfiguration 的 bean

也可以使用下边的方法,更改 bean 的名字:

1
ProcessEngineConfiguration.createProcessEngineConfigurationFromResource(String resource, String beanName);

# 4.4 工作流引擎创建

工作流引擎(ProcessEngine),相当于一个门面接口,通过 ProcessEngineConfiguration 创建 processEngine,通过 ProcessEngine 创建各个 service 接口。

# 4.4.1 默认创建方式

将 activiti.cfg.xml 文件名及路径固定,且 activiti.cfg.xml 文件中有 processEngineConfiguration 的配置, 可以使用如下代码创建 processEngine:

1
2
3
//直接使用工具类 ProcessEngines,使用classpath下的activiti.cfg.xml中的配置创建processEngine
ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
System.out.println(processEngine);

# 4.4.2 一般创建方式

1
2
3
4
//先构建ProcessEngineConfiguration
ProcessEngineConfiguration configuration = ProcessEngineConfiguration.createProcessEngineConfigurationFromResource("activiti.cfg.xml");
//通过ProcessEngineConfiguration创建ProcessEngine,此时会创建数据库
ProcessEngine processEngine = configuration.buildProcessEngine();

# 4.5 Servcie 服务接口

Service 是工作流引擎提供用于进行工作流部署、执行、管理的服务接口,我们使用这些接口可以就是操作服务对应的数据表

# 4.5.1 Service 创建方式

通过 ProcessEngine 创建 Service

方式如下:

1
2
3
RuntimeService runtimeService = processEngine.getRuntimeService();
RepositoryService repositoryService = processEngine.getRepositoryService();
TaskService taskService = processEngine.getTaskService();

# 4.5.2 Service 总览

service 名称 service 作用
RepositoryService activiti 的资源管理类
RuntimeService activiti 的流程运行管理类
TaskService activiti 的任务管理类
HistoryService activiti 的历史管理类
ManagerService activiti 的引擎管理类

简单介绍:

RepositoryService

是 activiti 的资源管理类,提供了管理和控制流程发布包和流程定义的操作。使用工作流建模工具设计的业务流程图需要使用此 service 将流程定义文件的内容部署到计算机。

除了部署流程定义以外还可以:查询引擎中的发布包和流程定义。

暂停或激活发布包,对应全部和特定流程定义。 暂停意味着它们不能再执行任何操作了,激活是对应的反向操作。获得多种资源,像是包含在发布包里的文件, 或引擎自动生成的流程图。

获得流程定义的 pojo 版本, 可以用来通过 java 解析流程,而不必通过 xml。

# RuntimeService

Activiti 的流程运行管理类。可以从这个服务类中获取很多关于流程执行相关的信息

# TaskService

Activiti 的任务管理类。可以从这个类中获取任务的信息。

# HistoryService

Activiti 的历史管理类,可以查询历史信息,执行流程时,引擎会保存很多数据(根据配置),比如流程实例启动时间,任务的参与者, 完成任务的时间,每个流程实例的执行路径,等等。 这个服务主要通过查询功能来获得这些数据。

# ManagementService

Activiti 的引擎管理类,提供了对 Activiti 流程引擎的管理和维护功能,这些功能不在工作流驱动的应用程序中使用,主要用于 Activiti 系统的日常维护。

# 五、Activiti 入门

在本章内容中,我们来创建一个 Activiti 工作流,并启动这个流程。

创建 Activiti 工作流主要包含以下几步:

1、定义流程,按照 BPMN 的规范,使用流程定义工具,用流程符号把整个流程描述出来

2、部署流程,把画好的流程定义文件,加载到数据库中,生成表的数据

3、启动流程,使用 java 代码来操作数据库表中的内容

# 5.1 流程符号

BPMN 2.0 是业务流程建模符号 2.0 的缩写。

它由 Business Process Management Initiative 这个非营利协会创建并不断发展。作为一种标识,BPMN 2.0 是使用一些符号来明确业务流程设计流程图的一整套符号规范,它能增进业务建模时的沟通效率。

目前 BPMN2.0 是最新的版本,它用于在 BPM 上下文中进行布局和可视化的沟通。

接下来我们先来了解在流程设计中常见的 符号。

BPMN2.0 的基本符合主要包含:

# 事件 Event

1574522151044

# 活动 Activity

活动是工作或任务的一个通用术语。一个活动可以是一个任务,还可以是一个当前流程的子处理流程; 其次,你还可以为活动指定不同的类型。常见活动如下:

1574562726375

# 网关 GateWay

网关用来处理决策,有几种常用网关需要了解:

1574563600305

# 排他网关 (x)

—— 只有一条路径会被选择。流程执行到该网关时,按照输出流的顺序逐个计算,当条件的计算结果为 true 时,继续执行当前网关的输出流;

如果多条线路计算结果都是 true,则会执行第一个值为 true 的线路。如果所有网关计算结果没有 true,则引擎会抛出异常。

排他网关需要和条件顺序流结合使用,default 属性指定默认顺序流,当所有的条件不满足时会执行默认顺序流。

# 并行网关 (+)

—— 所有路径会被同时选择

拆分 —— 并行执行所有输出顺序流,为每一条顺序流创建一个并行执行线路。

合并 —— 所有从并行网关拆分并执行完成的线路均在此等候,直到所有的线路都执行完成才继续向下执行。

# 包容网关 (+)

—— 可以同时执行多条线路,也可以在网关上设置条件

拆分 —— 计算每条线路上的表达式,当表达式计算结果为 true 时,创建一个并行线路并继续执行

合并 —— 所有从并行网关拆分并执行完成的线路均在此等候,直到所有的线路都执行完成才继续向下执行。

# 事件网关 (+)

—— 专门为中间捕获事件设置的,允许设置多个输出流指向多个不同的中间捕获事件。当流程执行到事件网关后,流程处于等待状态,需要等待抛出事件才能将等待状态转换为活动状态。

# 流向 Flow

流是连接两个流程节点的连线。常见的流向包含以下几种:

1574563937457

# 5.2 流程设计器使用

# Activiti-Designer 使用

# Palette(画板)

在 idea 中安装插件即可使用,画板中包括以下结点:

Connection— 连接

Event— 事件

Task— 任务

Gateway— 网关

Container— 容器

Boundary event— 边界事件

Intermediate event- - 中间事件

流程图设计完毕保存生成.bpmn 文件

# 新建流程 (IDEA 工具)

首先选中存放图形的目录 (选择 resources 下的 bpmn 目录),点击菜单:New -> BpmnFile,如图:

1575106985823

弹出如下图所示框,输入 evection 表示 出差审批流程:

1575107231004

起完名字 evection 后(默认扩展名为 bpmn),就可以看到流程设计页面,如图所示:

1575107336431

左侧区域是绘图区,右侧区域是 palette 画板区域

鼠标先点击画板的元素即可在左侧绘图

# 绘制流程

使用滑板来绘制流程,通过从右侧把图标拖拽到左侧的画板,最终效果如下:

1575107648105

# 指定流程定义 Key

流程定义 key 即流程定义的标识,通过 properties 视图查看流程的 key

1575115474865

# 指定任务负责人

在 properties 视图指定每个任务结点的负责人,如:填写出差申请的负责人为 zhangsan

1575121491752

经理审批负责人为 jerry

总经理审批负责人为 jack

财务审批负责人为 rose

# 六、流程操作

# 6.1 流程定义

# 概述

流程定义是线下按照 bpmn2.0 标准去描述 业务流程,通常使用 idea 中的插件对业务流程进行建模。

使用 idea 下的 designer 设计器绘制流程,并会生成两个文件:.bpmn 和.png

# .bpmn 文件

使用 activiti-desinger 设计业务流程,会生成.bpmn 文件,上面我们已经创建好了 bpmn 文件

BPMN 2.0 根节点是 definitions 节点。 这个元素中,可以定义多个流程定义(不过我们建议每个文件只包含一个流程定义, 可以简化开发过程中的维护难度)。 注意,definitions 元素 最少也要包含 xmlns 和 targetNamespace 的声明。 targetNamespace 可以是任意值,它用来对流程实例进行分类。

流程定义部分:定义了流程每个结点的描述及结点之间的流程流转。

流程布局定义:定义流程每个结点在流程图上的位置坐标等信息。

# 生成.png 图片文件

IDEA 工具中的操作方式

# 1、修改文件后缀为 xml

首先将 evection.bpmn 文件改名为 evection.xml,如下图:

1575108966935

evection.xml 修改前的 bpmn 文件,效果如下:
在这里插入图片描述

# 2、使用 designer 设计器打开.xml 文件

在 evection.xml 文件上面,点右键并选择 Diagrams 菜单,再选择 Show BPMN2.0 Designer…

1575109268443

# 3、查看打开的文件

打开后,却出现乱码,如图:

1575109366989

# 4、解决中文乱码

1、打开 Settings,找到 File Encodings,把 encoding 的选项都选择 UTF-8

1575112075626

2、打开 IDEA 安装路径,找到如下的安装目录

1575109627745

根据自己所安装的版本来决定,我使用的是 64 位的 idea,所以在 idea64.exe.vmoptions 文件的最后一行追加一条命令: -Dfile.encoding=UTF-8

如下所示:

https://images2017.cnblogs.com/blog/963440/201712/963440-20171221132445475-1259807908.png

一定注意,不要有空格,否则重启 IDEA 时会打不开,然后 重启 IDEA。

如果以上方法已经做完,还出现乱码,就再修改一个文件,并在文件的末尾添加: -Dfile.encoding=UTF-8,然后重启 idea,如图:

1575113551947

最后重新在 evection.xml 文件上面,点右键并选择 Diagrams 菜单,再选择 Show BPMN2.0 Designer…,看到生成图片,如图:

1575113951966

到此,解决乱码问题

# 5、导出为图片文件

点击 Export To File 的小图标,打开如下窗口,注意填写文件名及扩展名,选择好保存图片的位置:

1575114245068

然后,我们把 png 文件拷贝到 resources 下的 bpmn 目录,并且把 evection.xml 改名为 evection.bpmn。

# 6.2 流程定义部署

# 概述

将上面在设计器中定义的流程部署到 activiti 数据库中,就是流程定义部署。

通过调用 activiti 的 api 将流程定义的 bpmn 和 png 两个文件一个一个添加部署到 activiti 中,也可以将两个文件打成 zip 包进行部署。

# 单个文件部署方式

分别将 bpmn 文件和 png 图片文件部署。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class ActivitiDemo {
/**
* 部署流程定义
*/
@Test
public void testDeployment(){
// 1、创建ProcessEngine
ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
// 2、得到RepositoryService实例
RepositoryService repositoryService = processEngine.getRepositoryService();
// 3、使用RepositoryService进行部署
Deployment deployment = repositoryService.createDeployment()
.addClasspathResource("bpmn/evection.bpmn") // 添加bpmn资源
.addClasspathResource("bpmn/evection.png") // 添加png资源
.name("出差申请流程")
.deploy();
// 4、输出部署信息
System.out.println("流程部署id:" + deployment.getId());
System.out.println("流程部署名称:" + deployment.getName());
}
}

执行此操作后 activiti 会将上边代码中指定的 bpm 文件和图片文件保存在 activiti 数据库。

# 压缩包部署方式

将 evection.bpmn 和 evection.png 压缩成 zip 包。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Test
public void deployProcessByZip() {
// 定义zip输入流
InputStream inputStream = this
.getClass()
.getClassLoader()
.getResourceAsStream(
"bpmn/evection.zip");
ZipInputStream zipInputStream = new ZipInputStream(inputStream);
// 获取repositoryService
RepositoryService repositoryService = processEngine
.getRepositoryService();
// 流程部署
Deployment deployment = repositoryService.createDeployment()
.addZipInputStream(zipInputStream)
.deploy();
System.out.println("流程部署id:" + deployment.getId());
System.out.println("流程部署名称:" + deployment.getName());
}

执行此操作后 activiti 会将上边代码中指定的 bpm 文件和图片文件保存在 activiti 数据库。

# 操作数据表

流程定义部署后操作 activiti 的 3 张表如下:

act_re_deployment 流程定义部署表,每部署一次增加一条记录

act_re_procdef 流程定义表,部署每个新的流程定义都会在这张表中增加一条记录

act_ge_bytearray 流程资源表

接下来我们来看看,写入了什么数据:

1
2
SELECT * FROM act_re_deployment #流程定义部署表,记录流程部署信息
1

结果:

1575116732147

1
SELECT * FROM act_re_procdef #流程定义表,记录流程定义信息

结果:

注意,KEY 这个字段是用来唯一识别不同流程的关键字

1575116797665

1
2
SELECT * FROM act_ge_bytearray #资源表 

结果:

在这里插入图片描述

注意:

act_re_deployment 和 act_re_procdef 一对多关系,一次部署在流程部署表生成一条记录,但一次部署可以部署多个流程定义,每个流程定义在流程定义表生成一条记录。每一个流程定义在 act_ge_bytearray 会存在两个资源记录,bpmn 和 png。

建议:一次部署一个流程,这样部署表和流程定义表是一对一有关系,方便读取流程部署及流程定义信息。

# 6.3 启动流程实例

流程定义部署在 activiti 后就可以通过工作流管理业务流程了,也就是说上边部署的出差申请流程可以使用了。

针对该流程,启动一个流程表示发起一个新的出差申请单,这就相当于 java 类与 java 对象的关系,类定义好后需要 new 创建一个对象使用,当然可以 new 多个对象。对于请出差申请流程,张三发起一个出差申请单需要启动一个流程实例,出差申请单发起一个出差单也需要启动一个流程实例。

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
    /**
* 启动流程实例
*/
@Test
public void testStartProcess(){
// 1、创建ProcessEngine
ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
// 2、获取RunTimeService
RuntimeService runtimeService = processEngine.getRuntimeService();
// 3、根据流程定义Id启动流程
ProcessInstance processInstance = runtimeService
.startProcessInstanceByKey("myEvection");
// 输出内容
System.out.println("流程定义id:" + processInstance.getProcessDefinitionId());
System.out.println("流程实例id:" + processInstance.getId());
System.out.println("当前活动Id:" + processInstance.getActivityId());
}

输出内容如下:

1575117588624

操作数据表

act_hi_actinst 流程实例执行历史

act_hi_identitylink 流程的参与用户历史信息

act_hi_procinst 流程实例历史信息

act_hi_taskinst 流程任务历史信息

act_ru_execution 流程执行信息

act_ru_identitylink 流程的参与用户信息

act_ru_task 任务信息

# 6.4 任务查询

流程启动后,任务的负责人就可以查询自己当前需要处理的任务,查询出来的任务都是该用户的待办任务。

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
/**
* 查询当前个人待执行的任务
*/
@Test
public void testFindPersonalTaskList() {
// 任务负责人
String assignee = "zhangsan";
ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
// 创建TaskService
TaskService taskService = processEngine.getTaskService();
// 根据流程key 和 任务负责人 查询任务
List<Task> list = taskService.createTaskQuery()
.processDefinitionKey("myEvection") //流程Key
.taskAssignee(assignee)//只查询该任务负责人的任务
.list();

for (Task task : list) {

System.out.println("流程实例id:" + task.getProcessInstanceId());
System.out.println("任务id:" + task.getId());
System.out.println("任务负责人:" + task.getAssignee());
System.out.println("任务名称:" + task.getName());

}
}

输出结果如下:

1
2
3
4
流程实例id:2501
任务id:2505
任务负责人:zhangsan
任务名称:创建出差申请

# 6.5 流程任务处理

任务负责人查询待办任务,选择任务进行处理,完成任务。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 完成任务
@Test
public void completTask(){
// 获取引擎
ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
// 获取taskService
TaskService taskService = processEngine.getTaskService();

// 根据流程key 和 任务的负责人 查询任务
// 返回一个任务对象
Task task = taskService.createTaskQuery()
.processDefinitionKey("myEvection") //流程Key
.taskAssignee("zhangsan") //要查询的负责人
.singleResult();

// 完成任务,参数:任务id
taskService.complete(task.getId());
}

# 6.6 流程定义信息查询

查询流程相关信息,包含流程定义,流程部署,流程定义版本

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
    /**
* 查询流程定义
*/
@Test
public void queryProcessDefinition(){
// 获取引擎
ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
// repositoryService
RepositoryService repositoryService = processEngine.getRepositoryService();
// 得到ProcessDefinitionQuery 对象
ProcessDefinitionQuery processDefinitionQuery = repositoryService.createProcessDefinitionQuery();
// 查询出当前所有的流程定义
// 条件:processDefinitionKey =evection
// orderByProcessDefinitionVersion 按照版本排序
// desc倒叙
// list 返回集合
List<ProcessDefinition> definitionList = processDefinitionQuery.processDefinitionKey("myEvection")
.orderByProcessDefinitionVersion()
.desc()
.list();
// 输出流程定义信息
for (ProcessDefinition processDefinition : definitionList) {
System.out.println("流程定义 id="+processDefinition.getId());
System.out.println("流程定义 name="+processDefinition.getName());
System.out.println("流程定义 key="+processDefinition.getKey());
System.out.println("流程定义 Version="+processDefinition.getVersion());
System.out.println("流程部署ID ="+processDefinition.getDeploymentId());
}

}

输出结果:

1
2
3
4
流程定义id:myEvection:1:4
流程定义名称:出差申请单
流程定义key:myEvection
流程定义版本:1

# 6.7 流程删除

1
2
3
4
5
6
7
8
9
10
11
12
13
public void deleteDeployment() {
// 流程部署id
String deploymentId = "1";

ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
// 通过流程引擎获取repositoryService
RepositoryService repositoryService = processEngine
.getRepositoryService();
//删除流程定义,如果该流程定义已有流程实例启动则删除时出错
repositoryService.deleteDeployment(deploymentId);
//设置true 级联删除流程定义,即使该流程有流程实例启动也可以删除,设置为false非级别删除方式,如果流程
//repositoryService.deleteDeployment(deploymentId, true);
}

说明:

  1.   使用repositoryService删除流程定义,历史表信息不会被删除
    
    1
    2
    3

    2. ```
    如果该流程定义下没有正在运行的流程,则可以用普通删除。

如果该流程定义下存在已经运行的流程,使用普通删除报错,可用级联删除方法将流程及相关记录全部删除。

先删除没有完成流程节点,最后就可以完全删除流程定义信息

项目开发中级联删除操作一般只开放给超级管理员使用.

# 6.8 流程资源下载

现在我们的流程资源文件已经上传到数据库了,如果其他用户想要查看这些资源文件,可以从数据库中把资源文件下载到本地。

解决方案有:

1、jdbc 对 blob 类型,clob 类型数据读取出来,保存到文件目录

2、使用 activiti 的 api 来实现

使用 commons-io.jar 解决 IO 的操作

引入 commons-io 依赖包

1
2
3
4
5
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.6</version>
</dependency>

通过流程定义对象获取流程定义资源,获取 bpmn 和 png

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
import org.apache.commons.io.IOUtils;

@Test
public void deleteDeployment(){
// 获取引擎
ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
// 获取repositoryService
RepositoryService repositoryService = processEngine.getRepositoryService();
// 根据部署id 删除部署信息,如果想要级联删除,可以添加第二个参数,true
repositoryService.deleteDeployment("1");
}

public void queryBpmnFile() throws IOException {
// 1、得到引擎
ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
// 2、获取repositoryService
RepositoryService repositoryService = processEngine.getRepositoryService();
// 3、得到查询器:ProcessDefinitionQuery,设置查询条件,得到想要的流程定义
ProcessDefinition processDefinition = repositoryService.createProcessDefinitionQuery()
.processDefinitionKey("myEvection")
.singleResult();
// 4、通过流程定义信息,得到部署ID
String deploymentId = processDefinition.getDeploymentId();
// 5、通过repositoryService的方法,实现读取图片信息和bpmn信息
// png图片的流
InputStream pngInput = repositoryService.getResourceAsStream(deploymentId, processDefinition.getDiagramResourceName());
// bpmn文件的流
InputStream bpmnInput = repositoryService.getResourceAsStream(deploymentId, processDefinition.getResourceName());
// 6、构造OutputStream流
File file_png = new File("d:/evectionflow01.png");
File file_bpmn = new File("d:/evectionflow01.bpmn");
FileOutputStream bpmnOut = new FileOutputStream(file_bpmn);
FileOutputStream pngOut = new FileOutputStream(file_png);
// 7、输入流,输出流的转换
IOUtils.copy(pngInput,pngOut);
IOUtils.copy(bpmnInput,bpmnOut);
// 8、关闭流
pngOut.close();
bpmnOut.close();
pngInput.close();
bpmnInput.close();
}

说明:

  1.   deploymentId为流程部署ID
    
    1
    2
    3

    2. ```
    resource_name为act_ge_bytearray表中NAME_列的值
  2.   使用repositoryService的getDeploymentResourceNames方法可以获取指定部署下得所有文件的名称
    
    1
    2
    3

    4. ```
    使用repositoryService的getResourceAsStream方法传入部署ID和资源图片名称可以获取部署下指定名称文件的输入流

最后的将输入流中的图片资源进行输出。

# 6.9 流程历史信息的查看

即使流程定义已经删除了,流程执行的历史信息通过前面的分析,依然保存在 activiti 的 act_hi_* 相关的表中。所以我们还是可以查询流程执行的历史信息,可以通过 HistoryService 来查看相关的历史记录。

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
    /**
* 查看历史信息
*/
@Test
public void findHistoryInfo(){
// 获取引擎
ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
// 获取HistoryService
HistoryService historyService = processEngine.getHistoryService();
// 获取 actinst表的查询对象
HistoricActivityInstanceQuery instanceQuery = historyService.createHistoricActivityInstanceQuery();
// 查询 actinst表,条件:根据 InstanceId 查询
// instanceQuery.processInstanceId("2501");
// 查询 actinst表,条件:根据 DefinitionId 查询
instanceQuery.processDefinitionId("myEvection:1:4");
// 增加排序操作,orderByHistoricActivityInstanceStartTime 根据开始时间排序 asc 升序
instanceQuery.orderByHistoricActivityInstanceStartTime().asc();
// 查询所有内容
List<HistoricActivityInstance> activityInstanceList = instanceQuery.list();
// 输出
for (HistoricActivityInstance hi : activityInstanceList) {
System.out.println(hi.getActivityId());
System.out.println(hi.getActivityName());
System.out.println(hi.getProcessDefinitionId());
System.out.println(hi.getProcessInstanceId());
System.out.println("<==========================>");
}
}

# 总结

# 关于我

Brath 是一个热爱技术的 Java 程序猿,公众号「InterviewCoder」定期分享有趣有料的精品原创文章!

图片

InterviewCoder 非常感谢各位人才能看到这里,原创不易,文章如果有帮助可以关注、点赞、分享或评论,这都是对我的莫大支持!

【Redis】SpringBoot整合Redis,缓存批量删除 redisTemplate.keys(pattern)模糊查询找不到keys,“ “ 通配符无效

InterviewCoder

# 【Redis】SpringBoot 整合 Redis,缓存批量删除 redisTemplate.keys (pattern) 模糊查询找不到 keys,“ “ 通配符无效

# 引言

最近,在学习 Spring Boot 整合 Redis 的知识,在业务中需要删除某个前缀的所有 Redis 缓存,首先使用 RedisTemplate.keys () 模糊查询出所有合适的 keys,再使用 redisTemplate.delete () 方法进行批量删除。参考代码:

1
2
Set<String> keys = redisTemplate.keys(prefix + "*");
redisTemplate.delete(pageKeys);

然而,发现 redisTemplate.keys (prefix + “*” ) 模糊查询,总是返回一个空的集合,找不到 key。

在日志中打印查询的 keys 集合,一直为空集合:

img

# 1、问题分析:

首先,确保查询字符串正确。

然后,尝试 redisTemplate.keys (key) ,用完整的一个 key 进行查询,发现能正常返回只有一个 key 的集合。

说明模糊查询 的 通配符 " * " 没有发挥作用,可能只是被当作一个普通的字符了。

# 2、问题解决:

为 redis 添加配置文件 RedisConfig,重新定义 RedisTemplate 的 key 为 String 类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Configuration
public class RedisConfig {

@Bean(name = "redisTemplate")
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory){
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
StringRedisSerializer redisSerializer = new StringRedisSerializer();
// key采用String的序列化方式
redisTemplate.setKeySerializer(redisSerializer);
// hash的key也采用String的序列化方式
redisTemplate.setHashKeySerializer(redisSerializer);
return redisTemplate;
}
}

再次测试 redisTemplate.keys (prefix + “*” ) 模糊查询,可以正常返回缓存中的 keys 集合!!!

# 关于我

Brath 是一个热爱技术的 Java 程序猿,公众号「InterviewCoder」定期分享有趣有料的精品原创文章!

InterviewCoder

非常感谢各位人才能看到这里,原创不易,文章如果有帮助可以关注、点赞、分享或评论,这都是对我的莫大支持!

Linux环境下安装并启动Elasticsearch7

InterviewCoder

# Elasticsearch

​ Elasticsearch (ES) 是一个基于 Lucene 构建的开源、分布式、RESTful 接口全文搜索引擎。Elasticsearch 还是一个分布式文档数据库,其中每个字段均是被索引的数据且可被搜索,它能够扩展至数以百计的服务器存储以及处理 PB 级的数据。它可以在很短的时间内在存储、搜索和分析大量的数据。它通常作为具有复杂搜索场景情况下的核心发动机。es 是由 java 语言编写的。

Elasticsearch就是为高可用和可扩展而生的。可以通过购置性能更强的服务器来完成。

Elasticsearch:官方分布式搜索和分析引擎 | Elasticimghttps://www.elastic.co/cn/elasticsearch/

#

# Linux 里部署 ES

# 下载地址

​ 我下载的版本是 ES7.15.1

Elasticsearch 7.15.1 | Elasticimghttps://www.elastic.co/cn/downloads/past-releases/elasticsearch-7-15-1

img

img

# 上传到 Linux

​ 压缩包下载完成后上传到服务器

img

# 解压软件

​ 解压到上级目录,然后进行改名

1
2
3
4
# 解压缩
tar -zxvf elasticsearch-7.15.1-linux-x86_64.tar.gz -C ../
# 改名
mv elasticsearch-7.15.1 es-7.15.1

img

img

在 /opt 目录下新建 module/es 目录,同时把 es-7.15.1 移到该目录

1
mv es-7.15.1 /opt/module/es

# 创建用户

​ 因为安全问题, Elasticsearch 不允许 root 用户直接运行,所以要创建新用户,在 root 用户中创建新用户。

1
2
3
4
useradd es #新增 es 用户
passwd es #为 es 用户设置密码
userdel -r es #如果错了,可以删除再加
chown -R es:es /opt/module/es/es-7.15.1 #文件夹所有者

img

img

# 修改配置文件

修改 /root/es-7.15.1/config/elasticsearch.yml 文件。

img

1
2
3
4
5
6
# 加入如下配置
cluster.name: elasticsearch
node.name: node-1
network.host: 0.0.0.0
http.port: 9200
cluster.initial_master_nodes: ["node-1"]

img

# 修改 /etc/security/limits.conf

1
2
3
4
# 在文件末尾中增加下面内容
# 每个进程可以打开的文件数的限制
es soft nofile 65536
es hard nofile 65536

img

# 修改 /etc/sysctl.conf

1
2
3
# 在文件中增加下面内容
# 一个进程可以拥有的 VMA(虚拟内存区域)的数量,默认值为 65536
vm.max_map_count=655360

# 重新加载

sysctl -p

# 注意:

​ 启动前需要先切换到 es 用户

1
su es

# 启动 es

1
2
3
4
#启动 进入bin目录:
./elasticsearch
#后台启动
./elasticsearch -d

# 测试连接

​ 浏览器中打开 http:// 服务器 IP:9200/, 出现如下则说明安装成功

img

# 关于我

Brath 是一个热爱技术的 Java 程序猿,公众号「InterviewCoder」定期分享有趣有料的精品原创文章!

InterviewCoder

非常感谢各位人才能看到这里,原创不易,文章如果有帮助可以关注、点赞、分享或评论,这都是对我的莫大支持!