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

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


  • 首页

  • 归档

  • 搜索

重学c语言(二、反汇编和hello_worldo分析)

发表于 2021-11-27

之前的计划就是想重新学习一边编译链接的过程,这次是安排上了,但是还是感觉太年轻了,以上一篇的问题,去准备这一篇的时候,发现学习的东西还是很多的,应该是很多很多,惆怅。

但是flag已经发出去了,就硬着头皮去搞把,希望能坚持下来。加油。。。

2.1 目标文件

我们按照上一节的步骤,我们分步编译出来了,预处理文件.i,还有汇编文件.s,还有目标文件.o。

预处理文件上一篇就讲过了,汇编文件的话里面都是一些汇编语句,这个以后再讲,这一节我们的目标就是.o文件。

我有在之前的hello_world的基础上,添加了一些变量,和一个函数调用,这样分析起来会更全面。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
c复制代码#include <stdio.h>

int g_a = 0;
int g_b = 84;

int func1(int i)
{
printf("i = %d\n", i);
return 0;
}

int main(int argc, char **argv)
{
static int s_a = 0;
static int s_b = 84;

int a = 1;
int b;
func1(s_a+s_b+a+b);
printf("hello world %d %d %d\n", g_a, a, b);

return 0;
}

在这里hello_world中,我们定义了全局变量,局部变量,函数调用。

就按上一节的操作,我们直接汇编成.o文件。

我们可以使用file命令查看这个文件的格式:

1
2
shell复制代码root@ubuntu:~/c_test/02# file hello_world.o 
hello_world.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped

很明显hello_world.o文件也是一个ELF文件,不过不是可执行文件,是可重定位的文件,等待链接器把多个.o文件链接起来,怎么链接呢?下节再讲。

2.2 objdump命令

linux下,已经准备了两个命令给我们,就是给我们这些不安分的,一个是objdump反汇编的命令,一个是readelf解析elf的命令,这样我们就不用一个字节一个字节去对比elf文件,如果有兴趣的话,也可以去一个字节一个字节对比,不过我这样就不对比了,后来的任务巨重。

下面简单介绍一个objdump常用的参数:

1
2
3
4
5
6
7
shell复制代码objdump -f hello_world.o		# 显示文件的头信息
objdump -h hello_world.o # 显示常用段信息
objdump -x hello_world.o # 显示全面的段信息
objdump -d hello_world.o # 显示机制指令段的汇编信息 很常用
objdump -D hello_world.o # 显示全部汇编信息
objdump -s hello_world.o # 显示请求的所有部分的全部内容,以十六进制的方式
objdump -t hello_world.o # 显示符号表,类似nm -s

2.3 readelf命令

接着看看readelf常用的参数:

1
2
3
4
5
shell复制代码readelf -h hello_world.o		# 显示elf格式的头信息
readelf -S hello_world.o # 显示elf段表
readelf -s hello_world.o # 显示符号表
readelf -r hello_world.o # 显示重定位
readelf -d hello_world.o # 显示动态段

其实也不是很熟这些命令,以后有常用在添加。

2.4 hello_world.o分析

来到了这一章的重点,我们先分析hello_world.o,下次分析hello_world,以后看安排要不要把.a .so的也分析一波。

2.4.1 头信息

查看头信息,两个命令都可以查看到:

2.4.1.1 objdump -f

1
2
3
4
5
6
shell复制代码root@ubuntu:~/c_test/02# objdump -f hello_world.o

hello_world.o: file format elf64-x86-64
architecture: i386:x86-64, flags 0x00000011:
HAS_RELOC, HAS_SYMS
start address 0x0000000000000000

可以查看到编译文件的系统架构,文件的格式,后面还有文件的标记:

1
2
3
4
5
6
7
8
arduino复制代码/* BFD contains relocation entries.  */
#define HAS_RELOC 0x01

/* BFD is directly executable. */
#define EXEC_P 0x02
...
/* BFD has symbols. */
#define HAS_SYMS 0x10

2.4.1.2 readelf -h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
shell复制代码root@ubuntu:~/c_test/02# readelf -h hello_world.o 
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: REL (Relocatable file)
Machine: Advanced Micro Devices X86-64
Version: 0x1
Entry point address: 0x0
Start of program headers: 0 (bytes into file)
Start of section headers: 1168 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes)
Size of program headers: 0 (bytes)
Number of program headers: 0
Size of section headers: 64 (bytes)
Number of section headers: 13
Section header string table index: 10
root@ubuntu:~/c_test/02#

这个readelf命令重点是根据elf的二进制编码,然后把属于头部分的信息,转码出来,所以我们看的就是这个样子,又回想起当年,动不动就分析二进制的时候,哎,现在有一个工具多爽,直接输入一个命令就出来了,就不用去对这二进制数据分析了。

2.4.2 段信息

段信息是一个很重要的知识点,段信息也是可以使用两个命令来读取的。

这里readelf -S这个我们留着下节去分析hello_world,这一节我们用objdump -h。

先来总览一下:

1
2
3
shell复制代码root@ubuntu:~/c_test/02# size hello_world.o
text data bss dec hex filename
245 8 8 261 105 hello_world.o

我们可以用size来简单查看各个段大小,text段、data段、bss段。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
shell复制代码root@ubuntu:~/c_test/02# objdump -h hello_world.o

hello_world.o: file format elf64-x86-64

Sections:
Idx Name Size VMA LMA File off Algn
0 .text 0000007f 0000000000000000 0000000000000000 00000040 2**0
CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
1 .data 00000008 0000000000000000 0000000000000000 000000c0 2**2
CONTENTS, ALLOC, LOAD, DATA
2 .bss 00000008 0000000000000000 0000000000000000 000000c8 2**2
ALLOC
3 .rodata 0000001e 0000000000000000 0000000000000000 000000c8 2**0
CONTENTS, ALLOC, LOAD, READONLY, DATA
4 .comment 00000036 0000000000000000 0000000000000000 000000e6 2**0
CONTENTS, READONLY
5 .note.GNU-stack 00000000 0000000000000000 0000000000000000 0000011c 2**0
CONTENTS, READONLY
6 .eh_frame 00000058 0000000000000000 0000000000000000 00000120 2**3
CONTENTS, ALLOC, LOAD, RELOC, READONLY, DATA

重点出来了,接下来的目标,我们就重点分析这几个段。

2.4.2.1 text段

这一段是专门存放代码的,我们就来看看代码最后是怎么存的?想看不?

看的话还是需要objdump这个老工具人

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
shell复制代码root@ubuntu:~/c_test/02# objdump -s hello_world.o 

hello_world.o: file format elf64-x86-64

Contents of section .text:
0000 554889e5 4883ec10 897dfc8b 45fc89c6 UH..H....}..E...
0010 bf000000 00b80000 0000e800 000000b8 ................
0020 00000000 c9c35548 89e54883 ec20897d ......UH..H.. .}
0030 ec488975 e0c745f8 01000000 8b150000 .H.u..E.........
0040 00008b05 00000000 01c28b45 f801c28b ...........E....
0050 45fc01d0 89c7e800 0000008b 05000000 E...............
0060 008b4dfc 8b55f889 c6bf0000 0000b800 ..M..U..........
0070 000000e8 00000000 b8000000 00c9c3 ...............
... 只留下 .text 段
root@ubuntu:~/c_test/02#

-s 是把所有段以十六进制的格式输出,这也是代码中存储的格式,直接看十六进制看不懂,是不是,所以我们还需要把机器指令段反汇编出来,一对比就明白了很多。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
shell复制代码root@ubuntu:~/c_test/02# objdump -d hello_world.o 

hello_world.o: file format elf64-x86-64


Disassembly of section .text:

0000000000000000 <func1>:
0: 55 push %rbp
1: 48 89 e5 mov %rsp,%rbp
4: 48 83 ec 10 sub $0x10,%rsp
8: 89 7d fc mov %edi,-0x4(%rbp)
b: 8b 45 fc mov -0x4(%rbp),%eax
e: 89 c6 mov %eax,%esi
10: bf 00 00 00 00 mov $0x0,%edi
15: b8 00 00 00 00 mov $0x0,%eax
1a: e8 00 00 00 00 callq 1f <func1+0x1f>
1f: b8 00 00 00 00 mov $0x0,%eax
24: c9 leaveq
25: c3 retq

0000000000000026 <main>:
26: 55 push %rbp
27: 48 89 e5 mov %rsp,%rbp
2a: 48 83 ec 20 sub $0x20,%rsp
2e: 89 7d ec mov %edi,-0x14(%rbp)
31: 48 89 75 e0 mov %rsi,-0x20(%rbp)
35: c7 45 f8 01 00 00 00 movl $0x1,-0x8(%rbp)
3c: 8b 15 00 00 00 00 mov 0x0(%rip),%edx # 42 <main+0x1c>
42: 8b 05 00 00 00 00 mov 0x0(%rip),%eax # 48 <main+0x22>
48: 01 c2 add %eax,%edx
4a: 8b 45 f8 mov -0x8(%rbp),%eax
4d: 01 c2 add %eax,%edx
4f: 8b 45 fc mov -0x4(%rbp),%eax
52: 01 d0 add %edx,%eax
54: 89 c7 mov %eax,%edi
56: e8 00 00 00 00 callq 5b <main+0x35>
5b: 8b 05 00 00 00 00 mov 0x0(%rip),%eax # 61 <main+0x3b>
61: 8b 4d fc mov -0x4(%rbp),%ecx
64: 8b 55 f8 mov -0x8(%rbp),%edx
67: 89 c6 mov %eax,%esi
69: bf 00 00 00 00 mov $0x0,%edi
6e: b8 00 00 00 00 mov $0x0,%eax
73: e8 00 00 00 00 callq 78 <main+0x52>
78: b8 00 00 00 00 mov $0x0,%eax
7d: c9 leaveq
7e: c3 retq

左侧第一列,是这条语句的偏移,通过这个就能明白x86的机器命令是变长的。

左侧第二列,就是十六进制的内容,可以看成机器码。

右侧,就是对应的汇编指令了。

通过我们对比两个的十六进制,是不是完全符合,所以说代码存储在文件中,就是这样存储的。

2.4.2.2 .data段

程序中初始化的数据,就是不为0的全局变量,不为0的局部静态变量。

我们看上面的这个段是8个字节,怎么是8个字节呢?

在这里插入图片描述

这两个数据是存储在data段中的,不信是不是?

我们用反汇编来验证一波:

1
2
shell复制代码Contents of section .data:
0000 54000000 54000000 T...T...

0x54000000 = 84,这里面就是存了两个84。

为什么最低位在前面,这个就是linux系统一般都是小端模式。

顺序的话,应该是按变量的加载顺序,取值。

2.4.2.3 BSS段

程序中为0的全局变量,为0的静态变量,或者是不赋值,不赋值也会默认为0。

为什么会分出一个.bss段,是因为这样可以节省一点空间,等程序运行的时候在搞上来,所以这个段是需要大小的,统计程序中符合这个段的变量大小,留下空间,但是因为值为0,所以不要存储。

CONTENTS表示是这个.o程序包含CONTENTS这个字段,因为bss没有,所以没包含。

2.4.2.4 rodata

只读数据段,一般是放的是常量(const),比如字符串常量。

为什么会出现这样的只读数据段,这是在编译器中把这些只读的数据存储到一起,如果这段数据被修改,就会出现报错,编译器是这样检测只读数据的。

不过好像可以用一个指针指向这个地址,然后就可以直接修改了,因为linux系统,存放只读数据也是放在RAM上,理论上也是可以绕过编译器修改的。

像51单片机,因为RAM太少,只读数据是直接存储到flash中的,flash是一个不能改的存储介质,这个用指针指向也是不能修改的。

其实我很疑惑的一定就是这个代码中没有只读数据啊,怎么会有呢?

这就是我们c语言功底不够用,其实printf函数中的字符串,就是存储成字符串常量的,就是存在这个只读数据段的。不信?我们反汇编看看:

1
2
3
shell复制代码Contents of section .rodata:
0000 69203d20 25640a00 68656c6c 6f20776f i = %d..hello wo
0010 726c6420 25642025 64202564 0a00 rld %d %d %d..

是不是安排的明明白白,清清楚楚的。

2.4.2.5 comment

包含编译器的信息段

1
2
3
4
5
shell复制代码Contents of section .comment: 
0000 00474343 3a202855 62756e74 7520352e .GCC: (Ubuntu 5.
0010 342e302d 36756275 6e747531 7e31362e 4.0-6ubuntu1~16.
0020 30342e31 32292035 2e342e30 20323031 04.12) 5.4.0 201
0030 36303630 3900 60609.

2.4.2.6 .note.GNU-stack

堆栈提示段,表示不是很明白,

2.4.2.7 .eh_frame

保存c++异常处理的内容

1
2
3
4
5
6
7
shell复制代码Contents of section .eh_frame:
0000 14000000 00000000 017a5200 01781001 .........zR..x..
0010 1b0c0708 90010000 1c000000 1c000000 ................
0020 00000000 26000000 00410e10 8602430d ....&....A....C.
0030 06610c07 08000000 1c000000 3c000000 .a..........<...
0040 00000000 59000000 00410e10 8602430d ....Y....A....C.
0050 0602540c 07080000 ..T.....

这一段也不懂,以后再补补,不懂的地方还有很多。

2.4.2.8 自定义段

1
2
3
4
5
6
7
shell复制代码__attribute__((section("FOO"))) int global = 42;

__attribute__((section("BAR"))) void foo()

{undefined

}

当初分析uboot代码的时候,就很多这种自定义段,uboot启动后,会把同一个初始化的函数放到同一个段,然后好像是依次编译这个段的内容。

本文转载自: 掘金

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

MySQL高级应用窗口函数之偏移分析函数

发表于 2021-11-27

前言

本篇将通过示例讲解:偏移分析函数+over()

一、偏移分析函数概念

  • lag(col,n,default):用于统计分组内往上第n行值。
+ 第一个参数为列名
+ 第二个参数为往上第n行(可选,不填默认为1)
+ 第三个参数为默认值(当往上第n行为NULL时候,取默认值,如不指定,则为NULL)
  • lead(col,n,default):与lag相反,统计分组内往下第n行值。
+ 第一个参数为列名
+ 第二个参数为往下第n行(可选,不填默认为1)
+ 第三个参数为默认值(当往下第n行为NULL时候,取默认值,如不指定,则为NULL)
  • first_value(col):用于取分组内排序后,截止到当前行,第一个col的值。
  • last_value(col):用于取分组内排序后,截止到当前行,最后一个col的值。

二、示例讲解

2.1 案例

查询出用户【yantian】和【lisi】的时间偏移(前N行)

分析:通过lag()窗口函数进行实现

1
2
3
4
5
6
7
8
9
10
11
mysql复制代码SELECT
user_name,
pay_time,
lag( pay_time, 1, pay_time ) over ( PARTITION BY user_name ORDER BY pay_time ) lag1,
lag( pay_time ) over ( PARTITION BY user_name ORDER BY pay_time ) lag2,
lag( pay_time, 2, pay_time ) over ( PARTITION BY user_name ORDER BY pay_time ) lag3,
lag( pay_time, 2 ) over ( PARTITION BY user_name ORDER BY pay_time ) lag4
FROM
user_order
WHERE
user_name IN ( 'lisi', 'yantian' )

运行结果

05.MySQL高级应用窗口函数(四)01.jpg

2.2 案例

查询出用户【yantian】和【lisi】的时间偏移(后N行)

分析:通过lead()窗口函数进行实现

1
2
3
4
5
6
7
8
9
10
11
mysql复制代码SELECT
user_name,
pay_time,
lead( pay_time, 1, pay_time ) over ( PARTITION BY user_name ORDER BY pay_time ) lead1,
lead( pay_time ) over ( PARTITION BY user_name ORDER BY pay_time ) lead2,
lead( pay_time, 2, pay_time ) over ( PARTITION BY user_name ORDER BY pay_time ) lead3,
lead( pay_time, 2 ) over ( PARTITION BY user_name ORDER BY pay_time ) lead4
FROM
user_order
WHERE
user_name IN ( 'lisi', 'yantian' );

运行结果

05.MySQL高级应用窗口函数(四)02.jpg

2.3 案例

查询出支付时间间隔超过10天的用户数

分析:

  • 同一用户,相邻的订单进行下单时间比较,如果相邻订单下单时间间隔超过10天,那么这个用户就是需要统计的
  • 在这个过程中,需要进行相邻订单支付时间相减

实现步骤

1
2
scss复制代码(1)通过lead窗口函数根据支付时间进行分组,将下一行支付时间移到上一行当中
(2)通过上一步将支付时间做差,然后对用户去重,再进行count操作

实现

步骤一:通过lead窗口函数根据支付时间进行分组,将下一行支付时间移到上一行当中

1
2
3
4
5
6
mysql复制代码SELECT
user_name,
pay_time,
lead( pay_time, 1 ) over ( PARTITION BY user_name ORDER BY pay_time ) lead_time
FROM
user_order;

步骤二:通过上一步将支付时间做差,然后对用户去重,再进行count操作

1
2
3
4
5
6
mysql复制代码SELECT
count( DISTINCT user_name )
FROM
( SELECT user_name, pay_time, lead( pay_time, 1 ) over ( PARTITION BY user_name ORDER BY pay_time ) lead_time FROM user_order ) a
WHERE
datediff( a.lead_time, a.pay_time )> 10;

05.MySQL高级应用窗口函数(四)03.jpg

2.4 案例

查询出每年支付时间间隔最长的用户

实现步骤

1
2
3
scss复制代码(1)根据用户和支付年份进行分组,通过lag()窗口函数将上一行的支付时间合并到下一行
(2)计算订单时间间隔,在年度内按照各自的订单时间间隔进行排名
(3)将上方查询结果作为表进行子查询,取得排名结果为1的值

实现

步骤一:根据用户和支付年份进行分组,通过lag()窗口函数将上一行的支付时间合并到下一行

1
2
3
4
5
6
7
mysql复制代码SELECT YEAR
( pay_time ) year_time,
user_name,
pay_time,
lag( pay_time ) over ( PARTITION BY user_name, YEAR ( pay_time ) ORDER BY pay_time ASC ) lag_time
FROM
user_order;

步骤二:计算订单时间间隔,在年度内按照各自的订单时间间隔进行排名

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
mysql复制代码SELECT
a.year_time,
a.user_name,
datediff( a.pay_time, a.lag_time ) interval_days,
rank() over ( PARTITION BY a.year_time ORDER BY datediff( a.pay_time, a.lag_time ) DESC ) rank1
FROM
(
SELECT YEAR
( pay_time ) year_time,
user_name,
pay_time,
lag( pay_time ) over ( PARTITION BY user_name, YEAR ( pay_time ) ORDER BY pay_time ASC ) lag_time
FROM
user_order
) a;

步骤三:将上方查询结果作为表进行子查询,取得排名结果为1的值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
mysql复制代码SELECT
b.year_time,
b.user_name,
b.interval_days
FROM
(
SELECT
a.year_time,
a.user_name,
datediff( a.pay_time, a.lag_time ) interval_days,
rank() over ( PARTITION BY a.year_time ORDER BY datediff( a.pay_time, a.lag_time ) DESC ) rank1
FROM
(
SELECT YEAR
( pay_time ) year_time,
user_name,
pay_time,
lag( pay_time ) over ( PARTITION BY user_name, YEAR ( pay_time ) ORDER BY pay_time ASC ) lag_time
FROM
user_order
) a
) b
where b.rank1 = 1;

运行结果

05.MySQL高级应用窗口函数(四)04.jpg

本文转载自: 掘金

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

Java代理模式之Java访问者模式 Java访问者模式

发表于 2021-11-27

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

Java访问者模式

在访问者模式(Visitor Pattern)中,我们使用了一个访问者类,它改变了元素类的执行算法。通过这种方式,元素的执行算法可以随着访问者改变而改变。这种类型的设计模式属于行为型模式。根据模式,元素对象已接受访问者对象,这样访问者对象就可以处理元素对象上的操作。

介绍

意图:主要将数据结构与数据操作分离。

主要解决:稳定的数据结构和易变的操作耦合问题。

何时使用:需要对一个对象结构中的对象进行很多不同的并且不相关的操作,而需要避免让这些操作”污染”这些对象的类,使用访问者模式将这些封装到类中。

如何解决:在被访问的类里面加一个对外提供接待访问者的接口。

关键代码:在数据基础类里面有一个方法接受访问者,将自身引用传入访问者。

应用实例:您在朋友家做客,您是访问者,朋友接受您的访问,您通过朋友的描述,然后对朋友的描述做出一个判断,这就是访问者模式。

优点: ① 符合单一职责原则。 ② 优秀的扩展性。 ③ 灵活性。

缺点: ① 具体元素对访问者公布细节,违反了迪米特原则。② 具体元素变更比较困难。 ③ 违反了依赖倒置原则,依赖了具体类,没有依赖抽象。

使用场景: ① 对象结构中对象对应的类很少改变,但经常需要在此对象结构上定义新的操作。 ② 需要对一个对象结构中的对象进行很多不同的并且不相关的操作,而需要避免让这些操作”污染”这些对象的类,也不希望在增加新操作时修改这些类。

注意事项:访问者可以对功能进行统一,可以做报表、UI、拦截器与过滤器。

实现

我们将创建一个定义接受操作的 ComputerPart 接口。Keyboard、Mouse、Monitor 和 Computer 是实现了 ComputerPart 接口的实体类。我们将定义另一个接口 ComputerPartVisitor,它定义了访问者类的操作。Computer 使用实体访问者来执行相应的动作。

VisitorPatternDemo,我们的演示类使用 Computer、ComputerPartVisitor 类来演示访问者模式的用法。

步骤 1

定义一个表示元素的接口。

1
2
3
csharp复制代码public interface ComputerPart {
public void accept(ComputerPartVisitor computerPartVisitor);
}

步骤 2

创建扩展了上述类的实体类。

1
2
3
4
5
6
7
typescript复制代码public class Keyboard  implements ComputerPart {

@Override
public void accept(ComputerPartVisitor computerPartVisitor) {
computerPartVisitor.visit(this);
}
}
1
2
3
4
5
6
7
typescript复制代码public class Monitor  implements ComputerPart {

@Override
public void accept(ComputerPartVisitor computerPartVisitor) {
computerPartVisitor.visit(this);
}
}
1
2
3
4
5
6
7
typescript复制代码public class Mouse  implements ComputerPart {

@Override
public void accept(ComputerPartVisitor computerPartVisitor) {
computerPartVisitor.visit(this);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
scss复制代码public class Computer implements ComputerPart {

ComputerPart[] parts;

public Computer(){
parts = new ComputerPart[] {new Mouse(), new Keyboard(), new Monitor()};
}


@Override
public void accept(ComputerPartVisitor computerPartVisitor) {
for (int i = 0; i < parts.length; i++) {
parts[i].accept(computerPartVisitor);
}
computerPartVisitor.visit(this);
}
}

步骤 3

定义一个表示访问者的接口。

1
2
3
4
5
6
arduino复制代码public interface ComputerPartVisitor {
public void visit(Computer computer);
public void visit(Mouse mouse);
public void visit(Keyboard keyboard);
public void visit(Monitor monitor);
}

步骤 4

创建实现了上述类的实体访问者。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
typescript复制代码public class ComputerPartDisplayVisitor implements ComputerPartVisitor {

@Override
public void visit(Computer computer) {
System.out.println("Displaying Computer.");
}

@Override
public void visit(Mouse mouse) {
System.out.println("Displaying Mouse.");
}

@Override
public void visit(Keyboard keyboard) {
System.out.println("Displaying Keyboard.");
}

@Override
public void visit(Monitor monitor) {
System.out.println("Displaying Monitor.");
}
}

步骤 5

使用 ComputerPartDisplayVisitor 来显示 Computer 的组成部分。

1
2
3
4
5
6
7
typescript复制代码public class VisitorPatternDemo {
public static void main(String[] args) {

ComputerPart computer = new Computer();
computer.accept(new ComputerPartDisplayVisitor());
}
}

步骤 6

执行程序,输出结果:

1
2
3
4
erlang复制代码Displaying Mouse.
Displaying Keyboard.
Displaying Monitor.
Displaying Computer.

本文转载自: 掘金

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

C++类 类 类继承 操作符重载 struct 多态

发表于 2021-11-27

类

与java等面向对象语言类型类似,定义类需要定义访问修饰符、类成员与类函数。在类定义中定义的成员函数把函数声明为内联的,即便没有使用 inline 标识符。class定义的成员默认是 private 的。struct则是public,可以在外部定义成员函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
arduino复制代码class Box
{
private:
double length; // 盒子的长度
double breadth; // 盒子的宽度
double height; // 盒子的高度
public:
// 成员函数声明
double get(void);
void set( double len, double bre, double hei )
// 构造函数
Box();
Box(double len, double bre, double hei):length(len),breadth(bre),height(hei);
~Box();
};
// 成员函数定义
double Box::get(void)
{
return length * breadth * height;
}

类的对象的公共数据成员可以使用直接成员访问运算符 . 来访问,指针访问成员对象使用**→**

访问修饰符

类成员的访问限制是通过在类主体内部对各个区域标记 public、private、protected 来指定的

  • public公有成员在程序中类的外部是可访问的
  • private私有成员变量或函数在类的外部是不可访问的,只有类和友元函数可以访问私有成员
  • protected(受保护)成员在派生类(即子类)中是可访问的

构造函数执行顺序

基本逻辑:基类构造→子类构造→子类析构→基类析构

构造函数执行顺序

  1. 基类构造函数。如果有多个基类,则构造函数的调用顺序是该基类在派生类中出现的顺序,而不是他们在成员初始化列表中的顺序
  2. 成员类对象构造函数。如果有多个成员类构造函数调用顺序是对象在类中被声明的顺序,而不是他们在成员初始化列表中的顺序
  3. 派生类构造函数

析构函数顺序

  1. 派生类析构函数
  2. 成员类对象的析构函数
  3. 调用基类的析构函数

构造函数

用于为属性赋值/初始化,与java的构造函数一样

1
2
3
4
5
ini复制代码// 成员函数定义,包括构造函数
Line::Line(double len, int a) {
length = len;
ac = a;
}

另一种写法是使用初始化列表来初始化字段,推荐使用这种方法初始化构造函数,直接把参数len赋值给length

1
2
3
4
css复制代码C::C( double a, double b, double c): X(a), Y(b), Z(c)
{
....
}

需要注意,C++ 初始化类成员时,是按照声明的顺序初始化的,而不是按照出现在初始化列表中的顺序,顺序错误可能导致某些变量没有被赋值,因此变量顺序需要一致。

explicit 关键字

explicit 只对构造函数有效,用来避免隐式类型转换。且只对仅含有一个参数的类构造函数有效,因为多于两个的时候是不会发生隐式转换的(除非只有一个参数需要赋值,其他的参数有默认值)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
arduino复制代码Class String{
String (int n); // 分配n个字节空间给字符串
String (const char* p); // 用字符串p的值初始化字符串
}

# 正常初始化方法
String s1(10); // 10个字节长度的字符串
String s2("Hello world!") // s2的初始值为 Hello world

# 隐式转换写法
String s3 = 10; // 编译通过,分配10个字节长度的字符串
String s4 = 'a'; // 编译通过,分配int('a')个字节长度的字符串
String s5 = "a"; // 编译通过,调用的是String (const char* p)

# 使用 explicit 关键字
Class String{
explicit String (int n); // 分配n个字节空间给字符串
String (const char* p); // 用字符串p的值初始化字符串
}

String s3 = 10; // 编译不通过,不允许隐式转换类型
String s4 = 'a'; // 编译不通过,不允许隐式转换类型

使用**explicit关键字能够在编译阶段给出错误,避免运行时错误,规范化构造函数的使用,尽量使用explicit**

初始化对象

c++使用了两种用构造函数来初始化对象的方式:

1
2
3
4
5
6
ini复制代码//显示的调用构造函数
Stock food = Stock("World Cabbage", 250, 1.25);
//隐式的调用构造函数
Stock garment("Furry Mason", 50, 2.5);
// 动态分配new 动态的创建一个Stock对象,并将对象的地址赋给pstock指针,用指针管理该对象
Stock *pstock = new Stock("Furry Mason", 50, 2.5);

默认构造函数是指在没有显示的赋值时,用来创建对象的构造函数。如Stock fluffyp; ,如果类没有构造函数,c++编译器将自动提供一个默认构造函数,用来创建对象但不初始化值,一般默认构造函数的形式如Stokc::Stock(){} ,类没有定义任何构造函数的时候,编译器才会提供默认构造函数。但是如果类定义了构造函数,就必须要写一个默认构造函数。

析构函数

类的析构函数是类的一种特殊的成员函数,它会在每次删除所创建的对象时执行。析构函数的名称与类的名称是完全相同的,只是在前面加了个波浪号(~)作为前缀,它不会返回任何值,也不能带有任何参数。析构函数有助于在跳出程序(比如关闭文件、释放内存等)前释放资源。

拷贝构造函数

拷贝构造函数是一种特殊的构造函数,它在创建对象时使用同一类中之前创建的对象来初始化新创建的对象。拷贝构造函数通常用于:

  • 一个对象以值传递的方式传入函数体
  • 一个对象以值传递的方式从函数返回
  • 一个对象需要通过另外一个对象进行初始化。

当类成员中含有指针类型成员且需要对其分配内存时,一定要有总定义拷贝构造函数,默认的拷贝构造函数实现的只能是浅拷贝,即直接将原对象的数据成员值依次复制给新对象中对应的数据成员,并没有为新对象另外分配内存资源。这样,如果对象的数据成员是指针,两个指针对象实际上指向的是同一块内存空间。在某些情况下,浅拷贝回带来数据安全方面的隐患。

当类的数据成员中有指针类型时,我们就必须定义一个特定的拷贝构造函数,该拷贝构造函数不仅可以实现原对象和新对象之间数据成员的拷贝,而且可以为新的对象分配单独的内存资源,这就是**深拷贝构造函数**。

如果在类中没有定义拷贝构造函数,编译器会自行定义一个。如果类带有指针变量,并有动态内存分配,则它必须有一个拷贝构造函数。

1
2
3
arduino复制代码classname (const classname &obj) {
// 构造函数的主体
}
1
2
3
4
5
ini复制代码Line::Line(const Line &obj)
{
ptr = new int;
*ptr = *obj.ptr; // 拷贝值
}

友元函数

类的友元函数是定义在类外部,但有权访问类的所有私有(private)成员和保护(protected)成员。尽管友元函数的原型有在类的定义中出现过,但是友元函数并不是成员函数。

友元可以是一个函数,该函数被称为友元函数;友元也可以是一个类,该类被称为友元类,在这种情况下,整个类及其所有成员都是友元。

如果要声明函数为一个类的友元,需要在类定义中该函数原型前使用关键字 friend

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
arduino复制代码class Box
{
double width;
public:
friend void printWidth(Box box); // 声明友元函数
friend class BigBox; // 声明自己的友元类
void setWidth(double wid);
};

class BigBox
{
public :
void Print(int width, Box &box)
{
box.setWidth(width); // BigBox是Box的友元类,它可以直接访问Box类的任何成员
}
};

// 请注意:printWidth() 不是任何类的成员函数
void printWidth(Box box)
{
/* 因为 printWidth() 是 Box 的友元,它可以直接访问该类的任何成员 */
cout << "Width of box : " << box.width << endl;
}

类指针

一个指向 C++ 类的指针与指向结构的指针类似,访问指向类的指针的成员,需要使用成员访问运算符 -> ,就像访问指向结构的指针一样。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
c复制代码int main(void)
{
Box Box1(3.3, 1.2, 1.5); // Declare box1
Box Box2(8.5, 6.0, 2.0); // Declare box2
Box *ptrBox; // Declare pointer to a class.
// 保存第一个对象的地址
ptrBox = &Box1;
// 现在尝试使用成员访问运算符来访问成员
cout << "Volume of Box1: " << ptrBox->Volume() << endl;
// 保存第二个对象的地址
ptrBox = &Box2;
// 现在尝试使用成员访问运算符来访问成员
cout << "Volume of Box2: " << ptrBox->Volume() << endl;
return 0;
}

类作用域与静态成员

在类中声明常量要注意,在类中类只是描述一个结构的形式,在没有创建对象之前,类是没有用于存储的空间的,因此不能如下直接在类中声明一个变量值:

1
2
3
4
arduino复制代码class temp{
private:
const int Months = 12;//这样是不行的
};

如果我们要声明一个变量,要在前面加上static关键字,表明该常量是与其他静态变量存储在一起的,而不是存储在对象中的。因此month可以被所有的对象共享,在类声明时不能初始化static,但可以加上const初始化一个静态字面值常量。

类继承

在java中叫做子类,在c++中叫做派生类

1
2
3
4
5
6
7
8
9
kotlin复制代码// 基类
class Animal {
// eat() 函数
// sleep() 函数
};
//派生类
class Dog : public Animal {
// bark() 函数
};

派生类可以访问基类中所有的非私有成员。因此基类成员如果不想被派生类的成员函数访问,则应在基类中声明为 private。一个派生类继承了所有的基类方法,但下列情况除外:

  • 基类的构造函数、析构函数和拷贝构造函数。
  • 基类的重载运算符。
  • 基类的友元函数。

多继承

多继承即一个子类可以有多个父类,它继承了多个父类的特性。

1
2
3
4
5
6
7
8
9
10
11
kotlin复制代码// 基类 Shape
class Shape
{};

// 基类 PaintCost
class PaintCost
{};

// 派生类
class Rectangle: public Shape, public PaintCost
{};

操作符重载

操作符重载,也叫运算符重载,是C++的重要组成部分,如,加法运算符“+”对整数、单精度数和双精度数的操作是大不相同的。这是因为C++语言本身已经重载了该运算符,所以它能够用于int、float、double和其它内部定义类型的变量。操作符重载可对已有的运算符(C++中预定义的运算符)赋予多重的含义,使同一运算符作用于不同类型的数据时导致不同类型的行为。

其目的是:扩展C++中提供的运算符的适用范围,以用于类所表示的抽象数据类型。同一个运算符,对不同类型的操作数,所发生的行为不同。

1
2
3
4
5
6
arduino复制代码函数类型 operator 运算符名称 (形参表列)
{
对运算符的重载处理
}
// 例
Complex operator+ (Complex& c1,Complex& c2);

operator是C++的关键字,专门用于定义重载运算符的函数。operator +就是函数名,表示对运算符+重载。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
arduino复制代码class Complex
{
public:
Complex( double = 0.0, double = 0.0 );
Complex operator+( const Complex & ) const;
Complex operator-( const Complex & ) const;
private:
double real; // real part
double imaginary; // imaginary part
};
Complex Complex::operator+( const Complex &operand2 ) const
{
return Complex( real + operand2.real, imaginary + operand2.imaginary );
}
Complex Complex::operator-( const Complex &operand2 ) const
{
return Complex( real - operand2.real, imaginary - operand2.imaginary );
}

实质操作符的重载就是函数的重载,在程序编译时把指定的运算表达式转换成对运算符的调用,把运算的操作数转换成运算符函数的参数,根据实参的类型决定调用哪个操作符函数。如c3 = c1+c2最后在C++编译系统中被解释为:c3=c1.operator+(c2)

对于单目运算符++和–有两种使用方式,前置运算和后置运算,它们是不同的。针对这一特性,C++约定:如果在自增(自减)运算符重载函数中,无参数表示前置运算符函数,若加一个int型形参,就表示后置运算符函数。有一个Time类,包含数据成员minute(分)和sec(秒),模拟秒表,每次走一秒,满60秒进一分钟,此时秒又从0开始算。要求输出分和秒的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
arduino复制代码class Time
{
public:
Time( ){minute=0;sec=0;}
Time(int m,int s):minute(m),sec(s){ }
Time operator++( ); //声明前置自增运算符“++”重载函数
Time operator++(int); //声明后置自增运算符“++”重载函数
private:
int minute;
int sec;
};
Time Time::operator++( ) //定义前置自增运算符“++”重载函数
{
if(++sec>=60)
{
sec-=60; //满60秒进1分钟
++minute;
}
return *this; //返回当前对象值
}
Time Time::operator++(int) //定义后置自增运算符“++”重载函数
{
Time temp(*this);
sec++;
if(sec>=60)
{
sec-=60;
++minute;
}
return temp; //返回的是自加前的对象
}

struct

总的来说,struct更适合看成是一个数据结构的实现体,class更适合看成是一个对象的实现体。最本质的区别就是默认的访问控制,struct是 public的,class是 private 的。

struct重载运算符

1
2
3
4
5
6
7
8
9
csharp复制代码struct Inf
{
int hh, mm, ss;
string inOrOut;
Inf(){}
Inf(int hh, int mm, int ss) :hh(hh), mm(mm), ss(ss){}
}
# bool为函数类型,operator<一起为函数的名字(const Inf r)定义了一个不可以修改的参数
bool operator< (const Inf r)const { return hh * 3600 + mm * 60 + ss < r.hh * 3600 + r.mm * 60 + r.ss; }

多态

多态是在不同继承关系的类对象,去调同一函数,产生了不同的行为。在c++中多态有两种表现方式:

  • 「派生类的指针」可以赋给「基类指针」;调用哪个虚函数,取决于指针对象指向哪种类型的对象。
  • 派生类的对象可以赋给基类「引用」;调用哪个虚函数,取决于引用的对象是哪种类型的对象。

虚函数

c++使用虚函数实现多态,虚函数是一种由virtual关键字修饰的一种类内函数,可分为虚函数和纯虚函数。与java不同的是java中使用接口声明函数,父类与子类实现接口,C++在父类与子类中同时声明虚函数并实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
csharp复制代码// 基类
class CFather
{
public:
virtual void Fun() { } // 虚函数
};

// 派生类
class CSon : public CFather
{
public :
virtual void Fun() { }
};

int main()
{
CSon son;
CFather *p = &son;
p->Fun(); //调用哪个虚函数取决于 p 指向哪种类型的对象
// p 指针对象指向的是 CSon 类对象,所以 p->Fun() 调用的是 CSon 类里的 Fun 成员函数。
return 0;
}

析构函数可以写成虚的,但是构造函数不行。其中的原因比较复杂,简单地来说就是虚函数是通过一种特殊的功能来实现的,它储存在类所在的内存空间中,构造函数一般用于申请内存,那连内存都没有,怎么能找到这种特殊的功能呢?所以构造函数不能是虚的。

虚函数中的this指针

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
csharp复制代码class Base 
{
public:
void fun1()
{
this->fun2();
}

virtual void fun2() // 虚函数
{
cout << "Base::fun2()" << endl;
}
};

class Derived : public Base
{
public:
virtual void fun2() // 虚函数
{
cout << "Derived:fun2()" << endl;
}
};

int main()
{
Derived d;
Base *pBase = & d;
pBase->fun1();//Derived:fun2()
return 0;
}

this 指针的作用就是指向成员函数所作用的对象, 所以非静态成员函数中可以直接使用 this 来代表指向该函数作用的对象的指针。

pBase 指针对象指向的是派生类对象,派生类里没有 fun1 成员函数,所以就会调用基类的 fun1 成员函数,在Base::fun1() 成员函数体里执行 this->fun2() 时,实际上指向的是派生类对象的 fun2 成员函数。正确的输出结果是Derived:fun2()

析构函数最好是虚函数

基类中的析构函数如果是虚函数,那么派生类的析构函数就重写了基类的析构函数。这里他们的函数名不相同,看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor,这也说明的基类的析构函数最好写成虚函数。

将析构函数定义为虚函数的原因:

  • 基类指针可能指向派生类,当delete的时候,如果不定为虚函数,系统会直接调用基类的析构函数,这个时候派生类就有一部分没有被释放,就会造成可怕的内存泄漏问题。
  • 若定义为虚函数构成多态,那么就会先调用派生类的析构函数然后派生类的析构函数会自动调用基类的析构函数,这个结果满足我们的本意。

所以,在继承的时候,尽量把基类的析构函数定义为虚函数,这样继承下去的派生类的析构函数也会被变成虚函数构成多态。把基类的析构函数声明为virtual,派生类的析构函数可以 virtual 不进行声明,通过基类的指针删除派生类对象时,首先调用派生类的析构函数,然后调用基类的析构函数

纯虚函数

在虚函数的后面写上 =0 ,则这个函数就变成纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。这个纯虚函数的作用就是强迫我们重写虚函数,构成多态。这样更加体现出了接口继承。类似于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
arduino复制代码#include <iostream>

class Person
{
public:
virtual void Strength() = 0;
};

class Adult : public Person
{
public:
virtual void Strength()
{
std::cout << "Adult have big Strength!" << std::endl;
}
};

class Child : public Person
{
public:
virtual void Strength()
{
std::cout << "Child have Small Strength!" << std::endl;
}
};

在单继承的前提下,只要实例化的派生类不是抽象类就可以,一个抽象类是可以继承自抽象类的,并且它可以被另一个类所继承。

本文转载自: 掘金

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

哲学家就餐问题

发表于 2021-11-27

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

1.问题描述
五个哲学家围在一个圆桌,分别坐在圆桌的5个凳子上,圆桌上有5个碗5支筷子,他们就是思考和进餐交替进行,当哲学家饥饿时便试图去取左右最靠近他的筷子,只有他在拿到两个筷子才能进餐,进餐完毕又放下筷子继续思考。

2.分析
5个哲学家 只有5支筷子,而

  • 方法一
1
2
3
4
5
6
scss复制代码semaphore chopstick[5] = {1,1,1,1,1}; //5支筷子都是互斥信号量
p(chopstick[i]); //拿起左边的筷子
p(chopstick[i+1]%5); //拿起右边的筷子
吃饭。。。
v(chopstick[i]); //放下筷子
v(chopstick[i+1]%5);

假设我们5个哲学家都拿起了左边的筷子,而都在等待起右边的筷子,这样就会造成死锁。

  • 方法二
    既然5个人拿筷子吃饭 可能会造成死锁 拿就让四个人拿筷子,就可以打破这种死锁 所以我们增加一个信号量eat
1
2
3
4
5
6
7
8
9
scss复制代码semaphore chopstick[5] = {1,1,1,1,1}; //5支筷子都是互斥信号量
semaphore eat = 4;
p(eat)
p(chopstick[i]); //拿起左边的筷子
p(chopstick[i+1]%5); //拿起右边的筷子
吃饭。。。
v(chopstick[i]); //放下筷子
v(chopstick[i+1]%5);
v(eat)
  • 方法三
    可以在人数上进行限制 那也可以限制哲学家 必须在左右筷子都可拿的情况下才能拿(意思就是要拿就俩只都拿 要一个不能拿就两只都不拿 这样我们就需要设置一个互斥信号量mutex=1)
1
2
3
4
5
6
7
8
9
ini复制代码semaphore chopstick[5] = {1,1,1,1,1};
semaphore mutex = 1;
p(mutex)
p(chopstick[i]); //拿起左边的筷子
p(chopstick[i+1]%5); //拿起右边的筷子
吃饭。。。
v(chopstick[i]); //放下筷子
v(chopstick[i+1]%5);
v(mutex)
  • 方法四
    就是基数号的哲学家与偶数号的哲学家拿筷子不一样,这样理解 我们假设当哲学家饥饿时,会先拿左边的筷子再拿右边的筷子,当5个哲学家饥饿时,都取拿左边的筷子就会造成死锁 就如方法一上面的情形,假设我们让基数号的先拿左边筷子再拿右边 偶数号的先拿右边再拿左边 刚好就可以避免这种死锁。(反正就是基偶号先拿筷子要对比 不能顺着拿)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
scss复制代码 semaphore chopstick[5] = {1,1,1,1,1}; //5支筷子都是互斥信号量
if(i mod 2 != 0){ //基数
p(chopstick[i]); //拿起左边的筷子
p(chopstick[i+1]%5); //拿起右边的筷子
吃饭。。。
v(chopstick[i]); //放下筷子
v(chopstick[i+1]%5);
}else{
p(chopstick[i+1]%5); //拿起右边的筷子
p(chopstick[i]); //拿起左边的筷子
吃饭。。。
v(chopstick[i]); //放下筷子
v(chopstick[i+1]%5);

}

方法二-方法是争对方法一会死锁提出的改进原则,但我们首先要知道哲学家进餐的核心操作是要拿到两双筷子才能进餐,这里是通过一个数组来表示筷子这个互斥信号量的

本文转载自: 掘金

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

Linux文件分发脚本,只需一条命令将你的文件分发到各个服务

发表于 2021-11-27

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

背景

在运维或在日常工作生活中,我们经常会把一个文件拷贝到其它服务器上,或同时分发到多个服务器上,甚至要求目标机将文件放在相同的路径下,方便程序进一步调用。

遇到这种问题,我们通常的做法是使用scp或rsync命令把文件拷贝一个一个地拷贝到多台服务器上,这样做费时费力;大神的做法是使用ansible的playbook一下把事情干完,前提是你得会ansible;快捷的做法就是使用今天的脚本了。

效果演示

目前拥有4台机器,分别为client、node1、node2和node3,client与其它3台机器能够建立ssh链接。在client的/root/test目录下有a.txt和b.txt两个文件。

1
2
3
bash复制代码[root@client test]# ls /root/test/
a.txt b.txt
[root@client test]#

我把文件分发到node1、node2和node3的/root/test下,执行以下命令:

1
2
bash复制代码# 在/root/test目录下执行, xrsync是我的脚本
[root@client test]# xrsync a.txt b.txt

执行分发过程:

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
python复制代码[root@client test]# xrsync a.txt b.txt 
============ node1 ============
sending incremental file list
a.txt

sent 93 bytes received 35 bytes 256.00 bytes/sec
total size is 2 speedup is 0.02
sending incremental file list
b.txt

sent 93 bytes received 35 bytes 85.33 bytes/sec
total size is 2 speedup is 0.02
============ node2 ============
sending incremental file list
a.txt

sent 93 bytes received 35 bytes 256.00 bytes/sec
total size is 2 speedup is 0.02
sending incremental file list
b.txt

sent 93 bytes received 35 bytes 256.00 bytes/sec
total size is 2 speedup is 0.02
============ node3 ============
sending incremental file list
a.txt

sent 93 bytes received 35 bytes 85.33 bytes/sec
total size is 2 speedup is 0.02
sending incremental file list
b.txt

sent 93 bytes received 35 bytes 256.00 bytes/sec
total size is 2 spee

到node2上看一下,文件果然存在。同样地,node3和node4也同步过去了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
csharp复制代码# node2上查看
[root@node2 ~]# ls /root/test/
a.txt b.txt
[root@node2 ~]#

# node3上查看
[root@node3 ~]# ls /root/test/
a.txt b.txt
[root@node3 ~]#

# node4上查看
[root@node4 ~]# ls /root/test/
a.txt b.txt
[root@node4 ~]#

脚本奉上

整个脚本的代码,只需要把其中的node1 node2 node3修改为自己环境下的主机名或ip地址即可。

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
bash复制代码#!/bin/bash
# 判断参数是否足够
if [ $# -lt 1 ]
then
echo Not Enounh Arguement!
exit;
fi

# 遍历所有的机器
for host in node1 node2 node3
do
echo ============ $host ============
for file in $@
do
# 判断文件是否存在
if [ -e $file ]
then
# 获取父目录
pdir=$(cd -P $(dirname $file); pwd)

# 获取当前目录的名称
fname=$(basename $file)
ssh $host "mkdir -p $pdir"
rsync -av $pdir/$fname $host:$pdir
else
echo $file does not exists!
fi
done
done

运行条件

为了更方便脚本的运行,建议使用如下优化。

1.修改/etc/hosts文件,加入IP地址与主机名的对应关系,这样方便我们使用主机名直接操作。比如我演示的机器配置。

1
2
3
4
5
6
7
bash复制代码vim  /etc/hosts
# 加入配置,自己的机器对应修改
……
192.168.31.47 client
192.168.31.48 node1
192.168.31.50 node2
192.168.31.51 node3

2.客户机与目标机之间使用ssh密码验证登录,这样在传输文件时不需要二次验证。

1
2
3
4
5
6
7
8
9
bash复制代码# 生成ssh私钥
ssh-keygen -f /root/.ssh/id_rsa -N ''
# 循环把公钥传递到服务器上,免密登录
for i in node1 node2 node3
do
ssh-copy-id $i
done

# 根据提示输入密码

3.给脚本加可执行权限,并配置环境变量,使用全局可用。

1
2
3
4
5
6
7
8
9
10
11
ruby复制代码# 把文件存储为xrsync,加上x权限
[root@client shell]# chmod +x xrsync
[root@client shell]#

# 配置环境变量
# 我把脚本放在/opt/shell下的,自己情况类比修改
[root@client shell]# vim /etc/profile.d/my_env.sh
export PATH=$PATH:/opt/shell

# 配置生效,就可以在全局生效了
[root@client opt]# source /etc/profile

至此,早点干完活,开始愉快的玩耍吧~

本文转载自: 掘金

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

Go 实践 15:指针类型

发表于 2021-11-27

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

1 你在本章将学到什么?

  • 什么是指针?
  • 什么时指针类型?
  • 如何去创建并使用一个指针类型的变量。
  • 指正类型变量的零值是什么?
  • 什么是解除引用?
  • slices, maps, 和 channels 有什么特殊的地方?

2 涵盖的技术概念

  • 指针
  • 内存地址
  • 指针类型
  • 解除引用
  • 引用

3 什么是指针?

指针是“是一个数据项,它存储另外一个数据项的位置”。
在程序中,我们不断地存储和检索数据。例如,字符串、数字、复杂结构…。在物理层面,数据存储在内存中的特定地址,而指针存储的就是这些特定内存地址。

image

记住指针变量,就像其他变量一样,它也有一个内存地址。

4 指针类型

Go 中的指针类型不止一种,每一种普通类型就对应一个指针类型。相应地,指针类型也限定了它自己只能指向对应类型的普通变量(地址)。

指针类型的语法为:

1
text复制代码*BaseType

BaseType指代的是任何普通类型。

我们来看一下例子:

  • *int 表示指向 int 类型的指针
  • *uint8 表示指向 uint8 类型的指针
1
2
3
4
go复制代码type User struct {
ID string
Username string
}
  • *User 表示指向 User 类型的指针

5 如何去创建一个指针类型变量?

下面的语法可以创建:

1
go复制代码var p *int

这里我们创建了一个类型为 *int 的变量 p。*int 是指针类型(基础类型是 int)。

让我们来创建一个名为 answer 的整型变量。

1
go复制代码var answer int = 42

现在我们给变量 p 分配一个值了:

1
go复制代码p = &answer

使用 & 符号我们就能得到变 answer 的地址。来打印出这个地址~

1
2
go复制代码fmt.Println(p)
// 0xc000012070

0xc000012070 是一个十六进制数字,因为它的以 0x 为前缀。内存地址通常是以十六进制格式表示。你也可以使用二进制(用 0 和 1)表示,但不易读。

6 指针类型的零值

指针类型的零值都是 nil,也就是说,一个没有存储地址的指针等于 nil

1
2
3
go复制代码var q *int
fmt.Println(q == nil)
// true

7 解除引用

一个指针变量持有另一个变量的地址。如果你想通过指针去访问地址背后的变量值该怎么办?你可以使用解除引用操作符 *。

来举个例子,我们定义一个结构体类型 Cart:

1
2
3
4
go复制代码type Cart struct {
ID string
Paid bool
}

然后我们创建一个 Cart 类型的变量 cart,我们可以得到这个变量的地址,也可以通过地址找到这个变量:
image
image

  • 使用 * 操作符,你可以通过地址找到变量值
  • 使用 & 操作符,你可以得到变量的地址

7.1 空指针解引用:运行时 panic

每个 Go 程序员都会遇到这个 panic(报错):

1
2
shell复制代码panic: runtime error: invalid memory address or nil pointer dereference
[signal SIGSEGV: segmentation violation code=0x1 addr=0x0 pc=0x1091507]

为了更好地理解它,我们来复现一下:

1
2
3
4
5
6
7
8
go复制代码package main

import "fmt"

func main() {
var myPointerVar *int
fmt.Println(*myPointerVar)
}

在程序里,我们的定义了一个指针变量 myPointerVar,这个变量的类型是 *int(指向整型)。

然后我尝试对它进行解引用,myPointerVar 变量持有一个尚未初始化的指针,因此该指针的值为 nil。因为我们尝试去寻找一个不存在的地址,程序将会报错!我们尝试找到空地址,而空地址在内存中不存在。

8 Maps 和 channels

Maps 和 channels 变量里保存了对内部结构的指针。因此,即便向一个函数或方法传递的 map 或 channel 不是指针类型,也开始对这个 map 或 channel 进行修改。让我们看一个例子:

1
2
3
go复制代码func addElement(cities map[string]string) {
cities["France"] = "Paris"
}
  • 这个函数将一个 map 作为输入
  • 它向 map 中添加一项数据(key = “France”, value = “Paris”)
1
2
3
4
5
6
7
8
9
go复制代码package main

import "log"

func main() {
cities := make(map[string]string)
addElement(cities)
log.Println(cities)
}
  • 我们初始化一个名为 cities 的 map
  • 然后调用函数 addElement
  • 程序打印出:
1
text复制代码map[France:Paris]

我们将在专门的部分中更广泛地介绍 channels 和 maps。

9 切片

9.1 切片定义

切片是相同类型元素的集合。在内部,切片是一个具有三个字段的结构:

  • length:长度
  • capacity:容量
  • pointer:执向内部数组的指针
    下面是一个关于切片 EUcountries 的例子:
1
2
3
4
5
6
7
8
go复制代码package main

import "log"

func main() {
EUcountries := []string{"Austria", "Belgium", "Bulgaria"}
log.Println(EUcountries)
}

9.2 函数或方法将切片作为参数或接收器:小心

9.2.0.1 Example1: 向切片添加元素

1
2
3
4
5
6
7
8
9
10
11
12
13
go复制代码package main

import "log"

func main() {
EUcountries := []string{"Austria", "Belgium", "Bulgaria"}
addCountries(EUcountries)
log.Println(EUcountries)
}

func addCountries(countries []string) {
countries = append(countries, []string{"Croatia", "Republic of Cyprus", "Czech Republic", "Denmark", "Estonia", "Finland", "France", "Germany", "Greece", "Hungary", "Ireland", "Italy", "Latvia", "Lithuania", "Luxembourg", "Malta", "Netherlands", "Poland", "Portugal", "Romania", "Slovakia", "Slovenia", "Spain", "Sweden"}...)
}
  • 函数 addCountries 将一个字符串类型切片作为参数
  • 它通过内建函数 append 向切片添加字符串来修改切片
  • 它将缺失的欧盟国家附加到切片中
  • 问题*:依你看,程序的输出将会是下面的哪个?
1
2
3
text复制代码[Austria Belgium Bulgaria Croatia Republic of Cyprus Czech Republic Denmark Estonia Finland France Germany Greece Hungary Ireland Italy Latvia Lithuania Luxembourg Malta Netherlands Poland Portugal Romania Slovakia Slovenia Spain Sweden]

[Austria Belgium Bulgaria]

答案:这个函数实际输出:

1
text复制代码[Austria Belgium Bulgaria]

9.2.0.2 解释

  • 这个函数将[]string类型元素作为参数
  • 当函数被调用时,Go 会将切片 EUcountries 拷贝一份传进去
  • 函数将得到一个拷贝的切片数据:
    • 长度
    • 容量
    • 指向底层数据的指针
  • 在函数内部,缺失的国家被添加了进去
  • 切片的长度会增加
  • 运行时将分配一个新的内部数组

让我们在函数中添加一个日志来可视化它:

1
2
3
4
go复制代码func addCountries(countries []string) {
countries = append(countries, []string{"Croatia", "Republic of Cyprus", "Czech Republic", "Denmark", "Estonia", "Finland", "France", "Germany", "Greece", "Hungary", "Ireland", "Italy", "Latvia", "Lithuania", "Luxembourg", "Malta", "Netherlands", "Poland", "Portugal", "Romania", "Slovakia", "Slovenia", "Spain", "Sweden"}...)
log.Println(countries)
}

日志打印出:

1
text复制代码[Austria Belgium Bulgaria Croatia Republic of Cyprus Czech Republic Denmark Estonia Finland France Germany Greece Hungary Ireland Italy Latvia Lithuania Luxembourg Malta Netherlands Poland Portugal Romania Slovakia Slovenia Spain Sweden]
  • 这里的改变只会影响拷贝的版本

9.2.0.3 Example2:更新元素

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

import (
"log"
"strings"
)

func main() {
EUcountries := []string{"Austria", "Belgium", "Bulgaria"}
upper(EUcountries)
log.Println(EUcountries)
}

func upper(countries []string) {
for k, _ := range countries {
countries[k] = strings.ToUpper(countries[k])
}
}
  • 我们添加新函数 upper,它将把一个字符串切片的每个元素都转换成大写

问题:依你看,程序将传输下面哪个?

1
2
3
text复制代码[AUSTRIA BELGIUM BULGARIA]

[Austria Belgium Bulgaria]

答案:这个函数将返回:

1
text复制代码[AUSTRIA BELGIUM BULGARIA]

9.2.0.4 解释

  • 函数 upper 获取切片 EUcountries 的副本(和上面一样)
  • 在函数内部,我们更改切片元素的值 countries[k] = strings.ToUpper(countries[k])
  • 切片副本仍然有对底层数组的引用
  • 我们可以修改!
  • .. 但只有已经在切片中的切片元素。

9.2.0.5 结论

  • 当你将切片传递给函数时,它会获取切片的副本。
  • 这并不意味着你不能修改切片。
  • 你只可以修改切片中已经存在的元素。

9.3 函数或方法将切片指针作为参数或接收器

如果使用切片指针,你就可以在函数中修改这个切片了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
go复制代码package main

import (
"log"
)

func main() {
EUcountries := []string{"Austria", "Belgium", "Bulgaria"}
addCountries2(&EUcountries)
log.Println(EUcountries)
}

func addCountries2(countriesPtr *[]string) {
*countriesPtr = append(*countriesPtr, []string{"Croatia", "Republic of Cyprus", "Czech Republic", "Denmark", "Estonia", "Finland", "France", "Germany", "Greece", "Hungary", "Ireland", "Italy", "Latvia", "Lithuania", "Luxembourg", "Malta", "Netherlands", "Poland", "Portugal", "Romania", "Slovakia", "Slovenia", "Spain", "Sweden"}...)
}

这个程序将输出:

1
text复制代码[Austria Belgium Bulgaria Croatia Republic of Cyprus Czech Republic Denmark Estonia Finland France Germany Greece Hungary Ireland Italy Latvia Lithuania Luxembourg Malta Netherlands Poland Portugal Romania Slovakia Slovenia Spain Sweden]
  • 函数 addCountries2 将字符串切片的指针([]string)作为参数
  • 函数 append 调用时的第一个参数是 *countriesPtr(即我们通过指针 countriesPtr 去找到原值)
  • append 的第二个参数没有改变
  • 函数 addCountries2 的结果会影响到外部的变量

10 指向结构体的指针

有一个快捷方式可以让你直接修改 struct 类型的变量而无需使用*运算符:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
go复制代码type Item struct {
SKU string
Quantity int
}

type Cart struct {
ID string
CreatedDate time.Time
Items Item
}

cart := Cart{
ID: "115552221",
CreatedDate: time.Now(),
}
cartPtr := &cart
cartPtr.Items = []Item{
{SKU: "154550", Quantity: 12},
{SKU: "DTY8755", Quantity: 1},
}
log.Println(cart.Items)
// [{154550 12} {DTY8755 1}]
  • cart 是一个 Cart 类型变量
  • cartPtr := &cart 会获取变量 cart 的地址然后将其存储到 cartPtr 中
  • 使用变量 cartPtr,我们可以直接修改变量 cart 的 Item 字段
  • 这是因为运行时自动通过结构体指针找到了原值进行了修改,以下是等价的写法
1
2
3
4
go复制代码(*carPtr).Items = []Item{
{SKU: "154550", Quantity: 12},
{SKU: "DTY8755", Quantity: 1},
}

(这也有效,但更冗长)

11 使用指针作为方法的接收器

指针通常用作方法的接收器,让我们以 Cat 类型为例:

1
2
3
4
5
go复制代码type Cat struct {
Color string
Age uint8
Name string
}

你可以定义一个方法,使用指向 Cat 的指针作为方法的接收器(*Cat):

1
2
3
go复制代码func (cat *Cat) Meow(){
fmt.Println("Meooooow")
}

Meow 方法没有做任何有实际意义的事吗;它只是打印了字符串"Meooooow"。我们没有修改比变量的值。我们来看另一个方法,它修改了 cat 的 Name:

1
2
3
go复制代码func (cat *Cat) Rename(newName string){
cat.Name = newName
}

此方法将更改猫的名称。通过指针,我们修改了 Cat 结构体的一个字段。

当然,如果你不想使用指针作为接收器,你也可以:

1
2
3
go复制代码func (cat Cat) RenameV2(newName string){
cat.Name = newName
}

在这个例子中,变量 cat 是一个副本。接收器被命名为“值接收器”。因此,你对 cat 变量所做的任何修改都将在 cat 副本上完成:

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

import "fmt"

type Cat struct {
Color string
Age uint8
Name string
}

func (cat *Cat) Meow() {
fmt.Println("Meooooow")
}

func (cat *Cat) Rename(newName string) {
cat.Name = newName
}

func (cat Cat) RenameV2(newName string) {
cat.Name = newName
}

func main() {
cat := Cat{Color: "blue", Age: 8, Name: "Milow"}
cat.Rename("Bob")
fmt.Println(cat.Name)
// Bob

cat.RenameV2("Ben")
fmt.Println(cat.Name)
// Bob
}

在主函数的第一行,我们创建了一个 Cat 类型的变量 cat,它的 Name 是 "Millow"。
当我们调用具有值接收器的 RenameV2 方法时,函数外部变量 cat 的 Name 没有发生改变。
当我们调用 Rename 方法时,cat 的 Name 字段值会发生变化。
image

11.1 何时使用指针接收器,何时使用值接收器

  • 以下情况使用指针接收器:
    • 你的结构体很大(如果使用值接收器,Go 会复制它)
    • 你想修改接收器(例如,你想更改结构变量的名称字段)
    • 你的结构包含一个同步原语(如sync.Mutex)字段。如果你使用值接收器,它还会复制互斥锁,使其无用并导致同步错误。
    • 当接收器是一个 map、func、chan、slice、string 或 interface值时(因为在内部它已经是一个指针)
    • 当你的接收器是持有指针时

12 随堂测试

12.1 问题

  1. 如何去表示一个持有指向 Product 指针的变量?
  2. 指针类型的零值是多少?
  3. “解引用(dereferencing)” 是什么意思?
  4. 如何解引用一个指针?
  5. 填空: ____ 在内部是一个指向 ____ 的指针。
  6. 判断正误:当我想函数中修改 map 时,我的函数需要接收一个指向 map 的指针作为参数,我还需要返回修改后的 map?

12.2 答案

  1. 如何去表示一个持有指向 Product 指针的变量?
    *Product
  2. 指针类型的零值是多少?
    nil
  3. “解引用(dereferencing)” 是什么意思?
    • 指针是指向存储数据的内存位置的地址。
    • 当我们解引用一个指针时,我们可以访问存储在该地址的内存中的数据。
  4. 如何解引用一个指针?
    使用解引用操作符 *
  5. 填空: ____ 在内部是一个指向 ____ 的指针。

slice 在内部是一个指向 array 的指针。
6. 判断正误:当我想函数中修改 map 时,我的函数需要接收一个指向 map 的指针作为参数,我还需要返回修改后的 map
错, 函数中只要接收一个 map 类型参数就行,也不需要返回更改后的map,因为 map 变量内部存储了指向底层数据的指针

关键要点

  • 指针是指向数据的地址
  • 类型 *T 表示所有指向 T 类型变量的指针集合
  • 创建指针变量,可以使用运算符&。它将获取一个变量的地址
1
2
go复制代码userId := 12546584
p := &userId
1
2
3
go复制代码`userId` 是 `int` 类型的变量
`p` 是 `*int` 类型变量
`*int` 表示所有指向 `int` 类型变量的指针
  • 具有指针类型的参数/接收器的函数可以修改指针指向的值。
  • map 和 channel 是“引用类型”
  • 接收 map 或 channel 的函数/方法可以修改内部存储在这两个数据结构中的值(无需传递指向 map 的指针或指向 channel 的指针)
  • 切片在内部保存对数组的引用;任何接收切片的函数/方法都可以修改切片元素。
  • 当你想在函数中修改切片长度和容量时,你应该向该函数传递一个指向切片的指针 (*[]string)
  • 解引用允许你访问和修改存储在指针地址处的值。
  • 要对指针进行解引用操作,请使用运算符 *
1
2
3
4
go复制代码userId := 12546584
p := &userId
*p = 4
log.Println(userId)

p 是一个指针

  • 我们使用 *p 来对指针 p 进行解引用
  • 我们用指令 *p = 4 修改 userId 的值
  • 在代码片段的末尾,userId 的值为 4(不再是 12546584)
  • 当你有一个指向结构的指针时,你可以直接使用你的指针变量访问一个字段(不需要使用解引用运算符)
    • 例子:
1
2
3
4
5
go复制代码type Cart struct {
ID string
}
var cart Cart
cartPtr := &cart
  • 不需要这样写:(*cartPtr).ID = "1234"
  • 你可直接这样写:cartPtr.Items = "1234"
  • 变量 cart 就会被修改

本文转载自: 掘金

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

Go(六)来来来,教你怎么远程调用

发表于 2021-11-27

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

作者:lomtom

个人网站:lomtom.top,

个人公众号:博思奥园

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

Go系列:

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

每个微服务应用难免会有远程调用,那么在JAVA里面,有很多种远程调用的方法,最基础的手写HTTP调用,或者使用restTetmplate,再到使用openfeign仅仅写个接口就可以实现调用。

那么在Go语言里,Go也提供了Http调用的包net/http,或者使用http组件(gentleman、grequests、heimdall等)或者使用更高级的RPC调用,也有rpc框架(GRPC)。

那么在本章节探讨如何在Go语言里实现HTTP调用。

接口定义

首先进行接口的定义,方便后续进行远程调用。

这里以JAVA为例,以最常见的User实体,来编写相对应的操作。

例如:

1
2
3
4
5
bash复制代码GET 查询列表
GET 查询单个用户
POST 保存用户
PUT 修改单个用户
DELETE 删除单个用户

实体类

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
java复制代码@Data
@ApiModel("用户实体")
@NoArgsConstructor
public class User {
@ApiModelProperty(notes = "用户编号", example = "000001")
private Long id;

@NotEmpty(message = "用户名不能为空")
@ApiModelProperty(notes = "用户名", example = "小欧")
private String username;

@NotEmpty(message = "密码不能为空")
@ApiModelProperty(notes = "密码", example = "123456")
private String password;

@NotEmpty(message = "手机号不能为空")
@ApiModelProperty(notes = "手机号", example = "110")
private String telNew;

@TableLogic
@ApiModelProperty(notes = "是否删除", example = "true")
private Boolean deleted;

public User(Long id, String username, String password, String telNew, Boolean deleted) {
this.id = id;
this.username = username;
this.password = password;
this.telNew = telNew;
this.deleted = deleted;
}
}

接口

其次,编写相应接口

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
java复制代码@Api(tags = "用户管理")
@RestController
@RequestMapping("user")
@RequiredArgsConstructor(onConstructor_ = @Autowired)
public class UserController {

private final UserService userService;

private final IdGen idGen;

@ApiOperation("查看用户列表")
@GetMapping
public Result list(@RequestParam(required = false) Integer page,@RequestParam(required = false) Integer pageSize) {
Page<User> userPage = new Page<>();
userPage.setCurrent(page==null?1:page);
userPage.setSize(pageSize==null?10:pageSize);
Page<User> page1 = userService.page(userPage);
return Result.ok("page", page1);
}

@ApiOperation("获取单个用户信息")
@GetMapping("/{id}")
public Result info(@PathVariable Long id) {
User user = userService.getById(id);
return Result.ok("user", user).put("id", id);
}

@ApiOperation("保存用户信息")
@PostMapping
public Result save(@RequestBody @Validated User user) {
long id = idGen.nextId();
user.setId(id);
boolean save = userService.save(user);
return Result.ok("save", save).put("id",id);
}

@ApiOperation("更新用户信息")
@PutMapping
public Result update(@RequestBody @Validated User user) {
boolean update = userService.updateById(user);
return Result.ok("update", update);
}

@ApiOperation("删除用户信息")
@DeleteMapping("/{id}")
public Result delete(
@PathVariable
@NotNull(message = "不能为空")
Long id) {
boolean remove = userService.removeById(id);
return Result.ok("remove", remove).put("id",id);
}
}

Http请求调用

基本的GET请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
go复制代码// 基本的GET请求
func TestHttp(t *testing.T) {
res, err := http.Get("http://localhost:8080/user")
if err != nil {
log.Fatalf("did not connect: %v", err)
return
}
defer func(Body io.ReadCloser) {
err := Body.Close()
if err != nil {
log.Fatalf("did not close: %v", err)
return
}
}(res.Body)
if res.StatusCode == 200 {
log.Printf("success")
name, err := ioutil.ReadAll(res.Body)
if err != nil {
log.Fatalf("could not greet: %v", err)
}
log.Printf("Greeting: %s", 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
json复制代码2021/09/07 18:17:38 Greeting: 
{
"status":10020000,
"resMsg":"请求成功",
"data":{
"page":{
"records":[
{
"id":1,
"username":"lomtom",
"password":"123345",
"telNew":"110",
"deleted":false
},{
"id":2,
"username":"小菜",
"password":"123456",
"telNew":"112",
"deleted":false
}],
"total":2,
"size":10,
"current":1,
"orders":[],
"hitCount":false,
"searchCount":true,
"pages":1
}
}
}

GET请求 携带参数

若要在GET请求后携带参数,可以选择在URL后面进行拼接,当然也可以采用以下方式。

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
go复制代码// GET请求 携带参数 (除了拼接)
func TestHttp1(t *testing.T) {
params := url.Values{}
Url, err := url.Parse("http://localhost:8080/user")
if err != nil {
return
}
params.Set("page", "2")
params.Set("pageSize", "10")
Url.RawQuery = params.Encode()
urlPath := Url.String()
res, err := http.Get(urlPath)
if err != nil {
log.Fatalf("did not connect: %v", err)
return
}
defer func(Body io.ReadCloser) {
err := Body.Close()
if err != nil {
log.Fatalf("did not close: %v", err)
return
}
}(res.Body)
if res.StatusCode == 200 {
log.Printf("success")
name, err := ioutil.ReadAll(res.Body)
if err != nil {
log.Fatalf("could not greet: %v", err)
}
log.Printf("Greeting: %s", 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
json复制代码2021/09/07 18:17:38 Greeting: 
{
"status": 10020000,
"resMsg": "请求成功",
"data": {
"page": {
"records": [
{
"id": 1,
"username": "lomtom",
"password": "123345",
"telNew": "110",
"deleted": false
},
{
"id": 2,
"username": "小菜",
"password": "123456",
"telNew": "112",
"deleted": false
}
],
"total": 2,
"size": 10,
"current": 1,
"orders": [],
"hitCount": false,
"searchCount": true,
"pages": 1
}
}
}

基本的POST请求(form)

注意:该方法对应的接口接受类型必须也为表单接收,那么在java的接口需要去掉save方法里的@RequestBody注解,否则将调用失败。

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
go复制代码// 基本的POST请求 form
func TestHttp2(t *testing.T) {
params := url.Values{}
params.Set("username", "小小")
params.Set("password", "123456")
params.Set("telNew", "112")
res, err := http.PostForm("http://localhost:8080/user", params)
if err != nil {
log.Fatalf("did not connect: %v", err)
return
}
defer func(Body io.ReadCloser) {
err := Body.Close()
if err != nil {
log.Fatalf("did not close: %v", err)
return
}
}(res.Body)
if res.StatusCode == 200 {
log.Printf("success")
name, err := ioutil.ReadAll(res.Body)
if err != nil {
log.Fatalf("could not greet: %v", err)
}
log.Printf("Greeting: %s", name)
}
}

结果:

1
2
3
4
5
6
7
8
9
10
11
12
bash复制代码2021/09/07 18:22:12 Greeting: 
{
"status": 10020000,
"resMsg": "请求成功",
"data": {
"save": true,
"id": 1435186634277519360
}
}
2021-09-07 18:22:12.748 DEBUG 16028 --- [nio-8080-exec-2] c.l.d.mapper.UserMapper.insert : ==> Preparing: INSERT INTO user ( id, username, password, tel_new ) VALUES ( ?, ?, ?, ? )
2021-09-07 18:22:12.748 DEBUG 16028 --- [nio-8080-exec-2] c.l.d.mapper.UserMapper.insert : ==> Parameters: 1435186634277519360(Long), 小小(String), 123456(String), 112(String)
2021-09-07 18:22:12.752 DEBUG 16028 --- [nio-8080-exec-2] c.l.d.mapper.UserMapper.insert : <== Updates: 1

POST请求(body)

该方法为将参数放在请求的body部分。

  1. 同样需要注意的是,在java的接口需要加上save方法里的@RequestBody注解,否则将调用失败。
  2. 请求参数不能是自定义的结构体,必须是map类型,否则接口将匹配失败,猜测与自定义结构体的转换有关。
  3. 需要设置请求头部参数resq.Header.Set("Content-Type", "application/json")
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
go复制代码// POST请求 body
func TestHttp3(t *testing.T) {
//params := struct {
// username, password, telNew string
//}{"小小", "123456", "112"}
//str, err := json.Marshal(params)
//使用结构体 接口匹配不上

params := make(map[string]interface{})
params["username"] = "小小"
params["password"] = "123456"
params["telNew"] = "112"
str, err := json.Marshal(params)
resq, err := http.NewRequest("POST","http://localhost:8080/user", bytes.NewBuffer(str))
if err != nil {
log.Fatalf("error: %v", err)
return
}
resq.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(resq)
if err != nil {
log.Fatalf("did not connect: %v", err)
return
}
defer func(Body io.ReadCloser) {
err := Body.Close()
if err != nil {
log.Fatalf("did not close: %v", err)
return
}
}(res.Body)
if res.StatusCode == 200 {
log.Printf("success")
name, err := ioutil.ReadAll(res.Body)
if err != nil {
log.Fatalf("could not greet: %v", err)
}
log.Printf("Greeting: %s", name)
}
}

结果:

1
2
3
4
5
6
7
8
9
10
11
12
bash复制代码2021/09/08 16:05:30 Greeting: 
{
"status": 10020000,
"resMsg": "请求成功",
"data": {
"save": true,
"id": 1435514618490388480
}
}
2021-09-08 16:05:30.857 DEBUG 6100 --- [nio-8080-exec-6] c.l.d.mapper.UserMapper.insert : ==> Preparing: INSERT INTO user ( id, username, password, tel_new ) VALUES ( ?, ?, ?, ? )
2021-09-08 16:05:30.873 DEBUG 6100 --- [nio-8080-exec-6] c.l.d.mapper.UserMapper.insert : ==> Parameters: 1435514618490388480(Long), 小小(String), 123456(String), 112(String)
2021-09-08 16:05:30.879 DEBUG 6100 --- [nio-8080-exec-6] c.l.d.mapper.UserMapper.insert : <== Updates: 1

PUT 请求

对于net/http,暂时提供了两种接口Get和Post,那么对于PUT和DELETE的实现,就需要使用它的高级功能了。

  1. 首先使用http.NewRequest申明一个新的请求,设置相对应的请求方式、url、参数等
  2. 再使用http.DefaultClient.Do(resq)发起请求即可
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
go复制代码// PUT 请求
func TestHttp4(t *testing.T) {
//params := struct {
// id,username, password, telNew string
//}{"1435514618490388480""小小", "123456", "112"}
//str, err := json.Marshal(params)
//使用结构体 接口匹配不上

params := make(map[string]interface{})
params["id"] = "1435514618490388480"
params["username"] = "小道科"
params["password"] = "123456"
params["telNew"] = "112"
str, err := json.Marshal(params)
resq, err := http.NewRequest("PUT","http://localhost:8080/user", bytes.NewBuffer(str))
if err != nil {
log.Fatalf("error: %v", err)
return
}
resq.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(resq)
if err != nil {
log.Fatalf("did not connect: %v", err)
return
}
defer func(Body io.ReadCloser) {
err := Body.Close()
if err != nil {
log.Fatalf("did not close: %v", err)
return
}
}(res.Body)
if res.StatusCode == 200 {
log.Printf("success")
name, err := ioutil.ReadAll(res.Body)
if err != nil {
log.Fatalf("could not greet: %v", err)
}
log.Printf("Greeting: %s", name)
}
}

结果:

1
2
3
4
5
6
7
8
9
10
11
bash复制代码2021/09/08 16:09:47 Greeting:
{
"status": 10020000,
"resMsg": "请求成功",
"data": {
"update": true
}
}
2021-09-08 16:14:51.785 DEBUG 4380 --- [nio-8080-exec-9] c.l.d.mapper.UserMapper.updateById : ==> Preparing: UPDATE user SET username=?, password=?, tel_new=? WHERE id=? AND deleted=0
2021-09-08 16:14:51.785 DEBUG 4380 --- [nio-8080-exec-9] c.l.d.mapper.UserMapper.updateById : ==> Parameters: 小道科(String), 123456(String), 112(String), 1435514618490388480(Long)
2021-09-08 16:14:51.787 DEBUG 4380 --- [nio-8080-exec-9] c.l.d.mapper.UserMapper.updateById : <== Updates: 1

DELETE请求

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
go复制代码func TestHttp5(t *testing.T) {
resq, err := http.NewRequest("DELETE","http://localhost:8080/user/1435514618490388480",nil)
if err != nil {
log.Fatalf("error: %v", err)
return
}
res, err := http.DefaultClient.Do(resq)
if err != nil {
log.Fatalf("did not connect: %v", err)
return
}
defer func(Body io.ReadCloser) {
err := Body.Close()
if err != nil {
log.Fatalf("did not close: %v", err)
return
}
}(res.Body)
if res.StatusCode == 200 {
log.Printf("success")
name, err := ioutil.ReadAll(res.Body)
if err != nil {
log.Fatalf("could not greet: %v", err)
}
log.Printf("Greeting: %s", name)
}
}

结果:

1
2
3
4
5
6
7
8
9
10
11
bash复制代码2021/09/08 16:14:14 Greeting:
{
"status": 10020000,
"resMsg": "请求成功",
"data": {
"remove": true
}
}
2021-09-08 16:14:14.400 DEBUG 4380 --- [nio-8080-exec-5] c.l.d.mapper.UserMapper.deleteById : ==> Preparing: UPDATE user SET deleted=1 WHERE id=? AND deleted=0
2021-09-08 16:14:14.400 DEBUG 4380 --- [nio-8080-exec-5] c.l.d.mapper.UserMapper.deleteById : ==> Parameters: 1435514618490388480(Long)
2021-09-08 16:14:14.404 DEBUG 4380 --- [nio-8080-exec-5] c.l.d.mapper.UserMapper.deleteById : <== Updates: 1
  1. 如果需要自定义请求头、自定义方法,都可以使用http.NewRequest和 http.DefaultClient.Do(resq)来完成。
  2. 如需设置Header参数,可采用resq.Header.Set("Content-Type", "application/json")(参考POST请求)

每个微服务应用难免会有远程调用,那么在JAVA里面,有很多种远程调用的方法,最基础的手写HTTP调用,或者使用restTetmplate,再到使用openfeign仅仅写个接口就可以实现调用。

那么在Go语言里,Go也提供了Http调用的包net/http,或者使用http组件(gentleman、grequests、heimdall等)或者使用更高级的RPC调用,也有rpc框架(GRPC)。

那么在本章节探讨如何在Go语言里实现HTTP调用。

接口定义

首先进行接口的定义,方便后续进行远程调用。

这里以JAVA为例,以最常见的User实体,来编写相对应的操作。

例如:

1
2
3
4
5
bash复制代码GET 查询列表
GET 查询单个用户
POST 保存用户
PUT 修改单个用户
DELETE 删除单个用户

实体类

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
java复制代码@Data
@ApiModel("用户实体")
@NoArgsConstructor
public class User {
@ApiModelProperty(notes = "用户编号", example = "000001")
private Long id;

@NotEmpty(message = "用户名不能为空")
@ApiModelProperty(notes = "用户名", example = "小欧")
private String username;

@NotEmpty(message = "密码不能为空")
@ApiModelProperty(notes = "密码", example = "123456")
private String password;

@NotEmpty(message = "手机号不能为空")
@ApiModelProperty(notes = "手机号", example = "110")
private String telNew;

@TableLogic
@ApiModelProperty(notes = "是否删除", example = "true")
private Boolean deleted;

public User(Long id, String username, String password, String telNew, Boolean deleted) {
this.id = id;
this.username = username;
this.password = password;
this.telNew = telNew;
this.deleted = deleted;
}
}

接口

其次,编写相应接口

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
java复制代码@Api(tags = "用户管理")
@RestController
@RequestMapping("user")
@RequiredArgsConstructor(onConstructor_ = @Autowired)
public class UserController {

private final UserService userService;

private final IdGen idGen;

@ApiOperation("查看用户列表")
@GetMapping
public Result list(@RequestParam(required = false) Integer page,@RequestParam(required = false) Integer pageSize) {
Page<User> userPage = new Page<>();
userPage.setCurrent(page==null?1:page);
userPage.setSize(pageSize==null?10:pageSize);
Page<User> page1 = userService.page(userPage);
return Result.ok("page", page1);
}

@ApiOperation("获取单个用户信息")
@GetMapping("/{id}")
public Result info(@PathVariable Long id) {
User user = userService.getById(id);
return Result.ok("user", user).put("id", id);
}

@ApiOperation("保存用户信息")
@PostMapping
public Result save(@RequestBody @Validated User user) {
long id = idGen.nextId();
user.setId(id);
boolean save = userService.save(user);
return Result.ok("save", save).put("id",id);
}

@ApiOperation("更新用户信息")
@PutMapping
public Result update(@RequestBody @Validated User user) {
boolean update = userService.updateById(user);
return Result.ok("update", update);
}

@ApiOperation("删除用户信息")
@DeleteMapping("/{id}")
public Result delete(
@PathVariable
@NotNull(message = "不能为空")
Long id) {
boolean remove = userService.removeById(id);
return Result.ok("remove", remove).put("id",id);
}
}

Http请求调用

基本的GET请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
go复制代码// 基本的GET请求
func TestHttp(t *testing.T) {
res, err := http.Get("http://localhost:8080/user")
if err != nil {
log.Fatalf("did not connect: %v", err)
return
}
defer func(Body io.ReadCloser) {
err := Body.Close()
if err != nil {
log.Fatalf("did not close: %v", err)
return
}
}(res.Body)
if res.StatusCode == 200 {
log.Printf("success")
name, err := ioutil.ReadAll(res.Body)
if err != nil {
log.Fatalf("could not greet: %v", err)
}
log.Printf("Greeting: %s", 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
json复制代码2021/09/07 18:17:38 Greeting: 
{
"status":10020000,
"resMsg":"请求成功",
"data":{
"page":{
"records":[
{
"id":1,
"username":"lomtom",
"password":"123345",
"telNew":"110",
"deleted":false
},{
"id":2,
"username":"小菜",
"password":"123456",
"telNew":"112",
"deleted":false
}],
"total":2,
"size":10,
"current":1,
"orders":[],
"hitCount":false,
"searchCount":true,
"pages":1
}
}
}

GET请求 携带参数

若要在GET请求后携带参数,可以选择在URL后面进行拼接,当然也可以采用以下方式。

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
go复制代码// GET请求 携带参数 (除了拼接)
func TestHttp1(t *testing.T) {
params := url.Values{}
Url, err := url.Parse("http://localhost:8080/user")
if err != nil {
return
}
params.Set("page", "2")
params.Set("pageSize", "10")
Url.RawQuery = params.Encode()
urlPath := Url.String()
res, err := http.Get(urlPath)
if err != nil {
log.Fatalf("did not connect: %v", err)
return
}
defer func(Body io.ReadCloser) {
err := Body.Close()
if err != nil {
log.Fatalf("did not close: %v", err)
return
}
}(res.Body)
if res.StatusCode == 200 {
log.Printf("success")
name, err := ioutil.ReadAll(res.Body)
if err != nil {
log.Fatalf("could not greet: %v", err)
}
log.Printf("Greeting: %s", 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
json复制代码2021/09/07 18:17:38 Greeting: 
{
"status": 10020000,
"resMsg": "请求成功",
"data": {
"page": {
"records": [
{
"id": 1,
"username": "lomtom",
"password": "123345",
"telNew": "110",
"deleted": false
},
{
"id": 2,
"username": "小菜",
"password": "123456",
"telNew": "112",
"deleted": false
}
],
"total": 2,
"size": 10,
"current": 1,
"orders": [],
"hitCount": false,
"searchCount": true,
"pages": 1
}
}
}

基本的POST请求(form)

注意:该方法对应的接口接受类型必须也为表单接收,那么在java的接口需要去掉save方法里的@RequestBody注解,否则将调用失败。

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
go复制代码// 基本的POST请求 form
func TestHttp2(t *testing.T) {
params := url.Values{}
params.Set("username", "小小")
params.Set("password", "123456")
params.Set("telNew", "112")
res, err := http.PostForm("http://localhost:8080/user", params)
if err != nil {
log.Fatalf("did not connect: %v", err)
return
}
defer func(Body io.ReadCloser) {
err := Body.Close()
if err != nil {
log.Fatalf("did not close: %v", err)
return
}
}(res.Body)
if res.StatusCode == 200 {
log.Printf("success")
name, err := ioutil.ReadAll(res.Body)
if err != nil {
log.Fatalf("could not greet: %v", err)
}
log.Printf("Greeting: %s", name)
}
}

结果:

1
2
3
4
5
6
7
8
9
10
11
12
bash复制代码2021/09/07 18:22:12 Greeting: 
{
"status": 10020000,
"resMsg": "请求成功",
"data": {
"save": true,
"id": 1435186634277519360
}
}
2021-09-07 18:22:12.748 DEBUG 16028 --- [nio-8080-exec-2] c.l.d.mapper.UserMapper.insert : ==> Preparing: INSERT INTO user ( id, username, password, tel_new ) VALUES ( ?, ?, ?, ? )
2021-09-07 18:22:12.748 DEBUG 16028 --- [nio-8080-exec-2] c.l.d.mapper.UserMapper.insert : ==> Parameters: 1435186634277519360(Long), 小小(String), 123456(String), 112(String)
2021-09-07 18:22:12.752 DEBUG 16028 --- [nio-8080-exec-2] c.l.d.mapper.UserMapper.insert : <== Updates: 1

POST请求(body)

该方法为将参数放在请求的body部分。

  1. 同样需要注意的是,在java的接口需要加上save方法里的@RequestBody注解,否则将调用失败。
  2. 请求参数不能是自定义的结构体,必须是map类型,否则接口将匹配失败,猜测与自定义结构体的转换有关。
  3. 需要设置请求头部参数resq.Header.Set("Content-Type", "application/json")
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
go复制代码// POST请求 body
func TestHttp3(t *testing.T) {
//params := struct {
// username, password, telNew string
//}{"小小", "123456", "112"}
//str, err := json.Marshal(params)
//使用结构体 接口匹配不上

params := make(map[string]interface{})
params["username"] = "小小"
params["password"] = "123456"
params["telNew"] = "112"
str, err := json.Marshal(params)
resq, err := http.NewRequest("POST","http://localhost:8080/user", bytes.NewBuffer(str))
if err != nil {
log.Fatalf("error: %v", err)
return
}
resq.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(resq)
if err != nil {
log.Fatalf("did not connect: %v", err)
return
}
defer func(Body io.ReadCloser) {
err := Body.Close()
if err != nil {
log.Fatalf("did not close: %v", err)
return
}
}(res.Body)
if res.StatusCode == 200 {
log.Printf("success")
name, err := ioutil.ReadAll(res.Body)
if err != nil {
log.Fatalf("could not greet: %v", err)
}
log.Printf("Greeting: %s", name)
}
}

结果:

1
2
3
4
5
6
7
8
9
10
11
12
bash复制代码2021/09/08 16:05:30 Greeting: 
{
"status": 10020000,
"resMsg": "请求成功",
"data": {
"save": true,
"id": 1435514618490388480
}
}
2021-09-08 16:05:30.857 DEBUG 6100 --- [nio-8080-exec-6] c.l.d.mapper.UserMapper.insert : ==> Preparing: INSERT INTO user ( id, username, password, tel_new ) VALUES ( ?, ?, ?, ? )
2021-09-08 16:05:30.873 DEBUG 6100 --- [nio-8080-exec-6] c.l.d.mapper.UserMapper.insert : ==> Parameters: 1435514618490388480(Long), 小小(String), 123456(String), 112(String)
2021-09-08 16:05:30.879 DEBUG 6100 --- [nio-8080-exec-6] c.l.d.mapper.UserMapper.insert : <== Updates: 1

PUT 请求

对于net/http,暂时提供了两种接口Get和Post,那么对于PUT和DELETE的实现,就需要使用它的高级功能了。

  1. 首先使用http.NewRequest申明一个新的请求,设置相对应的请求方式、url、参数等
  2. 再使用http.DefaultClient.Do(resq)发起请求即可
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
go复制代码// PUT 请求
func TestHttp4(t *testing.T) {
//params := struct {
// id,username, password, telNew string
//}{"1435514618490388480""小小", "123456", "112"}
//str, err := json.Marshal(params)
//使用结构体 接口匹配不上

params := make(map[string]interface{})
params["id"] = "1435514618490388480"
params["username"] = "小道科"
params["password"] = "123456"
params["telNew"] = "112"
str, err := json.Marshal(params)
resq, err := http.NewRequest("PUT","http://localhost:8080/user", bytes.NewBuffer(str))
if err != nil {
log.Fatalf("error: %v", err)
return
}
resq.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(resq)
if err != nil {
log.Fatalf("did not connect: %v", err)
return
}
defer func(Body io.ReadCloser) {
err := Body.Close()
if err != nil {
log.Fatalf("did not close: %v", err)
return
}
}(res.Body)
if res.StatusCode == 200 {
log.Printf("success")
name, err := ioutil.ReadAll(res.Body)
if err != nil {
log.Fatalf("could not greet: %v", err)
}
log.Printf("Greeting: %s", name)
}
}

结果:

1
2
3
4
5
6
7
8
9
10
11
bash复制代码2021/09/08 16:09:47 Greeting:
{
"status": 10020000,
"resMsg": "请求成功",
"data": {
"update": true
}
}
2021-09-08 16:14:51.785 DEBUG 4380 --- [nio-8080-exec-9] c.l.d.mapper.UserMapper.updateById : ==> Preparing: UPDATE user SET username=?, password=?, tel_new=? WHERE id=? AND deleted=0
2021-09-08 16:14:51.785 DEBUG 4380 --- [nio-8080-exec-9] c.l.d.mapper.UserMapper.updateById : ==> Parameters: 小道科(String), 123456(String), 112(String), 1435514618490388480(Long)
2021-09-08 16:14:51.787 DEBUG 4380 --- [nio-8080-exec-9] c.l.d.mapper.UserMapper.updateById : <== Updates: 1

DELETE请求

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
go复制代码func TestHttp5(t *testing.T) {
resq, err := http.NewRequest("DELETE","http://localhost:8080/user/1435514618490388480",nil)
if err != nil {
log.Fatalf("error: %v", err)
return
}
res, err := http.DefaultClient.Do(resq)
if err != nil {
log.Fatalf("did not connect: %v", err)
return
}
defer func(Body io.ReadCloser) {
err := Body.Close()
if err != nil {
log.Fatalf("did not close: %v", err)
return
}
}(res.Body)
if res.StatusCode == 200 {
log.Printf("success")
name, err := ioutil.ReadAll(res.Body)
if err != nil {
log.Fatalf("could not greet: %v", err)
}
log.Printf("Greeting: %s", name)
}
}

结果:

1
2
3
4
5
6
7
8
9
10
11
bash复制代码2021/09/08 16:14:14 Greeting:
{
"status": 10020000,
"resMsg": "请求成功",
"data": {
"remove": true
}
}
2021-09-08 16:14:14.400 DEBUG 4380 --- [nio-8080-exec-5] c.l.d.mapper.UserMapper.deleteById : ==> Preparing: UPDATE user SET deleted=1 WHERE id=? AND deleted=0
2021-09-08 16:14:14.400 DEBUG 4380 --- [nio-8080-exec-5] c.l.d.mapper.UserMapper.deleteById : ==> Parameters: 1435514618490388480(Long)
2021-09-08 16:14:14.404 DEBUG 4380 --- [nio-8080-exec-5] c.l.d.mapper.UserMapper.deleteById : <== Updates: 1
  1. 如果需要自定义请求头、自定义方法,都可以使用http.NewRequest和 http.DefaultClient.Do(resq)来完成。
  2. 如需设置Header参数,可采用resq.Header.Set("Content-Type", "application/json")(参考POST请求)

每个微服务应用难免会有远程调用,那么在JAVA里面,有很多种远程调用的方法,最基础的手写HTTP调用,或者使用restTetmplate,再到使用openfeign仅仅写个接口就可以实现调用。

那么在Go语言里,Go也提供了Http调用的包net/http,或者使用http组件(gentleman、grequests、heimdall等)或者使用更高级的RPC调用,也有rpc框架(GRPC)。

那么在本章节探讨如何在Go语言里实现HTTP调用。

接口定义

首先进行接口的定义,方便后续进行远程调用。

这里以JAVA为例,以最常见的User实体,来编写相对应的操作。

例如:

1
2
3
4
5
bash复制代码GET 查询列表
GET 查询单个用户
POST 保存用户
PUT 修改单个用户
DELETE 删除单个用户

实体类

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
java复制代码@Data
@ApiModel("用户实体")
@NoArgsConstructor
public class User {
@ApiModelProperty(notes = "用户编号", example = "000001")
private Long id;

@NotEmpty(message = "用户名不能为空")
@ApiModelProperty(notes = "用户名", example = "小欧")
private String username;

@NotEmpty(message = "密码不能为空")
@ApiModelProperty(notes = "密码", example = "123456")
private String password;

@NotEmpty(message = "手机号不能为空")
@ApiModelProperty(notes = "手机号", example = "110")
private String telNew;

@TableLogic
@ApiModelProperty(notes = "是否删除", example = "true")
private Boolean deleted;

public User(Long id, String username, String password, String telNew, Boolean deleted) {
this.id = id;
this.username = username;
this.password = password;
this.telNew = telNew;
this.deleted = deleted;
}
}

接口

其次,编写相应接口

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
java复制代码@Api(tags = "用户管理")
@RestController
@RequestMapping("user")
@RequiredArgsConstructor(onConstructor_ = @Autowired)
public class UserController {

private final UserService userService;

private final IdGen idGen;

@ApiOperation("查看用户列表")
@GetMapping
public Result list(@RequestParam(required = false) Integer page,@RequestParam(required = false) Integer pageSize) {
Page<User> userPage = new Page<>();
userPage.setCurrent(page==null?1:page);
userPage.setSize(pageSize==null?10:pageSize);
Page<User> page1 = userService.page(userPage);
return Result.ok("page", page1);
}

@ApiOperation("获取单个用户信息")
@GetMapping("/{id}")
public Result info(@PathVariable Long id) {
User user = userService.getById(id);
return Result.ok("user", user).put("id", id);
}

@ApiOperation("保存用户信息")
@PostMapping
public Result save(@RequestBody @Validated User user) {
long id = idGen.nextId();
user.setId(id);
boolean save = userService.save(user);
return Result.ok("save", save).put("id",id);
}

@ApiOperation("更新用户信息")
@PutMapping
public Result update(@RequestBody @Validated User user) {
boolean update = userService.updateById(user);
return Result.ok("update", update);
}

@ApiOperation("删除用户信息")
@DeleteMapping("/{id}")
public Result delete(
@PathVariable
@NotNull(message = "不能为空")
Long id) {
boolean remove = userService.removeById(id);
return Result.ok("remove", remove).put("id",id);
}
}

Http请求调用

基本的GET请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
go复制代码// 基本的GET请求
func TestHttp(t *testing.T) {
res, err := http.Get("http://localhost:8080/user")
if err != nil {
log.Fatalf("did not connect: %v", err)
return
}
defer func(Body io.ReadCloser) {
err := Body.Close()
if err != nil {
log.Fatalf("did not close: %v", err)
return
}
}(res.Body)
if res.StatusCode == 200 {
log.Printf("success")
name, err := ioutil.ReadAll(res.Body)
if err != nil {
log.Fatalf("could not greet: %v", err)
}
log.Printf("Greeting: %s", 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
json复制代码2021/09/07 18:17:38 Greeting: 
{
"status":10020000,
"resMsg":"请求成功",
"data":{
"page":{
"records":[
{
"id":1,
"username":"lomtom",
"password":"123345",
"telNew":"110",
"deleted":false
},{
"id":2,
"username":"小菜",
"password":"123456",
"telNew":"112",
"deleted":false
}],
"total":2,
"size":10,
"current":1,
"orders":[],
"hitCount":false,
"searchCount":true,
"pages":1
}
}
}

GET请求 携带参数

若要在GET请求后携带参数,可以选择在URL后面进行拼接,当然也可以采用以下方式。

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
go复制代码// GET请求 携带参数 (除了拼接)
func TestHttp1(t *testing.T) {
params := url.Values{}
Url, err := url.Parse("http://localhost:8080/user")
if err != nil {
return
}
params.Set("page", "2")
params.Set("pageSize", "10")
Url.RawQuery = params.Encode()
urlPath := Url.String()
res, err := http.Get(urlPath)
if err != nil {
log.Fatalf("did not connect: %v", err)
return
}
defer func(Body io.ReadCloser) {
err := Body.Close()
if err != nil {
log.Fatalf("did not close: %v", err)
return
}
}(res.Body)
if res.StatusCode == 200 {
log.Printf("success")
name, err := ioutil.ReadAll(res.Body)
if err != nil {
log.Fatalf("could not greet: %v", err)
}
log.Printf("Greeting: %s", 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
json复制代码2021/09/07 18:17:38 Greeting: 
{
"status": 10020000,
"resMsg": "请求成功",
"data": {
"page": {
"records": [
{
"id": 1,
"username": "lomtom",
"password": "123345",
"telNew": "110",
"deleted": false
},
{
"id": 2,
"username": "小菜",
"password": "123456",
"telNew": "112",
"deleted": false
}
],
"total": 2,
"size": 10,
"current": 1,
"orders": [],
"hitCount": false,
"searchCount": true,
"pages": 1
}
}
}

基本的POST请求(form)

注意:该方法对应的接口接受类型必须也为表单接收,那么在java的接口需要去掉save方法里的@RequestBody注解,否则将调用失败。

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
go复制代码// 基本的POST请求 form
func TestHttp2(t *testing.T) {
params := url.Values{}
params.Set("username", "小小")
params.Set("password", "123456")
params.Set("telNew", "112")
res, err := http.PostForm("http://localhost:8080/user", params)
if err != nil {
log.Fatalf("did not connect: %v", err)
return
}
defer func(Body io.ReadCloser) {
err := Body.Close()
if err != nil {
log.Fatalf("did not close: %v", err)
return
}
}(res.Body)
if res.StatusCode == 200 {
log.Printf("success")
name, err := ioutil.ReadAll(res.Body)
if err != nil {
log.Fatalf("could not greet: %v", err)
}
log.Printf("Greeting: %s", name)
}
}

结果:

1
2
3
4
5
6
7
8
9
10
11
12
bash复制代码2021/09/07 18:22:12 Greeting: 
{
"status": 10020000,
"resMsg": "请求成功",
"data": {
"save": true,
"id": 1435186634277519360
}
}
2021-09-07 18:22:12.748 DEBUG 16028 --- [nio-8080-exec-2] c.l.d.mapper.UserMapper.insert : ==> Preparing: INSERT INTO user ( id, username, password, tel_new ) VALUES ( ?, ?, ?, ? )
2021-09-07 18:22:12.748 DEBUG 16028 --- [nio-8080-exec-2] c.l.d.mapper.UserMapper.insert : ==> Parameters: 1435186634277519360(Long), 小小(String), 123456(String), 112(String)
2021-09-07 18:22:12.752 DEBUG 16028 --- [nio-8080-exec-2] c.l.d.mapper.UserMapper.insert : <== Updates: 1

POST请求(body)

该方法为将参数放在请求的body部分。

  1. 同样需要注意的是,在java的接口需要加上save方法里的@RequestBody注解,否则将调用失败。
  2. 请求参数不能是自定义的结构体,必须是map类型,否则接口将匹配失败,猜测与自定义结构体的转换有关。
  3. 需要设置请求头部参数resq.Header.Set("Content-Type", "application/json")
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
go复制代码// POST请求 body
func TestHttp3(t *testing.T) {
//params := struct {
// username, password, telNew string
//}{"小小", "123456", "112"}
//str, err := json.Marshal(params)
//使用结构体 接口匹配不上

params := make(map[string]interface{})
params["username"] = "小小"
params["password"] = "123456"
params["telNew"] = "112"
str, err := json.Marshal(params)
resq, err := http.NewRequest("POST","http://localhost:8080/user", bytes.NewBuffer(str))
if err != nil {
log.Fatalf("error: %v", err)
return
}
resq.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(resq)
if err != nil {
log.Fatalf("did not connect: %v", err)
return
}
defer func(Body io.ReadCloser) {
err := Body.Close()
if err != nil {
log.Fatalf("did not close: %v", err)
return
}
}(res.Body)
if res.StatusCode == 200 {
log.Printf("success")
name, err := ioutil.ReadAll(res.Body)
if err != nil {
log.Fatalf("could not greet: %v", err)
}
log.Printf("Greeting: %s", name)
}
}

结果:

1
2
3
4
5
6
7
8
9
10
11
12
bash复制代码2021/09/08 16:05:30 Greeting: 
{
"status": 10020000,
"resMsg": "请求成功",
"data": {
"save": true,
"id": 1435514618490388480
}
}
2021-09-08 16:05:30.857 DEBUG 6100 --- [nio-8080-exec-6] c.l.d.mapper.UserMapper.insert : ==> Preparing: INSERT INTO user ( id, username, password, tel_new ) VALUES ( ?, ?, ?, ? )
2021-09-08 16:05:30.873 DEBUG 6100 --- [nio-8080-exec-6] c.l.d.mapper.UserMapper.insert : ==> Parameters: 1435514618490388480(Long), 小小(String), 123456(String), 112(String)
2021-09-08 16:05:30.879 DEBUG 6100 --- [nio-8080-exec-6] c.l.d.mapper.UserMapper.insert : <== Updates: 1

PUT 请求

对于net/http,暂时提供了两种接口Get和Post,那么对于PUT和DELETE的实现,就需要使用它的高级功能了。

  1. 首先使用http.NewRequest申明一个新的请求,设置相对应的请求方式、url、参数等
  2. 再使用http.DefaultClient.Do(resq)发起请求即可
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
go复制代码// PUT 请求
func TestHttp4(t *testing.T) {
//params := struct {
// id,username, password, telNew string
//}{"1435514618490388480""小小", "123456", "112"}
//str, err := json.Marshal(params)
//使用结构体 接口匹配不上

params := make(map[string]interface{})
params["id"] = "1435514618490388480"
params["username"] = "小道科"
params["password"] = "123456"
params["telNew"] = "112"
str, err := json.Marshal(params)
resq, err := http.NewRequest("PUT","http://localhost:8080/user", bytes.NewBuffer(str))
if err != nil {
log.Fatalf("error: %v", err)
return
}
resq.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(resq)
if err != nil {
log.Fatalf("did not connect: %v", err)
return
}
defer func(Body io.ReadCloser) {
err := Body.Close()
if err != nil {
log.Fatalf("did not close: %v", err)
return
}
}(res.Body)
if res.StatusCode == 200 {
log.Printf("success")
name, err := ioutil.ReadAll(res.Body)
if err != nil {
log.Fatalf("could not greet: %v", err)
}
log.Printf("Greeting: %s", name)
}
}

结果:

1
2
3
4
5
6
7
8
9
10
11
bash复制代码2021/09/08 16:09:47 Greeting:
{
"status": 10020000,
"resMsg": "请求成功",
"data": {
"update": true
}
}
2021-09-08 16:14:51.785 DEBUG 4380 --- [nio-8080-exec-9] c.l.d.mapper.UserMapper.updateById : ==> Preparing: UPDATE user SET username=?, password=?, tel_new=? WHERE id=? AND deleted=0
2021-09-08 16:14:51.785 DEBUG 4380 --- [nio-8080-exec-9] c.l.d.mapper.UserMapper.updateById : ==> Parameters: 小道科(String), 123456(String), 112(String), 1435514618490388480(Long)
2021-09-08 16:14:51.787 DEBUG 4380 --- [nio-8080-exec-9] c.l.d.mapper.UserMapper.updateById : <== Updates: 1

DELETE请求

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
go复制代码func TestHttp5(t *testing.T) {
resq, err := http.NewRequest("DELETE","http://localhost:8080/user/1435514618490388480",nil)
if err != nil {
log.Fatalf("error: %v", err)
return
}
res, err := http.DefaultClient.Do(resq)
if err != nil {
log.Fatalf("did not connect: %v", err)
return
}
defer func(Body io.ReadCloser) {
err := Body.Close()
if err != nil {
log.Fatalf("did not close: %v", err)
return
}
}(res.Body)
if res.StatusCode == 200 {
log.Printf("success")
name, err := ioutil.ReadAll(res.Body)
if err != nil {
log.Fatalf("could not greet: %v", err)
}
log.Printf("Greeting: %s", name)
}
}

结果:

1
2
3
4
5
6
7
8
9
10
11
bash复制代码2021/09/08 16:14:14 Greeting:
{
"status": 10020000,
"resMsg": "请求成功",
"data": {
"remove": true
}
}
2021-09-08 16:14:14.400 DEBUG 4380 --- [nio-8080-exec-5] c.l.d.mapper.UserMapper.deleteById : ==> Preparing: UPDATE user SET deleted=1 WHERE id=? AND deleted=0
2021-09-08 16:14:14.400 DEBUG 4380 --- [nio-8080-exec-5] c.l.d.mapper.UserMapper.deleteById : ==> Parameters: 1435514618490388480(Long)
2021-09-08 16:14:14.404 DEBUG 4380 --- [nio-8080-exec-5] c.l.d.mapper.UserMapper.deleteById : <== Updates: 1
  1. 如果需要自定义请求头、自定义方法,都可以使用http.NewRequest和 http.DefaultClient.Do(resq)来完成。
  2. 如需设置Header参数,可采用resq.Header.Set("Content-Type", "application/json")(参考POST请求)

每个微服务应用难免会有远程调用,那么在JAVA里面,有很多种远程调用的方法,最基础的手写HTTP调用,或者使用restTetmplate,再到使用openfeign仅仅写个接口就可以实现调用。

那么在Go语言里,Go也提供了Http调用的包net/http,或者使用http组件(gentleman、grequests、heimdall等)或者使用更高级的RPC调用,也有rpc框架(GRPC)。

那么在本章节探讨如何在Go语言里实现HTTP调用。

接口定义

首先进行接口的定义,方便后续进行远程调用。

这里以JAVA为例,以最常见的User实体,来编写相对应的操作。

例如:

1
2
3
4
5
bash复制代码GET 查询列表
GET 查询单个用户
POST 保存用户
PUT 修改单个用户
DELETE 删除单个用户

实体类

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
java复制代码@Data
@ApiModel("用户实体")
@NoArgsConstructor
public class User {
@ApiModelProperty(notes = "用户编号", example = "000001")
private Long id;

@NotEmpty(message = "用户名不能为空")
@ApiModelProperty(notes = "用户名", example = "小欧")
private String username;

@NotEmpty(message = "密码不能为空")
@ApiModelProperty(notes = "密码", example = "123456")
private String password;

@NotEmpty(message = "手机号不能为空")
@ApiModelProperty(notes = "手机号", example = "110")
private String telNew;

@TableLogic
@ApiModelProperty(notes = "是否删除", example = "true")
private Boolean deleted;

public User(Long id, String username, String password, String telNew, Boolean deleted) {
this.id = id;
this.username = username;
this.password = password;
this.telNew = telNew;
this.deleted = deleted;
}
}

接口

其次,编写相应接口

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
java复制代码@Api(tags = "用户管理")
@RestController
@RequestMapping("user")
@RequiredArgsConstructor(onConstructor_ = @Autowired)
public class UserController {

private final UserService userService;

private final IdGen idGen;

@ApiOperation("查看用户列表")
@GetMapping
public Result list(@RequestParam(required = false) Integer page,@RequestParam(required = false) Integer pageSize) {
Page<User> userPage = new Page<>();
userPage.setCurrent(page==null?1:page);
userPage.setSize(pageSize==null?10:pageSize);
Page<User> page1 = userService.page(userPage);
return Result.ok("page", page1);
}

@ApiOperation("获取单个用户信息")
@GetMapping("/{id}")
public Result info(@PathVariable Long id) {
User user = userService.getById(id);
return Result.ok("user", user).put("id", id);
}

@ApiOperation("保存用户信息")
@PostMapping
public Result save(@RequestBody @Validated User user) {
long id = idGen.nextId();
user.setId(id);
boolean save = userService.save(user);
return Result.ok("save", save).put("id",id);
}

@ApiOperation("更新用户信息")
@PutMapping
public Result update(@RequestBody @Validated User user) {
boolean update = userService.updateById(user);
return Result.ok("update", update);
}

@ApiOperation("删除用户信息")
@DeleteMapping("/{id}")
public Result delete(
@PathVariable
@NotNull(message = "不能为空")
Long id) {
boolean remove = userService.removeById(id);
return Result.ok("remove", remove).put("id",id);
}
}

Http请求调用

基本的GET请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
go复制代码// 基本的GET请求
func TestHttp(t *testing.T) {
res, err := http.Get("http://localhost:8080/user")
if err != nil {
log.Fatalf("did not connect: %v", err)
return
}
defer func(Body io.ReadCloser) {
err := Body.Close()
if err != nil {
log.Fatalf("did not close: %v", err)
return
}
}(res.Body)
if res.StatusCode == 200 {
log.Printf("success")
name, err := ioutil.ReadAll(res.Body)
if err != nil {
log.Fatalf("could not greet: %v", err)
}
log.Printf("Greeting: %s", 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
json复制代码2021/09/07 18:17:38 Greeting: 
{
"status":10020000,
"resMsg":"请求成功",
"data":{
"page":{
"records":[
{
"id":1,
"username":"lomtom",
"password":"123345",
"telNew":"110",
"deleted":false
},{
"id":2,
"username":"小菜",
"password":"123456",
"telNew":"112",
"deleted":false
}],
"total":2,
"size":10,
"current":1,
"orders":[],
"hitCount":false,
"searchCount":true,
"pages":1
}
}
}

GET请求 携带参数

若要在GET请求后携带参数,可以选择在URL后面进行拼接,当然也可以采用以下方式。

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
go复制代码// GET请求 携带参数 (除了拼接)
func TestHttp1(t *testing.T) {
params := url.Values{}
Url, err := url.Parse("http://localhost:8080/user")
if err != nil {
return
}
params.Set("page", "2")
params.Set("pageSize", "10")
Url.RawQuery = params.Encode()
urlPath := Url.String()
res, err := http.Get(urlPath)
if err != nil {
log.Fatalf("did not connect: %v", err)
return
}
defer func(Body io.ReadCloser) {
err := Body.Close()
if err != nil {
log.Fatalf("did not close: %v", err)
return
}
}(res.Body)
if res.StatusCode == 200 {
log.Printf("success")
name, err := ioutil.ReadAll(res.Body)
if err != nil {
log.Fatalf("could not greet: %v", err)
}
log.Printf("Greeting: %s", 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
json复制代码2021/09/07 18:17:38 Greeting: 
{
"status": 10020000,
"resMsg": "请求成功",
"data": {
"page": {
"records": [
{
"id": 1,
"username": "lomtom",
"password": "123345",
"telNew": "110",
"deleted": false
},
{
"id": 2,
"username": "小菜",
"password": "123456",
"telNew": "112",
"deleted": false
}
],
"total": 2,
"size": 10,
"current": 1,
"orders": [],
"hitCount": false,
"searchCount": true,
"pages": 1
}
}
}

基本的POST请求(form)

注意:该方法对应的接口接受类型必须也为表单接收,那么在java的接口需要去掉save方法里的@RequestBody注解,否则将调用失败。

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
go复制代码// 基本的POST请求 form
func TestHttp2(t *testing.T) {
params := url.Values{}
params.Set("username", "小小")
params.Set("password", "123456")
params.Set("telNew", "112")
res, err := http.PostForm("http://localhost:8080/user", params)
if err != nil {
log.Fatalf("did not connect: %v", err)
return
}
defer func(Body io.ReadCloser) {
err := Body.Close()
if err != nil {
log.Fatalf("did not close: %v", err)
return
}
}(res.Body)
if res.StatusCode == 200 {
log.Printf("success")
name, err := ioutil.ReadAll(res.Body)
if err != nil {
log.Fatalf("could not greet: %v", err)
}
log.Printf("Greeting: %s", name)
}
}

结果:

1
2
3
4
5
6
7
8
9
10
11
12
bash复制代码2021/09/07 18:22:12 Greeting: 
{
"status": 10020000,
"resMsg": "请求成功",
"data": {
"save": true,
"id": 1435186634277519360
}
}
2021-09-07 18:22:12.748 DEBUG 16028 --- [nio-8080-exec-2] c.l.d.mapper.UserMapper.insert : ==> Preparing: INSERT INTO user ( id, username, password, tel_new ) VALUES ( ?, ?, ?, ? )
2021-09-07 18:22:12.748 DEBUG 16028 --- [nio-8080-exec-2] c.l.d.mapper.UserMapper.insert : ==> Parameters: 1435186634277519360(Long), 小小(String), 123456(String), 112(String)
2021-09-07 18:22:12.752 DEBUG 16028 --- [nio-8080-exec-2] c.l.d.mapper.UserMapper.insert : <== Updates: 1

POST请求(body)

该方法为将参数放在请求的body部分。

  1. 同样需要注意的是,在java的接口需要加上save方法里的@RequestBody注解,否则将调用失败。
  2. 请求参数不能是自定义的结构体,必须是map类型,否则接口将匹配失败,猜测与自定义结构体的转换有关。
  3. 需要设置请求头部参数resq.Header.Set("Content-Type", "application/json")
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
go复制代码// POST请求 body
func TestHttp3(t *testing.T) {
//params := struct {
// username, password, telNew string
//}{"小小", "123456", "112"}
//str, err := json.Marshal(params)
//使用结构体 接口匹配不上

params := make(map[string]interface{})
params["username"] = "小小"
params["password"] = "123456"
params["telNew"] = "112"
str, err := json.Marshal(params)
resq, err := http.NewRequest("POST","http://localhost:8080/user", bytes.NewBuffer(str))
if err != nil {
log.Fatalf("error: %v", err)
return
}
resq.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(resq)
if err != nil {
log.Fatalf("did not connect: %v", err)
return
}
defer func(Body io.ReadCloser) {
err := Body.Close()
if err != nil {
log.Fatalf("did not close: %v", err)
return
}
}(res.Body)
if res.StatusCode == 200 {
log.Printf("success")
name, err := ioutil.ReadAll(res.Body)
if err != nil {
log.Fatalf("could not greet: %v", err)
}
log.Printf("Greeting: %s", name)
}
}

结果:

1
2
3
4
5
6
7
8
9
10
11
12
bash复制代码2021/09/08 16:05:30 Greeting: 
{
"status": 10020000,
"resMsg": "请求成功",
"data": {
"save": true,
"id": 1435514618490388480
}
}
2021-09-08 16:05:30.857 DEBUG 6100 --- [nio-8080-exec-6] c.l.d.mapper.UserMapper.insert : ==> Preparing: INSERT INTO user ( id, username, password, tel_new ) VALUES ( ?, ?, ?, ? )
2021-09-08 16:05:30.873 DEBUG 6100 --- [nio-8080-exec-6] c.l.d.mapper.UserMapper.insert : ==> Parameters: 1435514618490388480(Long), 小小(String), 123456(String), 112(String)
2021-09-08 16:05:30.879 DEBUG 6100 --- [nio-8080-exec-6] c.l.d.mapper.UserMapper.insert : <== Updates: 1

PUT 请求

对于net/http,暂时提供了两种接口Get和Post,那么对于PUT和DELETE的实现,就需要使用它的高级功能了。

  1. 首先使用http.NewRequest申明一个新的请求,设置相对应的请求方式、url、参数等
  2. 再使用http.DefaultClient.Do(resq)发起请求即可
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
go复制代码// PUT 请求
func TestHttp4(t *testing.T) {
//params := struct {
// id,username, password, telNew string
//}{"1435514618490388480""小小", "123456", "112"}
//str, err := json.Marshal(params)
//使用结构体 接口匹配不上

params := make(map[string]interface{})
params["id"] = "1435514618490388480"
params["username"] = "小道科"
params["password"] = "123456"
params["telNew"] = "112"
str, err := json.Marshal(params)
resq, err := http.NewRequest("PUT","http://localhost:8080/user", bytes.NewBuffer(str))
if err != nil {
log.Fatalf("error: %v", err)
return
}
resq.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(resq)
if err != nil {
log.Fatalf("did not connect: %v", err)
return
}
defer func(Body io.ReadCloser) {
err := Body.Close()
if err != nil {
log.Fatalf("did not close: %v", err)
return
}
}(res.Body)
if res.StatusCode == 200 {
log.Printf("success")
name, err := ioutil.ReadAll(res.Body)
if err != nil {
log.Fatalf("could not greet: %v", err)
}
log.Printf("Greeting: %s", name)
}
}

结果:

1
2
3
4
5
6
7
8
9
10
11
bash复制代码2021/09/08 16:09:47 Greeting:
{
"status": 10020000,
"resMsg": "请求成功",
"data": {
"update": true
}
}
2021-09-08 16:14:51.785 DEBUG 4380 --- [nio-8080-exec-9] c.l.d.mapper.UserMapper.updateById : ==> Preparing: UPDATE user SET username=?, password=?, tel_new=? WHERE id=? AND deleted=0
2021-09-08 16:14:51.785 DEBUG 4380 --- [nio-8080-exec-9] c.l.d.mapper.UserMapper.updateById : ==> Parameters: 小道科(String), 123456(String), 112(String), 1435514618490388480(Long)
2021-09-08 16:14:51.787 DEBUG 4380 --- [nio-8080-exec-9] c.l.d.mapper.UserMapper.updateById : <== Updates: 1

DELETE请求

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
go复制代码func TestHttp5(t *testing.T) {
resq, err := http.NewRequest("DELETE","http://localhost:8080/user/1435514618490388480",nil)
if err != nil {
log.Fatalf("error: %v", err)
return
}
res, err := http.DefaultClient.Do(resq)
if err != nil {
log.Fatalf("did not connect: %v", err)
return
}
defer func(Body io.ReadCloser) {
err := Body.Close()
if err != nil {
log.Fatalf("did not close: %v", err)
return
}
}(res.Body)
if res.StatusCode == 200 {
log.Printf("success")
name, err := ioutil.ReadAll(res.Body)
if err != nil {
log.Fatalf("could not greet: %v", err)
}
log.Printf("Greeting: %s", name)
}
}

结果:

1
2
3
4
5
6
7
8
9
10
11
bash复制代码2021/09/08 16:14:14 Greeting:
{
"status": 10020000,
"resMsg": "请求成功",
"data": {
"remove": true
}
}
2021-09-08 16:14:14.400 DEBUG 4380 --- [nio-8080-exec-5] c.l.d.mapper.UserMapper.deleteById : ==> Preparing: UPDATE user SET deleted=1 WHERE id=? AND deleted=0
2021-09-08 16:14:14.400 DEBUG 4380 --- [nio-8080-exec-5] c.l.d.mapper.UserMapper.deleteById : ==> Parameters: 1435514618490388480(Long)
2021-09-08 16:14:14.404 DEBUG 4380 --- [nio-8080-exec-5] c.l.d.mapper.UserMapper.deleteById : <== Updates: 1
  1. 如果需要自定义请求头、自定义方法,都可以使用http.NewRequest和 http.DefaultClient.Do(resq)来完成。
  2. 如需设置Header参数,可采用resq.Header.Set("Content-Type", "application/json")(参考POST请求)

本文转载自: 掘金

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

在 C++ 中标记字符串与getline() 函数和字符数组

发表于 2021-11-27

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

在 C++ 中标记字符串

对字符串进行标记表示根据某些分隔符拆分字符串。有很多方法可以对字符串进行标记。在这篇文章中解释了其中的四个:

使用字符串流

一个字符串流与允许你从字符串,就好像它是一个流中读取流的字符串对象关联。

下面是 C++ 实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
c++复制代码#include <bits/stdc++.h>

using namespace std;

int main()
{

string line = "juejin is a must try";
vector <string> tokens;
stringstream check1(line);
string intermediate;
while(getline(check1, intermediate, ' ')){
tokens.push_back(intermediate);
}

for(int i = 0; i < tokens.size(); i++)
cout << tokens[i] << '\n';
}

输出

1
2
3
4
5
c++复制代码juejin
is
a
must
try

使用strtok()

1
2
c++复制代码// 根据给定的分隔符拆分 string[]。 并返回下一个令牌。 它需要在循环中调用以获取所有令牌。 当没有更多标记时,它返回 NULL。
char * strtok(char str[], const char *delims);

下面是 C++ 实现:

1
2
3
4
5
6
7
8
9
10
11
12
c++复制代码#include <stdio.h>
#include <string.h>

int main(){
char str[] = "juejin-for-juejin";
char *token = strtok(str, "-");
while (token != NULL){
printf("%s\n", token);
token = strtok(NULL, "-");
}
return 0;
}

输出

1
2
3
c++复制代码juejin
for
juejin

strtok() 的另一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
c++复制代码#include <string.h>
#include <stdio.h>

int main(){
char gfg[100] = " juejin - for - juejin - Contribute";
const char s[4] = "-";
char* tok;
tok = strtok(gfg, s);
while (tok != 0) {
printf(" %s\n", tok);
tok = strtok(0, s);
}
return (0);
}

输出

1
2
3
4
c++复制代码juejin 
for
juejin
Contribute

使用strtok_r()

就像 C 中的 strtok() 函数一样,strtok_r() 执行相同的任务,将字符串解析为标记序列。strtok_r() 是 strtok() 的可重入版本。

我们可以通过两种方式调用 strtok_r()

下面是一个简单的 C++ 程序来展示 strtok_r() 的使用:

1
2
3
4
5
6
7
8
9
10
11
12
c++复制代码#include<stdio.h>
#include<string.h>

int main(){
char str[] = "juejin for juejin";
char *token;
char *rest = str;
while ((token = strtok_r(rest, " ", &rest)))
printf("%s\n", token);

return(0);
}

输出

1
2
3
c++复制代码juejin
for
juejin

使用 std::sregex_token_iterator

在这种方法中,标记化是在正则表达式匹配的基础上完成的。更适合需要多个分隔符的用例。

下面是一个简单的 C++ 程序,用于展示 std::sregex_token_iterator 的使用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
c++复制代码#include <iostream>
#include <regex>
#include <string>
#include <vector>

std::vector<std::string> tokenize(const std::string str,const std::regex re){
std::sregex_token_iterator it{ str.begin(),str.end(), re, -1 };
std::vector<std::string> tokenized{ it, {} };
tokenized.erase(std::remove_if(tokenized.begin(),tokenized.end(),[](std::string const& s) {
return s.size() == 0;
}),
tokenized.end());
return tokenized;
}


int main(){
const std::string str = "将 字符串 分隔,为,空格,和逗号";
const std::regex re(R"([\s|,]+)");
const std::vector<std::string> tokenized = tokenize(str, re);
for (std::string token : tokenized) std::cout << token << std::endl;
return 0;
}

输出

1
2
3
4
5
6
c++复制代码将
字符串
分隔
为
空格
和逗号

getline() 函数和字符数组

在C++中,流类支持面​​向行的函数,getline()和write()分别执行输入和输出功能。getline() 函数读取以新行结尾或直到达到最大限制的整行文本。getline() 是 istream 类的成员函数,语法如下:

1
2
3
4
5
c++复制代码//(缓冲区,流大小,分隔符)
istream& getline(char*, int size, char='\n')

// 分隔符被视为'\n'
istream& getline(char*, int size)

该函数执行以下操作:

  1. 提取字符直到分隔符。
  2. 将字符存储在缓冲区中。
  3. 提取的最大字符数为 size – 1。

注意终止符(或分隔符)可以是任何字符(如’ ‘、’、’ 或任何特殊字符等)。终止符被读取但不保存到缓冲区中,而是由空字符替换。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
c++复制代码// 用字符数组显示 getline() 的 C++ 程序
#include <iostream>
using namespace std;

int main()
{
char str[20];
cout << "Enter Your Name::";

// 查看 getline() 与数组 str 的使用也将上述语句替换为 cin >> str 并查看输出的差异
cin.getline(str, 20);

cout << "\n你的名字是: " << str;
return 0;
}

输入 :

1
c++复制代码鲸落

输出 :

1
c++复制代码你的名字是: 鲸落

在上面的程序中,语句 cin.getline(str, 20) 读取字符串,直到遇到换行符或最大字符数(此处为 20)。尝试具有不同限制的函数并查看输出。

本文转载自: 掘金

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

Tomcat

发表于 2021-11-27

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

Tomcat介绍

先简单介绍一下服务器,服务器的概念非常广泛,可以说服务器就是一台特殊的计算机,也可以说其指代用于部署网站的应用。在JavaWeb中的服务器其实是web服务器,其本质是一个软件,作用就是发布我们的应用,用用户可以通过浏览器来访问我们的应用。

常见的应用服务器有:

服务器名称 说明
weblogic 实现了javaEE规范,重量级服务器,又称为javaEE容器
websphereAS 实现了javaEE规范,重量级服务器。
JBOSSAS 实现了JavaEE规范,重量级服务器。免费的。
Tomcat 实现了jsp/servlet规范,是一个轻量级服务器,开源免费。

Tomcat下载与安装

官网地址为:tomcat.apache.org/,打开后是这样的:

1637993329323.png
我们在左边第二个大选项卡就可以选择下载的版本了,此处我下载的是9,下载之后直接解压到我们的指定地址就ok了。

解压完后目录:

1637993652655.png

Tomcat基本使用

1)启动和停止

启动和停止文件都在bin目录下,启动为startup.bat / startup.sh 分别对应Windows运行程序和linux运行程序,停止为shutdown.bat / shutdown.sh。

注意,在启动之前我们必须已经配置完成Java_Home的环境变量。

双击startup.bat,显示如图:

1637994226081.png
然后打开浏览器,输入:localhost:8080 就可以看到:

1637994338692.png
如果无法显示,可能但不限于的原因:

  1. Java环境变量没有配置
  2. 端口8080被占用

上面的cmd显示的是乱码,如何解决?

找到conf下的logging.properties文件,将java.util.logging.ConsoleHandler.encoding = UTF-8这行设置为java.util.logging.ConsoleHandler.encoding = gbk,重启服务即可。

2)IDEA集成Tomcat服务器

点击编辑配置:

1637994735039.png
找到模板下的tomcat server ,点击local,进入配置之前安装的tomcat路径即可:

1637994827947.png

此时如果配置成功,那么新建一个JavaEE项目就能在左下角找到这个图标:

1637995078398.png

3)Tomcat发布应用-JavaWeb应用

首先创建一个JavaWeb应用,在idea中new moudle :

1637995664270.png

此处JavaEE版本选择7,并且勾选下方的web application,自己选择版本即可。

JavaWeb应用目录结构详解

web项目创建完成,此时和平时的Java项目所不同的是多了一个web文件夹,web文件夹下可以直接存放我们的css或者html以及jsp文件,在WEB-INF文件夹中存放的则是配置文件。

JavaWeb应用的部署

第一步

1637996353910.png

第二步

1637996557265.png
第三步

1637996642086.png
直接启动即可。

打开为:

1637996825634.png
还可以改变项目访问路径:

1637996916057.png

显示界面的内容可以通过修改index.jsp实现。

war包发布

第一步:使用jar -cvf war 包的名称 当前目录中哪些资源要打入war****

第二步:把打好的war拷贝到tomcat的webapps目录中

第三步:启动服务时,tomcat会自动解压。

配置默认端口

Tomcat服务器主配置文件中配置着访问端口,它在配置文件中写的值是:8080。但是它不是Tomcat的默认端口。我们此处说的默认端口是,当使用默认值的时候,端口号可以省略。Tomcat的默认端口是80。

配置方式如下:

1
ini复制代码<Connector port="80" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" />

Tomcat默认打开浏览器的端口为8080,HTTP的默认端口为80,在访问Tomcat服务的时候必须指定8080端口或者修改端口号为80.

修改路径是conf文件夹下的server.xml,下面是这个文件的详解(来自黑马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
xml复制代码<?xml version="1.0" encoding="UTF-8"?>
<!--代表整个Servlet容器组件,是最顶层元素。它里面可以有多个Service标签-->
<Server port="8005" shutdown="SHUTDOWN">
   <!--Tomcat运行所需的监听器-->
   <!--默认的生命周期监听器:在server初始化之前打印操作系统、JVM及服务器的版本信息-->
   <Listener className="org.apache.catalina.startup.VersionLoggerListener" />
   <!-- 默认的生命周期监听器:在server初始化之前加载APR库,并在server停止后销毁 -->
   <Listener className="org.apache.catalina.core.AprLifecycleListener" SSLEngine="on" />
   <!-- 默认的生命周期监听器:server初始化之前调用,解决单例对象创建导致的jVM内存泄露问题以及锁文件问题 -->
   <Listener className="org.apache.catalina.core.JreMemoryLeakPreventionListener" />
   <!-- 默认的生命周期监听器:Server启动时将JNDI资源注册为MBean进行管理 -->
   <Listener className="org.apache.catalina.mbeans.GlobalResourcesLifecycleListener" />
   <!-- 默认的生命周期监听器:在context停止时重建Executor池中的线程,避免内存泄露 -->
   <Listener className="org.apache.catalina.core.ThreadLocalLeakPreventionListener" />
   <!--全局资源配置, 解析到此节点时开始创建JNDI上下文 -->
   <GlobalNamingResources>
       <Resource name="UserDatabase" auth="Container"
                 type="org.apache.catalina.UserDatabase"
                 description="User database that can be updated and saved"
                 factory="org.apache.catalina.users.MemoryUserDatabaseFactory"
                 pathname="conf/tomcat-users.xml" />
   </GlobalNamingResources>
   <!--配置的是Tomcat的服务包含<Engine>元素和<Connector>元素。其中<Connector>可以是一个,也可以是多个,且他们共享引擎<Engine>的配置-->
   <Service name="Catalina">
       <!-- 配置共享线程池:默认的线程池实现为StandardThreadExecutor,可以通过标签的className属性指定Catalina共享线程池的级别为Service,默认情况下不配置共享线程池-->
       <!-- <Executor name="tomcatThreadPool" namePrefix="catalina-exec-" maxThreads="150" minSpareThreads="4"/>-->
​
       <!--配置的是连接器,其中包括连接的端口和使用协议
           port:指定端口号
           protocol:指定协议名称
           connectionTimeout:连接超时时间
           redirectPort:当资源必须以https协议访问时,tomcat会重定向此端口访问。
       -->
       <Connector port="8080" protocol="HTTP/1.1"  connectionTimeout="20000" redirectPort="8443" />
       <Connector port="8009" protocol="AJP/1.3" redirectPort="8443" />
       <!--配置引擎
           name:引擎名称
           defaultHost:虚拟主机的引用-->
       <Engine name="Catalina" defaultHost="localhost">
           <!--配置Tomcat的Realm域,安全空间-->
           <Realm className="org.apache.catalina.realm.LockOutRealm">
               <Realm className="org.apache.catalina.realm.UserDatabaseRealm" resourceName="UserDatabase"/>
           </Realm>
           <!--配置虚拟主机。
                  name:主机名称
                  appBase:应用默认发布目录
                  unpackWARs:是否自动解压war包
                  autoDeploy:是否自动发布-->
           <Host name="localhost"  appBase="webapps" unpackWARs="true" autoDeploy="true">
               <!-- 日志的valve拦截 -->
               <Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs"
                      prefix="localhost_access_log" suffix=".txt"
                      pattern="%h %l %u %t &quot;%r&quot; %s %b" />
               <!--配置具体应用项目。
                   docBase是项目的位置,可以是绝对路径,也可以是相对路径。如果是相对路径,则必须发布在虚拟主机的应用默认发布目录下。
                   path是指定访问该Web应用的URI
                   reloadable是指定是否重新加载/WEB-INF/classes和/WEB-INF/lib目录下改动后的内容-->
               <!--<Context docBase="tomcat" path="/tomcat" reloadable="true"/>-->
           </Host>
       </Engine>
   </Service>
​
</Server>

Tomcat配置虚拟目录

虚拟目录的配置,支持两种方式。第一种是通过在主配置文件中添加标签实现。第二种是通过写一个独立配置文件实现。

第一种方式:在server.xml的<Host>元素中加一个<Context path="" docBase=""/>元素。 ​ path:访问资源URI。URI名称可以随便起,但是必须在前面加上一个/ ​ docBase:资源所在的磁盘物理地址。 第二种方式:是写一个独立的xml文件,该文件名可以随便起。在文件内写一个<Context/>元素。 ​ 该文件要放在Tomcat目录中的conf\Catalina\localhost目录下。 需要注意的是,在使用了独立的配置文件之后,访问资源URI就变成了 /+文件的名称。而Context的path属性就失效了。

Tomcat配置虚拟主机

在<Engine>元素中添加一个<Host name="" appBase="" unparkWARs="" autoDeploy="" />,其中: ​ name:指定主机的名称 ​ appBase:当前主机的应用发布目录 ​ unparkWARs:启动时是否自动解压war包 ​ autoDeploy:是否自动发布

配置示例如下:

1
2
3
ini复制代码<Host name="www.itcast.cn" appBase="D:\itcastapps" unpackWARs="true" autoDeploy="true"/>
​
<Host name="www.itheima.com" appBase="D:\itheimaapps" unpackWARs="true" autoDeploy="true"/>

本文转载自: 掘金

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

1…153154155…956

开发者博客

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