0%

MySQL创建表的时候可以不设置主键吗?

MySQL创建表的时候是可以不主动设置主键的,但是表是一定需要一个主键的,MySQL会主动将第一个不为null的唯一索引设置为主键

为什么MySQL推荐使用自增id作为主键?

mysql官方推荐不要使用uuid或者不连续不重复的雪花作为主键,而是使用连续自增的主键id

使用自增id的内部结构

自增id的值是顺序的,所以innodb在索引B+树的叶子节点层面可以直接把每一条记录都存储在上一条记录的后面,当达到页面的最大填充因子的时候(页面容量已经满了)下一条记录就会写入新的页中,数据按照这种顺序的方式进行填充,主键页就会以近乎于顺序的记录填满,提升了页面的最大填充率,不会有页的浪费

新插入的行一定会在原有的最大数据行下一行,MySQL定位和寻址非常快,不会因为计算而做出额外的消耗,

并且能够减少页分裂和碎片的产生

页分裂:保证后一个数据页的所有行主键值比前一个数据页的主键值大,所以当ID不为自增的主键的时候,就会导致后一个页的所有行并不一定比前一个数据页的行的id大。这时就会触发页分裂的逻辑,对两个页之间的数据进行调整,甚至会涉及到多个数据页,导致性能降低

使用自增id的缺点

  1. 别人一旦爬取你的数据库,就可以根据数据库的自增id获取到你业务的增长信息,从而分析出经营情况
  2. 对于高并发的负载,innodb在按照主键进行插入的时候会造成明显的锁争用,auto_increment锁机制会造成自增锁的争夺,有一定的性能损失

为什么分布式系统不用自增id,而是要用雪花算法生成id

分布式id创建的业务需求

  1. 全局唯一
  2. 趋势递增 innodb引擎的叶子结点是有序的双向链表,趋势递增可以增加性能,不会打乱树的结构
  3. 信息安全
  4. 最好包含时间戳

为什么自增id不适合分布式系统?

当数据庞大的时候,在数据库分库分表之后,数据库自增id不能满足唯一id来表示数据;因为每个表都按照自己的节奏自增,会造成id冲突,从而无法满足需求

使用auto_increment实现廉价的分布式唯一主键

flickr有类似的方案,构建是一个专用的数据库服务器,上面只有一个数据库,在数据库里面有用于32位id和64位id的id表,id是auto自增的,所有数据库生成id都会向这个服务器发请求,然后服务器分发id下去,也能达到一种分布式唯一主键的效果

类似于session-redis的思想,把所有的sessionid都存在redis里面,所有的服务器实例在比较cookie的时候就先去redis里面比较,这样就能避免因为负载均衡导致的cookie失效问题

当然这个廉价的做法显然是有很大问题的

  1. 并发量很小,因为只有一台服务器
  2. 增加开销,并且整个请求流程变慢,因为需要向服务器发请求,并且是在硬盘层面进行操作的
  3. flickr服务器成了整个系统的瓶颈和隐患,如果服务器宕机整个系统直接崩掉了

雪花算法

是twitter开源的分布式id生成算法,结果是一个64位的longint类型,核心思想是用41位来作为时间戳,10位来作为机器的id,12位作为毫秒内的流水号(意味着每个节点可以在每毫秒生成4096个id),最后还有一个符号为永远为0

优点

  • 完全在内存生成,高性能高可用
  • 容量大,每秒可以生成几百万id
  • 趋势递增,插入数据库索引树的时候,性能比较高

缺点

  • 依赖系统时钟的一致性,如果某台机器的系统时钟回拨,有可能造成id冲突
  • 多台机器的ID只能保证趋势增加,即每一台机器都能保证这台机器生成的ID是在增加的,但是多台机器并不一定绝对递增
  • 41位时间戳只能保证69年无重复ID
  • 因为是64位的ID,在传递给前端的时候需要用字符串的类型进行传递,因为js的number类型最大只支持53位

其他分布式ID方案

  • UUID:JAVA自带的API,生成一个唯一性的字符串,不能保证有序递增
  • UidGenerator:百度开源的分布式ID生成器,基于雪花算法
  • Leaf:美团开源的分布式ID生成器,能保证全局唯一,趋势递增,但是需要依赖关系数据库、Zookeeper等中间件

中台是一个非常宽泛的概念,可以是技术平台,比如框架、devops平台、容器云之类的,也可以是业务平台,像用户中心、订单中心、各种微服务集散。

为什么需要中台?

中台的产生可能是高层的战略设计,也可能是公司不断膨胀而暴露出来的种种问题需要被解决。

中台一开始诞生于16年阿里提出的“大中台,小前台”概念,中台的价值是以快速响应需求为依切,中台并不是一开始就有的,而是基于“前台+后台”的架构发展演变的

前台:系统的前端平台,是直接与终端用户进行交互的应用层,比如日常使用的app,h5(html5 通常用来写移动端的前端和web的前端),pc端都是属于前台

后台:后台是指系统的后端平台,终端用户无法感知,提供后端服务

因为用户的需求是会高速变化的,而用户需求的变化决定了前台系统需要快速迭代响应用户需求,而前端的变化又需要后端的变化来进行支撑,这就对后台的快速应变提出了要求,而后台的核心目的并不是服务于前台,而是提升后端数据的安全及系统的管理效率

于是就产生了“前端为了用户需求,期望系统不断的快速迭代”与“后段为了数据安全与系统稳定,期望系统趋于稳定”的矛盾局面,为了解决这一矛盾,架构师们创造性的提出了“中台”这一概念,核心就是将后台的逻辑层进行拆离,形成前端(应用层)-中台(逻辑层)-后台(数据层)的产品架构,在这一架构下,当前端需求出现时,中台能够快速的进行响应,从而提升了研发效率,降低了创新成本

阿里中台战略最早从业务中台和数据中台建设开始,采用了双中台的建设模式,到后来发展出了移动中台,技术中台和研发中台等等。

img

业务中台

一般是指支持企业线上核心业务的中台,承载了核心业务能力,实现企业级业务能力复用和各业务板块之间的联通和协同

数据中台

与业务中台相辅相成,共同支持前台一线业务,大部分数据来源于业务中台,经过数据建模和数据分析等操作之后,将加工后的数据返回业务中台为前台应用提供服务,或者直接以数据类应用的方式向前台应用提供api数据服务,一般包括数据采集、数据生成、数据治理、数据应用以及数据仓库或大数据等技术应用

技术中台

业务中台落地的时候需要很多的技术组建来支撑,这些不同技术领域的技术组建就组成了技术中台。比如一些业务关键技术的组件

  • api网关
  • 开发框架
  • 微服务治理
  • 分布式数据库
  • 数据处理组件

api网关

前后端分离,通过网关实现前后端集成,api网关主要包括:鉴权、降级限流、流量分析、负载均衡、服务路由和访问日志等功能。api网关可以帮助用户方便地管理微服务api接口,实现安全的前后端分离,实现高效的系统集成和精细的服务监控

开发框架

开发框架主要包括前端开发和后端微服务开发框架,支持代码自动生成,本地调试和依赖管理等功能

微服务治理

服务治理是在为服务的运行过程中,针对微服务的运行状况采取的动态治理策略,如服务注册、发现、限流、 熔断和降级等等,以保障微服务能够持续稳定的运行

治理主要应用于微服务运行中的状态监控、微服务运行异常时的治理策略配置等场景,保障微服务在常见异常场景下的自恢复能力

分布式数据库

分布式数据库一般都具有较强的数据线性拓展能力,它们大多采用数据多副本机制实现数据库高可用,具有可拓展和低成本等技术优势,一般分为三类:交易型、分析型、交易分析混合型

  • 交易型:用于解决交易类业务的数据库计算能力,支持数据分库、分片、数据多副本,具有高可用的特性,提供统一的运维界面,具备高性能的交易型业务处理分析能力。
  • 分析型:通过横向扩展能力和并行计算能力,提高数据整体计算能力和吞吐量,支持海量数据的分析
  • 混合型:通过资源隔离、分时和数据多副本等技术手段,基于不同的数据存储、访问性能和容量等需求,使用不同的存储介质和分布式计算存储,同时满足业务交易和分析需求

数据处理组件

为了提高应用性能和业务的承载能力,降低微服务的耦合度,实现分布式架构下的分布式事务等要求,技术中台还有很多数据处理相关的基础技术组件。如分布式缓存、搜索引擎、数据复制、消息中间件和分布式事务等技术组建

gRPC

gRPC是一种现代化开源的RPC框架,由Google进行研发,能够运行于任何环境之中,最初由谷歌进行开发,它使用HTTP/2作为传输协议。

在gRPC中,客户端可以像调用本地方法一样直接调用其他机器上的服务端应用程序的方法,帮助你更容易创建分布式应用程序和服务。gRPC是基于定义一个服务,制定一个可以远程调用的带有参数和返回类型的方法。在服务端程序中实现这个接口并且运行gRPC服务处理客户端调用,在客户端,有一个stub提供和服务端相同的方法

img

为什么要用gRPC

gRPC可以帮助我们一次性的在一个.proto文件中定义服务并使用任何支持它的语言去实现客户端和服务端,也就是说gRPC解决了不同语言以及环境间通信的复杂性。使用protocol buffer还能获得其他好处,包括高效的序列化,简单的IDL以及容易进行接口更新。总之,使用gRPC能够帮助我们更容易编写跨语言的分布式代码

IDL(Interface description Language) 是指接口描述语言,是用来描述软件组件接口的一种计算机语言,是跨平台开发的基础。IDL通过一种中立的方式来描述接口,使得在不同平台上运行的对象和用不同语言编写的程序可以相互通信交流;比如,一个组件用C++写成,另一个组件用Go写成

使用gRPC进行开发的步骤

1.编写.proto文件定义服务

默认情况下gRPC使用protocol buffers作为接口定义语言(IDL)来描述服务接口和有效负载消息的结构

在gRPC中可以定义四种类型的服务方法

普通rpc,客户端向服务器发送一个请求,然后得到一个响应,就像普通的函数调用一样

1
rpc SayHello(HelloRequest) returns (HelloResponse);

服务器流式rpc,其中客户端向服务器发送请求,并获得一个流来读取一系列消息。客户端从返回的流中读取,知道没有更多的消息,gRPC保证在单个RPC调用的消息是有序的

1
rpc LotsOfReplies(HelloRequest) returns (stream HelloResponse);

客户端流式rpc,其中客户端写入一系列消息并将其发送到服务器,同样使用提供的流。一旦客户端完成了消息的写入,它就等待服务器读取消息并返回响应,同样,gRPC保证单个RPC调用中的消息是有序的

1
rpc LostsOfGreetings(stream HelloRequest) returns(HelloResponse);

双向流式rpc,其中双方使用读写流发送一系列消息,这两个流独立运行,因此客户端和服务器可以按照自己指定的顺序读写;例如,服务器可以等待接受所有客户端消息后再写响应,或者可以交替读取消息然后写入消息,或者其他读写组合。每个流中的消息是有序的

1
rpc StreamGreetings(stream HelloRequest) returns(stream HelloResponse);

2.生成指定语言的代码(客户端一份、服务端一份)

.proto文件中定义好服务之后,gRPC提供了生成客户端和服务端代码的protocol buffers编译器插件。

我们使用这些插件可以根据需要生成Java Go C++ Python等语言的代码,我们通常会在客户端调用这些API,并且在服务器端实现对应的API

  • 在服务器端,服务器实现服务声明的方法,并运行一个gRPC服务器来处理客户端发来的调用请求。gRPC底层会对传入的请求进行编码,执行被调用的服务方法,并对服务响应进行编码
  • 在客户端,客户端有一个称为存根(stub)的本地对象,它实现了与服务相同的方法。然后,客户端可以在本地对象上调用这些方法,将调用的参数包装在适当的protocol buffers消息类型中–gRPC在向服务器发送请求并返回服务器的protocol buffers响应之后进行处理

3.编写业务逻辑代码

proto文件生成pb.go以及grpc.pb.go的命令

不指定proto路径

1
protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative xxx.proto

指定proto路径

1
protoc --proto_path=xx --go_out=pb --go_opt=paths=source_relative --go-grpc_out=pb --go-grpc_opt=paths=source_relative xxx.proto

使用grpc实现一个简单的hello服务

Server

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
type server struct {
pb.UnimplementedGreeterServer //当没有完全实现proto中的所有方法时依旧可以运行起来
}

// gRPC通过.proto文件自动生成的SayHello方法
func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloResponse, error) {
reply := "hello" + in.GetName()
return &pb.HelloResponse{Reply: reply}, nil
}

func main() {
// 启动服务
l, err := net.Listen("tcp", ":8972")
if err != nil {
fmt.Println("failed to listen,err:", err)
return
}
// 注册服务
s := grpc.NewServer()
pb.RegisterGreeterServer(s, &server{})
// 启动服务
err = s.Serve(l)
if err != nil {
fmt.Println("failed to server,err:", err)
}
}
syntax = "proto3"; //版本声明

option go_package="hello_server/pb"; // 项目中import导入生成go代码的模块

package pb; //proto文件模块

// 定义服务
service Greeter {
// 定义方法
rpc SayHello (HelloRequest) returns (HelloResponse) {}
}

// 定义消息
message HelloRequest {
string name = 1; //字段的序号
}

message HelloResponse {
string reply = 1;
}

Client

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
func main() {
//连接server 带加密连接
conn, err := grpc.Dial("127.0.0.1:8972", grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
log.Fatalf("grpc.Dial failed,err:%v", err)
return
}
defer conn.Close()
//创建客户端
c := proto.NewGreeterClient(conn)
//使用context进行控制,传入background和超时时间一秒钟
ctx, cancel := context.WithTimeout(context.Background(), time.Second*1)
defer cancel()
name := "xiaocheng"
resp, err := c.SayHello(ctx, &proto.HelloRequest{Name: name})
if err != nil {
log.Printf("c.SayHello failed, err:%v", err)
return
}
// 拿到RPC响应
log.Printf("resp:%v", resp.GetReply())
}
// 应该是同一份proto文件
syntax = "proto3"; //版本声明

option go_package="hello_client/proto"; // 项目中import导入生成go代码的模块

package pb; //proto文件模块 必须与server端一致

// 定义服务
service Greeter {
// 定义方法
rpc SayHello (HelloRequest) returns (HelloResponse) {}
}

// 定义消息
message HelloRequest {
string name = 1; //字段的序号
}

message HelloResponse {
string reply = 1;
}

使用grpc实现一个简单的add服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
type server struct {
pb.UnimplementedAddMethodServer
}

func (s *server) Add(ctx context.Context, in *pb.AddRequest) (*pb.AddResponse, error) {
reply := in.GetArgs1() + in.GetArgs2()
return &pb.AddResponse{Number: reply}, nil
}

func main() {
//启动服务
l, err := net.Listen("tcp", ":9999")
if err != nil {
fmt.Println("net listen failed,err:", err)
return
}
s := grpc.NewServer()
pb.RegisterAddMethodServer(s, &server{})
err = s.Serve(l)
if err != nil {
fmt.Println("failed to server,err:", err)
}
}

proto文件应该在客户端和服务端都有一份

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
syntax="proto3";
option go_package="server/pb";
package pb;

service AddMethod {
rpc Add(AddRequest) returns (AddResponse) {}
}

message AddRequest {
int32 args1 =1;
int32 args2 =2;
}

message AddResponse {
int32 number =1;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
func main() {
conn, err := grpc.Dial("127.0.0.1:9999", grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
fmt.Println("grpc dail failed,err:", err)
return
}
defer conn.Close()
c := pb.NewAddMethodClient(conn)
ctx, cancel := context.WithTimeout(context.Background(), time.Second*1)
defer cancel()
var args1, args2 int32
args1 = 1
args2 = 2
resp, err := c.Add(ctx, &pb.AddRequest{Args1: args1, Args2: args2})
if err != nil {
fmt.Println("c.Add failed,err:", err)
return
}
log.Println("Add Response:", resp)
}

protobuf语法

protobuf为什么体积小、解析快

protobuf是google提出的数据交换格式,同一条消息数据,使用Protobuf序列化之后占用空间是json的1/10,但是性能却是几十倍

原因

  • 编解码大多采用位运算,比JSON/XML的字符匹配效率更高
  • pb定义了varint类型,使用变长编码压缩数值类型。值越小的数字,使用的字节数就越少
  • 采用Tag-value类型,没有冗余字符

定义一个消息类型

1
2
3
4
5
6
7
8
9
10
11
syntax="proto3";

message SearchRequest {
string query =1;
int32 page_number=2;
}
//文件的第一行指定使用proto3语法,如果不这么写
//pb的编译器默认使用proto2

//SearchRequest定义了一个消息,使用了两个字段
//每个字段需要定义类型 名字 和编号

字段编号

消息定义中的每个字段都要有一个唯一的编号,这些编号用来在消息二进制格式中标识字段,在消息类型使用后就不能更改。在范围1到15中的字段需要一个字节进行编码,而16-2047的字段采用两个字节。所以应该为经常使用的消息元素保留数字1到15的编号,也要为将来可能添加的经常使用的元素留出一些编号

指定字段规则

消息字段可以是下列字段之一

  • singular:格式正确的消息可以有这个字段的0个或者一个,默认使用singular字段
  • repeated:该字段可以在格式正确的消息中重复任意次数(包括0次),重复值的顺序将被保留
  • optional:该字段在传递的时候可选也可不选

保留字段

如果你通过完全删除字段或者将其注释来更新消息类型,那么未来的用户在对该类型进行自己的更新的时候就可以重用字段号,如果其他人以后加载旧版本的相同.proto文件,这可能就会导致严重的问题,比如数据损坏、隐私漏洞等等。

解决方法是指定已经删除的字段的字段编号,如果将来有用户尝试使用这些字段标识符,protocol buffer编译器将发出提示

1
2
3
message Foo {
reserved 2,15,9 to 11;
}

值类型

.proto Type Notes C++ Type Java/Kotlin Type[1] Python Type[3] Go Type PHP Type
double double double float float64 float
float float float float float32 float
int32 使用可变长度编码。编码负数效率低下——如果你的字段可能有负值,则使用 sint32代替。 int32 int int int32 integer
int64 使用可变长度编码。编码负数效率低下——如果你的字段可能有负值,则使用 sint64代替。 int64 long int/long[4] int64 integer/string[6]
uint32 使用变长编码。 uint32 int[2] int/long[4] uint32 integer
uint64 使用变长编码。 uint64 long[2] int/long[4] uint64 integer/string[6]
sint32 使用可变长度编码。带符号的 int 值。这些编码比普通的 int32更有效地编码负数。 int32 int int int32 integer
sint64 使用可变长度编码。带符号的 int 值。这些编码比普通的 int64更有效地编码负数。 int64 long int/long[4] int64 integer/string[6]
fixed32 总是四个字节。如果值经常大于228,则比 uint32更有效率。 uint32 int[2] int/long[4] uint32 integer
fixed64 总是8字节。如果值经常大于256,则比 uint64更有效率。 uint64 integer/string[6]
sfixed32 总是四个字节。 int32 int int int32 integer
sfixed64 总是八个字节。 int64 integer/string[6]
bool bool boolean bool bool boolean
string 字符串必须始终包含 UTF-8编码的或7位 ASCII 文本,且不能长于232。 string String str/unicode[5] string string
bytes 可以包含任何不超过232字节的任意字节序列。 string ByteString str (Python 2) bytes (Python 3) []byte string

枚举

在定义消息类型的时候,可能希望其中的一个字段只能是预定义的值列表中的一个值。下面是一个栗子,Conrpus字段的值只能是其中的一个

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
message SearchRequest {
string query = 1;
int32 page_number = 2;
int32 result_per_page = 3;
enum Corpus {
UNIVERSAL = 0;
WEB = 1;
IMAGES = 2;
LOCAL = 3;
NEWS = 4;
PRODUCTS = 5;
VIDEO = 6;
}
Corpus corpus = 4;
}

嵌套消息类型

1
2
3
4
5
6
7
8
9
message SearchResponse {
repeated Result results = 1;
}

message Result {
string url = 1;
string title = 2;
repeated string snippets = 3;
}

Any

Any类型允许你将消息作为嵌入类型使用,使用Any类型需要导入google/protobuf/any.proto

1
2
3
4
5
6
import "google/protobuf/any.proto";

message ErrorStatus {
string message = 1;
repeated google.protobuf.Any details = 2;
}

oneof

如果你有一条包含多个字段的消息,并且同时最多设置其中的一个字段,那么可以通过oneof来实现并节省内存,可以通过case()或者WihchOneOf()来检查one of 中的哪个值被设置(如果有)

1
2
3
4
5
6
7
8
9
10
11
message SampleMessage {
oneof test_oneof {
string name = 4;
SubMessage sub_message = 9;
}
}
SampleMessage message;
message.set_name("name");
CHECK(message.has_name());
message.mutable_sub_message(); // Will clear name field.
CHECK(!message.has_name());

Maps

如果想创建一个关联映射作为数据定义的一部分,可以使用这个map

1
map<key_type, value_type> map_field = N;

protobuf实战

oneof字段

oneof中的值只能选择其中的一个

1
2
3
4
5
6
7
message NoticeReaderRequest {
string msg=1;
oneof notice_way{
string email=2;
string phone=3;
}
}

对应的服务端代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
func oneofDemo() {
req := &book.NoticeReaderRequest{
Msg: "here is chengxisheng",
NoticeWay: &book.NoticeReaderRequest_Email{
Email: "xxx",
},
}
req2 := &book.NoticeReaderRequest{
Msg: "here is xishengcheng",
NoticeWay: &book.NoticeReaderRequest_Phone{
Phone: "1008611",
},
}
switch v := req.NoticeWay.(type) {
case *book.NoticeReaderRequest_Email:
noticeWithEmail(v)
case *book.NoticeReaderRequest_Phone:
noticeWithPhone(v)
}
switch v := req2.NoticeWay.(type) {
case *book.NoticeReaderRequest_Email:
noticeWithEmail(v)
case *book.NoticeReaderRequest_Phone:
noticeWithPhone(v)
}
}
func noticeWithEmail(in *book.NoticeReaderRequest_Email) {
fmt.Printf("notice reader by email:%v\n", in.Email)
}
func noticeWithPhone(in *book.NoticeReaderRequest_Phone) {
fmt.Printf("notice reader by phone:%v\n", in.Phone)
}

//这里必须使用类型断言+switch case
//来进行one of 字段的确认

wrapvalue类型

首先让我们想一想Go中区分一个MySQL的int类型是默认值还是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
price sql.NullInt64
price *int64
//第一种方式是一个定义好的结构体
//里面有一个字段是 该结构体是否被赋值
//第二种方式是直接用指针来做
//对指针解引用,如果为0则赋值,如果为Nil则是默认值
func wrapValueDemo() {
// client
book:=book.Book{
Title: "learning go language",
Price: &wrapperspb.Int64Value{Value: 600},
Memo: &wrapperspb.StringValue{Value: "学"},
}
// server
if book.GetPrice()==nil {
fmt.Println("is not assigned")
} else {
fmt.Println(book.GetPrice().GetValue())
}
if book.GetMemo()==nil {
fmt.Println("is not assigned")
} else {
fmt.Println(book.GetMemo().GetValue())
}
}

FieldMask类型

当我们更新的时候,定义了很多字段,不可能全部进行全量更新Book的每个字段,因为通常操作只会更新1到2个字段。

当我们想知道更新操作涉及到的具体字段,就需要使用到filedmask类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
message UpdateBookRequest {
// 操作人
string op =1;
// 要更新的书籍信息
Book book=2;
// 要更新的字段
google.protobuf.FieldMask update_mask=3;
}
func fieldMaskDemo() {
//client
paths := []string{"price"}
req := &book.UpdateBookRequest{
Op: "chengxisheng",
Book: &book.Book{
Price: &wrapperspb.Int64Value{Value: 8800},
},
UpdateMask: &fieldmaskpb.FieldMask{Paths: paths},
}
mask, _ := fieldmask_utils.MaskFromProtoFieldMask(req.UpdateMask, generator.CamelCase)
var bookDst = make(map[string]interface{})
fieldmask_utils.StructToMap(mask, book.UpdateBookRequest{}.Book, bookDst)
fmt.Printf("bookDst:%#v\n", bookDst)
}

服务端流式RPC

对应的proto(client和server)中添加一个流式方法

1
rpc LotsOfReplies(HelloRequest) returns (stream HelloResponse);

Server添加一个新的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func (s *server) LotsOfReplies(in *pb.HelloRequest, stream pb.Greeter_LotsOfRepliesServer) error {
words := []string{
"你好",
"hello",
"こんにちは",
"안녕하세요",
}
for _, word := range words {
data := &pb.HelloResponse{
Reply: word + in.GetName(),
}
// 使用Send方法发送多个数据 每当有一个data就send一次数据
if err := stream.Send(data); err != nil {
return err
}
}
return nil
}

Client端添加一个新的函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
func callLotsOfReplies(c proto.GreeterClient) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*1)
defer cancel()
stream, err := c.LotsOfReplies(ctx, &proto.HelloRequest{Name: *name})
if err != nil {
log.Fatalf("c.LotsOfReplies failed,err:%v", err)
}
for {
//依次从流式响应中读取返回的响应数据
res, err := stream.Recv()
if err == io.EOF {
break
}
if err != nil {
log.Fatalf("c.LotsOfReplies failed,err:%v", err)
}
log.Printf("got reply: %q\n", res.GetReply())
}
}

客户端流式RPC

hello.proto中添加这么一个新的方法

1
rpc LotsOfGreetings(stream HelloRequest) returns (HelloResponse);

在server端添加

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func (s *server) LotsOfGreetings(stream pb.Greeter_LotsOfGreetingsServer) error {
reply := "你好:"
for {
//接受客户端发来的流式数据
res, err := stream.Recv()
if err == io.EOF {
return stream.SendAndClose(&pb.HelloResponse{
Reply: reply,
})
}
if err != nil {
return err
}
reply += res.GetName()
}
}

在Client端中添加

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func runLotsOfGreeting(c proto.GreeterClient) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
// 客户端要流式的发送请求消息
stream, err := c.LotsOfGreetings(ctx)
if err != nil {
log.Printf("c.LotsOfGreetings failed,err:%v\n", err)
return
}
names := []string{"张三", "李四", "王五"}
for _, name := range names {
stream.Send(&proto.HelloRequest{Name: name})
time.Sleep(200 * time.Millisecond)
}
//关闭流
res, err := stream.CloseAndRecv()
log.Printf("res:%v\n", res)
}

双向流式RPC

在proto中添加

1
rpc BidiHello(stream HelloRequest) returns(stream HelloResponse);

在client中添加

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
func (s *server) BidiHello(stream pb.Greeter_BidiHelloServer) error {
for {
//接受流式请求
in, err := stream.Recv()
if err == io.EOF {
return nil
}
if err != nil {
return err
}
reply := magic(in.GetName())
//返回流式响应
if err := stream.SendAndClose(&pb.HelloResponse{Reply: reply}); err != nil {
return err
}

}
}

在Server端中添加

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
func (s *server) BidiHello(stream pb.Greeter_BidiHelloServer) error {
for {
//接受流式请求
in, err := stream.Recv()
if err == io.EOF {
return nil
}
if err != nil {
return err
}
reply := magic(in.GetName())
//返回流式响应
if err := stream.SendAndClose(&pb.HelloResponse{Reply: reply}); err != nil {
return err
}

}
}


// magic 一段价值连城的“人工智能”代码
func magic(s string) string {
s = strings.ReplaceAll(s, "吗", "")
s = strings.ReplaceAll(s, "吧", "")
s = strings.ReplaceAll(s, "你", "我")

s = strings.ReplaceAll(s, "?", "!")
s = strings.ReplaceAll(s, "?", "!")
return s
}

MetaData元数据

metadata是指在处理RPC请求和响应过程中需要但又不属于具体业务(例如身份验证详细信息)的信息,采用的是键值对列表 的形式,其中键是string类型,值通常是[]string类型,但也可以是二进制数据。gRPC中的metadata类似于我们在HTTP headers中的键值对,元数据可以包含认真token、请求标识和监控标签等等

metadata中的键是大小写不敏感的,不能以grpc-开头,并且二进制的简明必须以-bin结尾,元数据对gRPC本身是不可见的,通常在应用程序代码或者中间件中处理元数据,我们不需要再.proto中指定元数据

1
type MD map[string][]string

基本概念

架构演变

用架构历史

1.单体架构 堆机子 高耦合 一改动就需要重新部署 而且编译时间很长,不容易拓展,不支持多语言技术栈

2.分层架构 典型的有MVC和MSC架构 当访问量逐渐增大,单体架构扛不住了,把单体项目进行垂直划分,耦合还是很大,项目之间的接口多为数据同步,比如不同项目之间的数据库同步。 架构简单,成本低开发周期短,经过垂直拆分之后原来的单体项目不至于太大,每一层可以用不同的技术,但还是不易拓展和维护

3.SOA面向服务架构 :当垂直架构的应用越来越多,就会出现多个应用都依赖的业务组件,比如数据库,而且各个应用交互越来越频繁,此时就需要把部分通用的组件拆分独立处理,于是SOA面向服务架构诞生了,它带来了模块化开发、分布式拓展部署和服务接口定义等概念

实时SOA需要建立企业服务总线,外部应用通过总线调用服务,有以下特征:可从企业外部访问、随时可用、标准化的服务接口等

img

优点:

  • 已经具有微服务的影子了,将重复的功能抽离出来,提高开发效率
  • 减少接口耦合

SOA架构适用于大型软件服务企业对外提供服务的场景,并不适合一般的业务场景,其服务的定义、注册和调用都需要繁琐的配置,业务总线的吞吐量决定了整个系统的上限,因为整个系统都是通过总线进行任务分配的。并且业务总线也容易导致系统崩掉、影响性能。

4.微服务架构:

img

特点

1.服务层完全独立出来 并将服务层抽取为一个一个的微服务

2.微服务遵循单一原则

3.微服务之间采用RESTful等轻量协议通信

4.微服务一般用容器技术部署 运行在自己的独立进程中

微服务架构下服务的拆分粒度更细,有利于资源重复利用,提高开发效率,采用去中心化思想,更轻量级

缺点:如果服务实例过多,治理成本就会很大,不利于维护;服务之间相互依赖,可能形成复杂的依赖链条,往往单个服务异常,其他服务也会受到影响,出现服务雪崩效应。

微服务与SOA的区别:

微服务继承了SOA的众多优点和理念

SOA更适合与许多其他应用程序继承的大型复杂企业应用程序环境,小型的应用并不适合SOA,微服务则更适合于较小和良好的分割式web业务系统

微服务不再强调SOA架构中比较重要的ESB企业服务总线,而是通过轻量级通信机制相互沟通

SOA注重的是系统继承,而微服务关注的则是完全分离,SOA尝试采用中心化管理来确保各个应用能够协同运作,微服务则尝试部署新功能,快速有效地拓展开发团队,它着重于分散管理、代码再利用和自动化执行。

微服务的优势和劣势

微服务的优势

1.快:更注重CI/CD 敏捷开发、持续交付

2.准:服务粒度小、服务质量精准可控

3.狠:适用于互联网时代、产品迭代周期更短

微服务的劣势

1.系统的复杂性

2.服务依赖管理

3.数据的一致性保障

4.测试更加艰难

5.对于DevOps等基础设施的高要求

如何划分微服务界限

如何进行服务划分?

1.按照业务职能进行划分

由公司内部不同部门提供的只能。例如客户服务部门提供客户服务的职能,财务部门提供财务相关的职能

2.按照DDD的限界上下文划分

限界上下文是DDD中用来划分不同业务边界的元素

这里业务边界的含义是“解决不同业务问题”的问题域和对应的解决方案域

为了解决某种类型的业务问题,贴近领域只是,也就是业务

CQRS将系统中的操作划分为两类,即【命令】Command和【查询】Query

命令则是对会引起数据发生变化操作的总称,即我们常说的新增、更新、删除的这些操作,都是命令。

而查询则和字面意思一样,即不会对数据产生变化的操作,只是按照某些条件查询数据。

CQRS的核心思想是将两类不同的操作进行分离,然后在两个独立的【服务】中实现。这里的服务一般指的是两个独立部署的应用,在某些特殊情况下,也可以部署在同一个应用内的不同接口上。

微服务的迭代

1.第一代

img

2.第二代

img

把那些服务监控、服务管理作为基础服务提供给我们的业务

架构分层

img

核心组件

  • API网关
  • 服务注册中心
  • 配置中心
  • 服务通信
  • 服务治理
  • 服务监控

net/rpc

RPC出现的原因

RPC需要解决三个问题

1.如何要确定要执行的函数?在本地调用中,函数主体通过函数指针函数指定,然后调用add函数,编译器你通过函数指针函数确定add函数在内存中的位置。但是在RPC中,调用不能通过函数指针完成,因为他们的内存地址可能完全不同。因此,调用方和被调用方都需要维护一个{fuction<->ID}映射表,以确保调用正确的函数

2.如何表达参数?本地过程调用中传递的参数是通过堆栈结构实现的,但是RPC不能直接使用内存传递参数,因此参数或返回值需要在传输期间徐丽湖儿啊并转换成字节流,反之亦然

3.如何通过网络传输?函数的调用方和被调用方通常是通过网络连接的,也就是说 function ID和序列化字节流需要通过网络传输,因此,只要能够完成传输,调用方和被调用方就不受某个网络协议的限制。例如,一些RPC框架使用TCP协议,一些使用HTTP。

以往实现跨服务调用的时候,我们会采用restful api的方式,被调用方会对外提供一个HTTP接口,调用方按要求发起HTTP请求并接收API接口返回的响应数据。

本地调用,通过HTTP的API的方式

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
server
//定义参数和响应
type addParam struct {
X int `json:"x"`
Y int `json:"y"`
}
type addResult struct {
Code int `json:"code"`
Data int `json:"data"`
}

func add(x, y int) int {
return x + y
}
// addHandler 解析参数+调用add+响应写回
func addHandler(w http.ResponseWriter, r *http.Request) {
// parse parameters
b, _ := ioutil.ReadAll(r.Body)
var param addParam
json.Unmarshal(b, &param)
// use the add func
ret := add(param.X, param.Y)
// return the response
respBytes, _ := json.Marshal(addResult{Code: 0, Data: ret})
w.Write(respBytes)
}

func main() {
http.HandleFunc("/add", addHandler)
log.Fatal(http.ListenAndServe(":9090", nil))
}
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
client
type addParam struct {
X int `json:"x"`
Y int `json:"y"`
}
type addResult struct {
Code int `json:"code"`
Data int `json:"data"`
}

func main() {
url := "http://127.0.0.1:9090/add"
param := addParam{
X: 10,
Y: 20,
}
// marshal to json

paramBytes, _ := json.Marshal(param)
// call
resp, _ := http.Post(url, "application/json", bytes.NewReader(paramBytes))
defer resp.Body.Close()
respBytes, _ := ioutil.ReadAll(resp.Body)
var respData addResult
json.Unmarshal(respBytes, &respData)
fmt.Println(respData.Data)
}

而RPC调用则不需要如此

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
service.go
type Args struct {
X, Y int
}

type ServiceA struct{}

// Add is an out method
// has two args and a return
// two params must be out
// and the return value must be error type
func (s *ServiceA) Add(args *Args, reply *int) error {
*reply = args.X + args.Y
return nil
}
server.go
func main() {
//new service instance
service := new(yunyuansheng.ServiceA)
//register rpc service
rpc.Register(service)
//botton on http
//rpc.HandleHTTP()
//botton on tcp
l, e := net.Listen("tcp", ":9091")
if e != nil {
log.Fatal("listen error:", e)
}
//http.Serve(l, nil)
for {
// accpet the request and serve
conn, _ := l.Accept()
rpc.ServeConn(conn)
}
}
client.go
func main() {
//因为服务端是HTTP请求 所以要建立HTTP连接
client, err := rpc.Dial("tcp", "127.0.0.1:9091")
if err != nil {
fmt.Println(err)
}
// 同步调用 Call
args := &yunyuansheng.Args{10, 20}
reply := new(int)
err = client.Call("ServiceA.Add", args, reply)
if err != nil {
log.Fatal("ServiceA.Add error:", err)
}
fmt.Printf("ServiceA.Add %d+%d=%d\n", args.X, args.Y, *reply)

//异步调用 Go
var reply2 int
divCall := client.Go("ServiceA.Add", args, &reply2, nil)
replyCall := <-divCall.Done //Done是一个调用结果的通知 有值了就说明调用完成了
fmt.Println(replyCall.Error)
fmt.Println(reply2)
}

RPC的最终目的:让调用远程方法更加简单,并且速度更快

Go原生net/rpc库需要注意的几点

1.可以支持很多种协议,包括但不限于HTTP和TCP,如果使用HTTP的话,那么客户端就使用DialHTTP,服务端通过HandleHTTP进行HTTP连接的处理,使用TCP的话,客户端使用Dial,服务端就应该for循环处理连接

2.客户端支持同步调用和异步调用两种方式,对应的分别是Call和Go

3.暴露出的服务必须满足两个条件,两个参数,一个返回值,返回值必须要是error类型,第二个参数必须是指针

RPC原理

img

  1. client以本地调用方式调用服务
  2. client stub接收到调用后负责将方法、参数等组装成能够进行网络传输的消息体
  3. client stub找到服务地址,并将服务发送到服务端
  4. server 接收到消息之后,通过server stub对消息进行解码
  5. server stub根据解码的结果调用本地服务
  6. 本地服务执行并把消息返回给server stub
  7. server stub将结果打包成能够进行网络传输的结构体,发送到消息方
  8. client 收到消息并进行解码,得到最终结果

前言

之前我一直觉得博客嘛,就应该发一些技术相关的东西,看了很多同龄人的博客我才发现,其实博客可以发的东西有很多很多,不一定要仅限于技术,于是就有了这篇文章。

同时我是一个很喜欢用文字记录生活与想法的人,但是由于文字功底太弱,这些文字大多都躺在我的备忘录和废纸篓里。

昨天看到了一个我很尊敬的学长写的一篇博文,深有所感。于是想把这篇于2023-2-24日写好的博文修改一下,记录一下大学这两年的一些经历,感悟和想法。

还记得去年这个时候我还没有开始准备实习,那段时间应该是在完成工作室的考核,现在回头看看其实已经走出去很远了。

下面简单说一下我找实习的几个阶段吧。

stage one

在21年年底的时候,我在QQ空间里面看到了有学长分享食铁兽招新的信息,然后加入了招新QQ群,22年1月13日,那时候我刚考完《数据结构》这门课,这也是那学期的最后一门考试,考完之后打算和我的爱人去主城区耍的,然后突如其来的约面了,那是我人生中的第一次技术面试,说实话答得很差,完全没有准备过八股,只能凭借平常做项目的一些实践经历答上来一部分问题,大概只答上来了一小半吧,我印象很深的问题有Redis的持久化,Redis的数据结构,HTTP2的特性这些,面试官还和我讨论了一下Go的优劣和以后职业规划的事情。

之后食铁兽的负责人晚上又和我联系了一下,简单问了一些职业发展之类的问题。

第二天通知我面试通过了,当时真的非常非常开心,第一次觉得自己在技术上或者说是在编程上被认可了,然后就打车去了食铁兽那边,见到了面试官源哥,婧姐,还有负责人峰哥。

之后的话就放寒假了,我当时在家不怎么学习,基本上都是食铁兽给的一些任务PUSH着我去学,比如尝试基于TCP、UDP、KCP、和HTTP编程,还有当时Go的最新版本是1.18beta,推出了泛型这一特性,但是还没有来得及对内置数据结构(slice map等)进行泛型的封装,于是我去学习了泛型,实现了简单的泛型封装,之后的任务就是写一个分布式消息队列。

差不多三月到五月吧,基本每个周末都会去食铁兽那边敲代码,慢慢的把分布式消息队列的拼图一块块补全,从raft选主的实现,到消息的存储和删除,到消息的切片,再到考虑结点状态进行消息的分发,慢慢的也把这个项目做出来了,这也是我第一个不看视频写出来的非web项目。

除了工程上的收获之外,我还认识了很多很厉害的大佬,比如源哥,给我的感觉就是在技术上钻的很深,不管是理论知识还是工程能力都很强,在大四的时候就能写出很多很多东西了,架构层面的知识也很优秀,之前问过他一点设计方面的题目,能感觉出来思考的方面特别多。而且他知识的广度也很大,前端后端嵌入式都会,现在我也大三下了,感觉我也很难在一年之后达到这样的水平。还有峰哥、翔哥、婧姐,就不一一说了,食铁兽的氛围是真的挺好的,这些哥哥姐姐们也很照顾我,在那边呆着也很舒服。

stage two

差不多五月开始正式准备八股和算法了,那段时间的学习强度真的特别大,差不多白天一整天都在工作室呆着,学到脑子疼才结束,晚上睡眠质量也差,差不多准备了两个礼拜之后就开始投递简历了,尴尬的事情来了,投了很多厂子,不过大部分厂子看到是24届就直接拒了,最后好像只有几个小厂还有字节给了面试机会,字节的算法没做出来,于是一面挂,拿了一个小厂的offer但是不太想去。

stage three

没有找到合适的实习就选择了去老师的实验室干活,做的是偏运维的华为HPC高性能迁移项目,其实就是写脚本,不过真的挺折磨的,需要保证一个大的脚本一次性执行成功。做完了就差不多七月底了,八月主要是在陪我的爱人,当时自己也生病了,除了复习八股之外还学了一些gRPC,做了一个简单的分布式系统来巩固知识。九月回校,这时候八股和算法都算比较巩固了,开始了新一轮的投递,两个礼拜下来面了五六家吧,也没有大厂愿意给面试机会,最后拿到了两个offer,分别是通明智云和daocloud,其实更想去daocloud,面试官说做的是容器方向,还能带薪做开源,狠狠的心动了,不过daocloud的给的薪资属实是有点少,算了一下得贴钱实习,于是去了通明智云,在通明智云的实习期间也很舒服,我的leader很信任我直接让我和客户对接,为期三个月的实习确实有真的在做东西学东西,也从同事前辈们身上学到了很多东西,总之是一段很有意义的实习经历。

stage four

差不多十二月的时候,当时考虑到通明这边的实习期快到了,于是开启了新一轮的投递,可能是因为在盘hc,同样的也没有大厂面试机会,一轮面下来也拿了几个小厂的offer,而且待遇也不是很好,做的东西也不太感兴趣,唯一感兴趣的可能是默安科技的云原生开发,但是突如其来的我的爱人与我分手了,当时就不太想去实习了,想回家修养一阵子,于是把offer给拒了,在流程中的面试也给推了。

image-20230224111242791

stage five

年后市场开始复苏了,从一月底开始投递,一共投的也就那么几家吧,最后拿到了百度中台和莉莉丝运维的offer,最后打算去百度。

image-20230224111357693

一些想法

从大一确定毕业就业开始,我就很想去大厂实习,但是拿到大厂offer之后也没有很开心,是真的没有波澜,很平淡,和我当时通过食铁兽面试,拿到通明智云的offer一样,非常平淡。也许这就是人生吧,很多事情不要太去追求结果,这样在结果不如意的时候真的会很痛苦,结果如意了也不见得开心,多关注过程,那些奋斗的日子回过头看是真的挺美好的。

理论

设计模式从何而来

模式:每个模式都描述了一个在我们的环境中不断出现的问题,然后描述了该问题的解决方案的核心,也就是说,设计模式是在特定环境下人们解决某类重复出现问题的一套成功或者有效的解决方案

软件设计模式

Gang of Four提出了软件设计模式
Gof提出的设计模式有23个,包括

  • 创建型模式:如何创建对象
  • 结构型模式:如何实现类或者对象的组合
  • 行为型模式:类或者对象怎么交互以及怎样分配职责

“简单工厂模式”不属于23种
设计模式:GOF的23种+简单工厂模式
image.png
image.png
image.png
image.png

设计原则

设计原则是设计模式的核心思想,一共有七种

  • 单一职责原则: 类的职责单一,对外只提供一种功能,而引起类变化的原因都应该只有一个
  • 开闭原则:类的改动是通过增加代码进行的,而不是修改源代码
  • 里氏代换原则:任何抽象类(interface接口)出现的地方都可以用他的实现类进行替换,实际就是虚拟机制,语言级别实现面向对象功能
  • 依赖倒转原则:依赖于抽象(接口),不要依赖具体的实现(类),也就是针对接口编程
  • 接口隔离原则:不应该强迫用户的程序依赖他们不需要的接口方法。一个接口应该只提供一种对外功能,不应该把所有操作都封装到一个接口中去
  • 合成复用原则:如果使用继承,会导致父类的任何变换都可能影响到子类的行为。如果使用对象组合,就降低了这种依赖关系。对于继承和组合,优先使用组合
  • 迪米特法则一个对象应当对其他对象尽可能少的了解,从而降低各个对象之间的耦合,提高系统的可维护性,例如在一个程序中,各个模块相互调用时,通常会提供一个同一的接口来实现。这样其他模块不需要了解另外一个模块的内部实现细节,这样当一个模块内部的实现发生改变的时候,不会影响其他模块的使用(黑盒原理)

单一职责原则

类的职责是单一的,对外只提供一种功能,而引起类变化的原因也应该只有一个
在面向对象编程的过程中,设计一个类,建议对外提供的功能单一,接口单一,影响一个类的范围就限定在这一个接口上,一个类的一个接口具备这个类的功能含义,职责单一不复杂
一个类对外只提供一种功能
实例代码

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

import "fmt"

// 以下代码不遵循单一职责原则,一个类实现了多个功能
// 即一个clothes结构体实现了“工作装扮”和“逛街装扮”两个方法
type Clothes struct {
}

//func (c *Clothes) Style() {
// fmt.Println("工作的装扮")
//}
//
//func (c *Clothes) Style2() {
// fmt.Println("逛街的装扮")
//}
//
//func main() {
// c := Clothes{}
// c.Style()
// c.Style2()
//}

// 单一职责原则
// 每一个类(结构体)负责一个功能或者一个逻辑

type ClothesShop struct {
}

type ClothesWork struct {
}

func (c *ClothesWork) Style() {
fmt.Println("工作的装扮")
}

func (c *ClothesShop) Style() {
fmt.Println("逛街的装扮")
}

func main() {
c := ClothesWork{}
c.Style()
c1 := ClothesShop{}
c1.Style()
}

开闭原则

开闭原则的核心思想就是当我们添加一个新功能的时候,不是通过修改代码,而是通过增添代码来实现的。
如果我们使用接口interface就可以进行一层抽象,然后提供一个抽象的方法供业务进行实现。
增加功能的时候去增加代码而不是修改代码
示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
// 以下代码是平铺式设计 每当添加一个业务就需要增加方法 会导致Banker类越来越臃肿
// 不符合开闭原则 每当有新的功能出现就要对类添加对应功能的代码
// 当Banker业务越多再修改Banker的业务或者添加新业务的时候 出现问题的问题也会越来越大
// 耦合度太高 Banker的职责也不够单一 代码的维护成本与业务的复杂程度成正比

//type Banker struct {
//}
//
//func (b *Banker) Save() {
// fmt.Println("进行了 存款业务...")
//}
//
//func (b *Banker) Transfer() {
// fmt.Println("进行了 转账业务...")
//}
//
//func (b *Banker) Pay() {
// fmt.Println("进行了 支付业务...")
//}
// 新增的Deal服务
//func (b *Banker) Deal() {
// fmt.Println("进行了 交易业务...")
//}
//
//func main() {
// banker := &Banker{}
// banker.Save()
// banker.Transfer()
// banker.Pay()
//}

// 开闭原则
// 在Go中的描述就是通过接口实现多态,每个类去实现接口
// 这样的话就能实现一个结果:类的改动是通过增加代码进行的,而不是修改源代码

type AbstractBanker interface {
Business()
}

type SaveBanker struct {
}

func (sb *SaveBanker) Business() {
fmt.Println("进行了存款")
}

// 添加转账功能

type TransferBanker struct {
}

func (tb *TransferBanker) Business() {
fmt.Println("进行了转账")
}

// 可以基于抽象层进行业务封装-针对interface接口进行封装

func BankBusiness(banker AbstractBanker) {
banker.Business()
}

func main() {
sb := SaveBanker{}
sb.Business()
tb := TransferBanker{}
tb.Business()
BankBusiness(&sb)
BankBusiness(&tb)
}

依赖倒转原则

在设计一个系统的时候我们可以将模块分成三个层次,抽象层、实现层、业务逻辑层。我们首先将抽象层的模块和接口定义出来,然后通过interface接口的设计依照抽象层依次实现每个实现层的模块,在我们写实现层代码的时候,实际上只需要参考对应的抽象层,实现每个模块。而业务逻辑层也是通过抽象层暴露出来的接口进行实现的,可以使用的方法就是抽象层暴露出来的方法
模块与模块之间依赖抽象而不是具体实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
// 下面的代码耦合度很高,不满足依赖倒转原则
// 如果要满足张三开宝马,李四开奔驰,就需要重新添加代码
// 如果司机人数为m,汽车数量为n,那么需要编写的方法为m*n

//type Benz struct {
//}
//
//func (b *Benz) Run() {
// fmt.Println("Benz is running")
//}
//
//type BMW struct {
//}
//
//func (b *BMW) Run() {
// fmt.Println("BMW is runnning")
//}
//
//type Zhang3 struct {
//}
//
//func (z *Zhang3) DriveBenz(benz *Benz) {
// benz.Run()
// fmt.Println("Zhang3 is driving Benz")
//}
//
//type Li4 struct {
//}
//
//func (l *Li4) DriveBMW(bmw *BMW) {
// bmw.Run()
// fmt.Println("Li4 is driving BMW")
//}
//
//func main() {
// benz := &Benz{}
// zhang3 := Zhang3{}
// zhang3.DriveBenz(benz)
// bmw := &BMW{}
// li4 := Li4{}
// li4.DriveBMW(bmw)
//}

// 抽象层

type Car interface {
Run()
}

type Driver interface {
Driver(car Car)
}

// 实现层
// 每个车子都实现Run方法
// 每个司机都实现Drive方法
// 这样需要实现的方法为m+n
// 而且实现层只依赖于抽象层
type Benz struct {
}

func (b *Benz) Run() {
fmt.Println("Benz is Running")
}

type BMW struct {
}

func (b *BMW) Run() {
fmt.Println("BMW is Running")
}

type Zhang3 struct {
}

func (z3 *Zhang3) Drive(car Car) {
fmt.Println("zhang3 drive car")
car.Run()
}

type Li4 struct {
}

func (l4 *Li4) Drive(car Car) {
fmt.Println("li4 drive car")
car.Run()
}

// 业务逻辑层

func main() {
var benz Car = new(Benz)
z := new(Zhang3)
z.Drive(benz)
}

合成复用原则

如果使用继承,会导致父类的任何变换都可能影响到子类的行为。如果使用对象组合,就降低了这种依赖关系。对于继承和组合,优先使用组合。
使用组合来实现父类方法

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
type Cat struct {
}

func (c *Cat) Eat() {
fmt.Println("小猫吃饭")
}

//使用继承来实现 添加一个睡觉的方法

type CatB struct {
Cat
}

func (c *CatB) Sleep() {
fmt.Println("小猫睡觉")
}

//使用组合来添加可睡觉的方法

type CatC struct {
C *Cat
}

func (cc *CatC) Sleep() {
fmt.Println("小猫睡觉 ")
}
func main() {
c := &Cat{}
c.Eat()
cb := &CatB{}
cb.Eat()
cb.Sleep()
cc := &CatC{}
cc.Sleep()
}

迪米特法则

依赖第三方进行解耦

接口的意义

接口的意义就是实现多态的思想,我们可以根据interface类型来设计API接口,那么这种API接口的适应能力不仅能够适应当下所实现的全部模块,也适应未来实现的模块来进行调用。**调用未来**也许是接口最大的意义所在,良好的架构师可以针对interface进行设计一套框架,在未来的许多年后仍然可以适用

设计模式

创建型模式

image.png
思想就是可以通过添加一层工厂模块,来做到业务逻辑层和基础模块层之间的耦合,避免业务逻辑层对基础模块层的直接依赖。

简单工厂模式

简单工厂模式并不属于GoF的23种设计模式,它是开发者自发认为的一种非常简易的设计模式,其角色和职责如下:

  • 工厂:简单工厂模式的核心,它负责创建所有实例的内部逻辑。工厂类可以被外界直接调用,创建所需要的产品对象
  • 抽象产品:简单工厂模式所创建的所有对象的分类,它负责描述实例所公有的公共接口
  • 具体产品:简单工厂模式所创建的具体实例对象

设计模式类图
image.png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
// 抽象层

type Fruit interface {
Show()
}

// 实现层

type Apple struct {
Fruit
}

func (a *Apple) Show() {
fmt.Println("i am apple")
}

type Banana struct {
Fruit
}

func (b *Banana) Show() {
fmt.Println("i am banana")
}

type Pear struct {
Fruit
}

func (p *Pear) Show() {
fmt.Println("i am pear")
}

// 工厂模块

type Factory struct {
}

func (f *Factory) CreateFruit(kind string) Fruit {
var fruit Fruit
if kind == "apple" {
fruit = new(Apple)
} else if kind == "banana" {
fruit = new(Banana)
} else if kind == "pear" {
fruit = new(Pear)
}
return fruit
}

// 逻辑层

func main() {
factory := new(Factory)
apple := factory.CreateFruit("apple")
apple.Show()
banana := factory.CreateFruit("banana")
banana.Show()
}

优缺点
优点:

  • 实现了对象创建和使用的分离
  • 不需要记住具体类名,记住参数就可以,减少使用者记忆量

缺点:

  • 对工厂职责过重,一旦不能工作,系统会受到影响
  • 增加系统中类的个数,复杂度和理解度增加
  • 违反“开闭原则”,添加新产品需要修改工厂逻辑,工厂越来越复杂

适用场景

  • 工厂类负责创建的对象比较少,由于创建的对象较少,不会造成工厂方法中的逻辑太复杂
  • 客户端只知道传入工厂类的参数,对于如何创建对象并不关心

工厂方法模式

  • 抽象工厂:工厂的核心,任何工厂类都必须实现这个接口
  • 工厂:具体工厂是抽象工厂的一个实现,负责实例化产品对象
  • 抽象产品:工厂方法模式所创建的所有对象的父类,它负责描述所有实例所共有的公共接口
  • 具体产品:工厂方法模式所创建的具体实例对象

简单工厂+开闭原则=工厂
模式类图
image.png

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

type fruit interface {
show()
}

// 工厂类(抽象的接口)

type AbstractFactory interface {
CreateFruit() fruit
}

// 基础模块层

type apple struct {
fruit
}

func (a *apple) show() {
fmt.Println("i am apple")
}

type banana struct {
fruit
}

func (b *banana) show() {
fmt.Println("i am banana")
}

type pear struct {
fruit
}

func (p *pear) show() {
fmt.Println("i am pear")
}

type AppleFactory struct {
AbstractFactory
}

func (fac *AppleFactory) CreateFruit() fruit {
var f fruit
f = new(apple)
return f
}

type BananaFactory struct {
AbstractFactory
}

func (fac *BananaFactory) CreateFruit() fruit {
var f fruit
f = new(banana)
return f
}

type PearFactory struct {
AbstractFactory
}

func (fac *PearFactory) CreateFruit() fruit {
var f fruit
f = new(pear)
return f
}

// 业务逻辑层
func main() {
//需求1 需要一个具体的苹果对象
//需要一个具体的苹果工厂
var a AbstractFactory
a = new(AppleFactory)
//生产一个具体的水果
var apple fruit
apple = a.CreateFruit()
apple.show()
}

优缺点
优点:

  • 不需要记住具体类名,甚至连具体参数都不用记忆
  • 实现了对象创建和使用的分离
  • 系统的可拓展性也变得非常好,不需要修改接口和原类
  • 对于新产品的创建,符合开闭原则

缺点:

  • 增加系统中的类的个数,复杂度和理解度增加
  • 增加了系统的抽象性

适用场景

  1. 客户端不知道它所需要的对象的类
  2. 抽象工厂类通过其子类来指定创建哪个对象

抽象工厂模式

工厂模式中的每个工厂只生产一类产品,可能会导致系统中存在大量的工厂类,势必会增加系统的开销。因此,可以将一些相关的产品组成一个“产品族”,从而由同一个工厂来统一生产。

  • 抽象工厂:它声明了一组用于创建一组产品的方法,每一个方法对应一种产品
  • 具体工厂:它实现了在抽象工厂中声明的创建产品的方法,生成一组具体产品,这些产品构成了一个产品族,每一个产品都位于某在产品等级结构中
  • 抽象产品:它为每种产品声明接口,在抽象产品中声明了产品所具有的业务方法
  • 具体产品:它定义具体工厂生产的具体产品对象,实现抽象产品接口中声明的业务方法

模式例图
image.png

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

type AbstractApple interface {
ShowApple()
}

type AbstractBanana interface {
ShowBanana()
}

type AbstractPear interface {
ShowPear()
}

// 抽象的工厂

type AbstractFactory interface {
CreateApple() AbstractApple
CreateBanana() AbstractBanana
CreatePear() AbstractPear
}

// 实现层

type ChinaApple struct {
}

type ChinaBanana struct {
}

type ChinaPear struct {
}

type ChinaFactory struct {
}

func (ca *ChinaApple) ShowApple() {
fmt.Println("china apple")
}

func (cb *ChinaBanana) ShowBanana() {
fmt.Println("china banana")
}

func (cp *ChinaPear) ShowPear() {
fmt.Println("china pear")
}

func (cf *ChinaFactory) CreateApple() AbstractApple {
var apple AbstractApple
apple = new(ChinaApple)
return apple
}

func (cf *ChinaFactory) CreateBanana() AbstractBanana {
var b AbstractBanana
b = new(ChinaBanana)
return b
}

func (cf *ChinaFactory) CreatePear() AbstractPear {
var p AbstractPear
p = new(ChinaPear)
return p
}

func main() {
// 需要中国的水果
//1. 创建中国工厂
var cF AbstractFactory
cF = new(ChinaFactory)
var cApple AbstractApple
cApple = cF.CreateApple()
cApple.ShowApple()
var cBanana AbstractBanana
cBanana = cF.CreateBanana()
cBanana.ShowBanana()
var cPear AbstractPear
cPear = cF.CreatePear()
cPear.ShowPear()
}

优缺点
优点

  1. 用于工厂方法模式的优点
  2. 当一个产品族中的多个对象被设计成一起工作时,它能够保证客户端始终只使用同一个产品族中的对象
  3. 增加新的产品族很方便,无须修改已有系统,符合“开闭原则”

缺点

  1. 增加新的产品等级结构麻烦,需要对原有系统进行较大的修改,甚至需要修改抽象层代码,这显然会带来较大的不便,违背了”开闭原则“

适用场景

  1. 系统中有多于一个的产品族,而每次只使用其中某一产品族,可以通过配置文件等方式来使得用户可以动态改变产品族,也可以很方便地增加新的产品族
  2. 产品等级结构稳定。设计完成之后,不会像系统中增加新的产品等级结构或者删除已有的产品等级结构

三种工厂的区别

  • 简单工厂:一个工厂负责创建所有产品,违反开闭原则,添加新产品需要修改工厂逻辑,工厂会变得越来越复杂
  • 工厂:一个工厂创建一个产品,系统的可扩展性非常好,无需修改接口和类,但是系统中类的个数变多,复杂度和理解度增加
  • 抽象工厂:一个工厂创建一系列(同一个产品族)的产品,增加新的产品族很方便,无需修改已有系统,符合开闭原则,增加新的产品等级结构很麻烦,需要对原有系统进行较大的修改,违背了开闭原则,相当于在工厂方法的模式下进行了折中,如果产品结构等级稳定,那么就相当于完全遵循开闭

单例模式

保证一个类、只有一个实例存在,同时提供能对该实例加以访问的全局访问方法。
要解决的问题是:保证一个类永远只能有一个对象,且该对象的功能依然能被其他模块使用。
类图:
image.png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
//三个要点
// 某个类只能有一个实例
// 它必须自行创建这个实例
// 必须自行向整个系统提供这个实例

// 总结:一个类永远只能有一个对象,这个对象还能被系统的其他模块使用

//1. 因为这个类必须保证私有化 所以首字母要小写
type singelton struct{}

// 2.指针只能指向这个唯一对象,但是这个指针不能改变方向,也必须小写
var instance *singelton = new(singelton)

// 3.对外提供一个方法来获取到这个对象 把instance的写权限去掉 只暴露读权限

func GetInstance() *singelton {
return instance
}

func (s *singelton) DoSomeThing() {
fmt.Println("Do something")
}

func main() {
s := GetInstance()
s.DoSomeThing()
}

// 懒汉式的单例模式:只有被第一次访问的时候 才给instance赋值 平常为nil
// 但是懒汉式可能有并发问题: 同时有两个Getinstance同一时刻首次调用 那么就会出现两个instance 可以加锁解决
// 锁的粒度太大了 可以通过一个uint的标记 使用atomic.LoadUnit函数判断 不用每次访问都加锁
// 或者直接使用sync.Once进行new 这是对atomic.LoadUint的封装

优缺点
优点:

  1. 单例模式提供了对唯一实例的受控访问
  2. 节约系统资源,由于在系统内存中只存在一个对象

缺点:

  1. 扩展性差,单利模式中没有抽象层
  2. 单例类的职责过重

适用场景

  1. 系统只需要一个实例对象,比如系统要求提供一个唯一的序列号生成器或者资源管理器,或者需要考虑资源消耗太大而只允许创建一个对象
  2. 客户调用类的单个实力只允许使用一个公共访问点,除了该节点之外,不能通过其他途径访问该实例

结构型模式

代理模式

Proxy模式又叫代理模式,可以为其他对象提供一种代理(Proxy)以控制对这个对象的访问。
所谓代理,是指具有与代理元(被代理的对象)具有相同的接口的类,客户端必须通过代理与被代理的目标类进行交互

  • 抽象主题:真实主题与代理主题的共同接口
  • 真实主题:定义了代理角色所代表的真实对象
  • 代理主题角色:含有对真实主题角色的引用,代理角色通常在客户端调用给真实主题对象之前或者之后执行某些操作

image.png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
type Goods struct {
Kind string
Fact bool
}

// 抽象层
type Shopping interface{
Buy(goods *Goods)
}

// 实现层
type KoreaShopping struct {
}

func (ks *KoreaShopping) Buy(good *Goods) {
fmt.Println("go korea buy", good.Kind)
}

type AmericaShopping struct {
}

func (as *AmericaShopping) Buy(good *Goods) {
fmt.Println("go america buy", good.Kind)
}

type AfricaShopping struct {
}

func (as *AfricaShopping) Buy(good *Goods) {
fmt.Println("go africa buy", good.Kind)
}

// 海外代理

type OverSeasProxy struct {
shopping Shopping
}

func (op *OverSeasProxy) Buy(good *Goods) {
//1.辨别真伪
if op.distinguish(good) == true {
op.shopping.Buy(good)
op.check(good)
}
//2.调用具体需要被代理的Buy方法
//3.海关安检
}

// 辨别真伪
func (op *OverSeasProxy) distinguish(goods *Goods) bool {
fmt.Println("对", goods.Kind, "进行了辨别真伪")
if goods.Fact == false {
fmt.Println("发现假货")
}
return goods.Fact
}

func (op *OverSeasProxy) check(good *Goods) {
fmt.Println("通过海关")
}

func NewProxy(s Shopping) Shopping {
return &OverSeasProxy{s}
}

func main() {
g1 := Goods{
Kind: "韩国面膜",
Fact: true,
}
g2 := Goods{
Kind: "苹果",
Fact: false,
}
var k Shopping = new(KoreaShopping)
var p Shopping
p = NewProxy(k)
p.Buy(&g1)
var a Shopping = new(AmericaShopping)
p = NewProxy(a)
p.Buy(&g2)
}

优缺点
优点:
1.能够协调调用者和被调用者,在一定程度上降低了系统的耦合度
2.客户端可以针对抽象主题角色进行编程,符合开闭原则,系统具有姣好的灵活性和可拓展性
缺点:
1.实现较为复杂
适用场景
为其他对象提供一种代理以控制对这个对象的访问

装饰模式

装饰模式(Decorator)用来动态地给一个对象增加一些额外的职责,比生成子类实现更加灵活
装饰模式关注于在一个对象上动态的添加方法,然而代理模式关注于控制对对象的访问。换句话说,用代理模式,代理类可以对它的客户隐藏一个对象的具体信息。因此,当使用模式的时候,我们常常在一个代理类中创建一个对象的实例。并且当我们使用装饰器模式的时候,我们通常的做法是将原始对象作为一个参数传给装饰者的构造器。

  • 抽象构件:是具体构件和抽象装饰类的共同父类,声明了在具体构件中实现的业务方法,它的引入可以使客户端以一致的方法处理未被装饰的对象以及装饰之后的对象,实现客户端的透明操作
  • 具体构件:它是抽象构建类的子类,用于定义具体的构件对象,实现了在抽象构件中声明的方法,装饰器可以给它增加额外的职责

例图
image.png

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

// 抽象的装饰器,装饰器的基础类

type Decorator struct {
phone Phone
}

func (d *Decorator) Show() {

}

// 具体的构件

type Huawei struct {
}

func (hw *Huawei) Show() {
fmt.Println("it is a huawei phone")
}

type Xiaomi struct {
}

func (xm *Xiaomi) Show() {
fmt.Println("it is a xiaomi phone")
}

type MoDecorator struct {
Decorator
}

func (md *MoDecorator) Show() {
md.phone.Show()
fmt.Println("it is a mo phone")
}

func NewMoDecorator(p Phone) Phone {
return &MoDecorator{Decorator{
p,
}}
}

type KeDecorator struct {
Decorator
}

func (kd *KeDecorator) Show() {
kd.phone.Show()
fmt.Println("it is a ke phone")
}

func NewKeDecorator(p Phone) Phone {
return &KeDecorator{Decorator{p}}
}

func main() {
var hw Phone
hw = new(Huawei)
hw.Show()
var mo Phone
mo = NewMoDecorator(hw)
mo.Show()
var ke Phone
ke = NewKeDecorator(hw)
ke.Show()
}

优缺点
优点:

  1. 对于扩展一个对象的功能,装饰模式比继承更加灵活,不会导致类的个数急剧增加
  2. 可以通过一种动态的方式来扩展一个对象的功能,从而实现不同的行为
  3. 可以对一个对象进行多次装饰
  4. 具体构建类与具体装饰类可以独立变化,符合开闭

缺点:

  1. 使用装饰模式进行系统设计时将产生很多小对象,大量小对象的产生势必会占用更多的系统资源,影响程序的性能
  2. 装饰模式提供了一种比继承更加灵活激动的解决方案,同时意味着排错也比较困难

适用场景

  1. 动态、透明的方式给单个对象添加职责
  2. 当不能采用继承的方式对系统进行拓展或者采用继承不利于系统拓展和维护时可以使用装饰模式

装饰与代理的区别

装饰器模式关注于在一个对象上动态的添加方法,然而代理模式关注于控制对对象的访问。换句话说,用代理模式,代理类可以对它的客户隐藏一个对象的具体信息。

适配器模式

将一个类的接口转换成客户希望的另外一个接口。使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。

  • 目标抽象类:定义客户所需接口,可以是具体类也可以是抽象接口
  • 适配器类:可以调用另一个接口,作为一个转换器,让目标抽象类和适配者类进行适配
  • 适配者类:被适配的角色,定义了一个已经存在的接口,这个接口需要适配

image.png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
type V5 interface {
Use5V()
}
type V220 struct {
}

type phone struct {
v V5
}

func (v *V220) Use220V() {
fmt.Println("使用220V的电压 ")
}

type Adapter struct {
v220 *V220
}

func (a *Adapter) Use5V() {
fmt.Println("使用适配器,以220V的电压充电")
a.v220.Use220V()
}

func NewPhone(v V5) *phone {
return &phone{v}
}

func NewAdapter(v220 *V220) *Adapter {
return &Adapter{v220}
}

func (p *phone) Charge() {
fmt.Println("Phone 进行了充电")
p.v.Use5V()
}

func main() {
phone := NewPhone(NewAdapter(&V220{}))
phone.Charge()
}

优缺点
优点:

  1. 将目标类和适配者类解耦,通过引入一个适配器类来重用现有的适配者类,无需修改原有结构
  2. 增加了类的透明性和复用性,将具体的业务实现封装在适配者类中,对于客户端类而言是透明的,而且提高了适配者的复用性,同一个适配者类可以在多个不同的系统中复用
  3. 灵活性和扩展性都很好,可以很方便地更换适配器,符合开闭原则

缺点:
适配器中置换适配者类的某些方法比较麻烦
适应场景
1.系统需要使用一些现有的类,而这些类的接口不符合系统的需要,甚至没有这些类的源代码
2.想创建一个可以重复使用的类,用来与一些彼此之间没有太大关联的一些类,包括一些可能在将来引进的类一起工作

外观模式

外观模式(Facade),为一组具有类型功能的类群,比如类库,子系统等等,提供一个一致的简单的界面

  • 外观角色:为调用方,定义简单的调用接口
  • 子系统角色:功能提供方,指提供功能的类群

image.png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
type SubSystemA struct {
}

type SubSystemB struct {
}

type SubSystemC struct {
}

type SubSystemD struct {
}

func (sa *SubSystemA) MethodA() {
fmt.Println("sub method a")
}

func (sb *SubSystemB) MethodB() {
fmt.Println("sub method b")
}

func (sc *SubSystemC) MethodC() {
fmt.Println("sub method c")
}

func (sd *SubSystemD) MethodD() {
fmt.Println("sub method d")
}

// 外观类
type Facade struct {
a *SubSystemA
b *SubSystemB
c *SubSystemC
d *SubSystemD
}

func (f *Facade) MethodOne() {
f.a.MethodA()
f.b.MethodB()
}

func (f *Facade) MethodTwo() {
f.c.MethodC()
f.d.MethodD()
}

func main() {
f := Facade{}
f.MethodOne()
}

优缺点
优点:
1.它对客户端屏蔽了子系统组件,减少了客户端所需处理的对象数目,并使得子系统使用起来更加容易。通过引入外观模式,客户端代码将变得很简单,与之关联的对象也很少。
2.它实现了子系统与客户端之间的松耦合关系,这使得子系统的变化不会影响到调用它的客户端,只需要调整外观类即可
3.一个子系统的修改对其他子系统没有任何影响
缺点;
1.不能很好的限制客户端直接使用子系统类,如果对客户端访问子系统类做太多的限制则减少了可变性和灵活性
2.如果设计不当,增加新的子系统可能需要修改外观类的源代码,违背了开闭原则
适用场景

  1. 复杂系统需要简单入口使用
  2. 客户端程序与多个子系统之间存在很大的依赖性
  3. 在层次化结构中,可以使用外观模式定义系统中每一层的入口,层与层之间不直接产生联系

行为型模式

模板方法模式

  • 抽象类:在抽象类中定义了一系列基本操作,可以是具体的,也可以是抽象的,每一个基本操作对应算法的一个步骤,在其子类中可以重定义或者实现这些步骤
  • 具体子类:是抽象类的子类,用于实现在父类中声明的抽象基本操作以完成子类特定算法的步骤,也可以覆盖在父类中已经实现的具体操作

image.png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
// 抽象类 做饮料

type Beverage interface {
BoilWater()
Brew()
PourInCup()
AddThings()
}

// 封装一套流程模板
type template struct {
b Beverage
}

func (t *template) MakeBeverage() {
if t == nil {
return
}
t.b.BoilWater()
t.b.Brew()
t.b.PourInCup()
t.b.AddThings()
}

type MakeCoffee struct {
template
}

func (mc *MakeCoffee) BoilWater() {
fmt.Println("boil the water")
}

func (mc *MakeCoffee) Brew() {
fmt.Println("use boiled water to brew")
}

func (mc *MakeCoffee) PourInCup() {
fmt.Println("pour the coffee to the cup")
}

func (mc *MakeCoffee) AddThings() {
fmt.Println("add sugar")
}

func NewMakeCoffee() *MakeCoffee {
m := new(MakeCoffee)
m.b = m
return m
}

func main() {
makeCoffee := NewMakeCoffee()
makeCoffee.MakeBeverage()
}

优缺点
优点:
1.在父类中形式化地定义一个算法,而由它的子类来实现细节的处理,在子类实现详细的处理算法时不会改变算法中步骤的执行顺序
2.模板方法是一种代码复用技术,他提取了类库中的公共行为,将公共行为放在父类中
3.可实现一种反向控制结构,通过子类覆盖父类的钩子方法来决定某一特定步骤是否需要执行
4.不同的子类可以提供基本方法的不同实现,更换和增加新的子类很方便,符合单一职责原则和开闭原则
缺点:
需要为每一个基本方法的不同实现提供一个子类,如果父类中可变的基本方法太多,将会导致类的个数增加,系统更加庞大,设计也更加抽象。
适用场景
1.具有同一的操作步骤或操作过程
2.具有不同的操作细节
3.存在多个具有同样操作步骤的应用场景,但某些具体的操作系统却各不相同
在抽象类中统一操作步骤,并规定好接口;让子类实现接口。这样可以把各个子类和操作步骤解耦合

命令模式

讲一个请求封装成一个对象,从而让我们可用不同的请求对客户进行参数化;对请求排队或者记录请求日志,以及支持可撤销的操作。命令模式是一种对象行为型模式,其别名为动作模式。命令模式可以将请求发送者和接收者完全解耦,发送者与接收者之间没有直接引用关系,发送请求的对象只需要知道如何发送请求,而不比知道如何完成请求。

  • 抽象命令类:一个抽象类或者接口,通过这些方法可以调用请求接收者的相关操作
  • 具体命令类:具体命令类是抽象命令类的子类,实现了在抽象命令类中声明的方法,它对应具体的接收者对象,将接收者对象的动作绑定其中
  • 调用者:请求发送者,通过命令对象来执行请求
  • 接收者:接收者执行与请求相关的操作

image.png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
type Doctor struct {
}

func (d *Doctor) treatEye() {
fmt.Println("doctor treat eye")
}

func (d *Doctor) treatMouth() {
fmt.Println("doctor treat mouth")
}

type Command interface {
Treat()
}

type CommandTreatEye struct {
d *Doctor
}

func (cmd *CommandTreatEye) Treat() {
cmd.d.treatEye()
}

type CommandTreatMouth struct {
d *Doctor
}

func (cmd *CommandTreatMouth) Treat() {
cmd.d.treatMouth()
}

type Nurse struct {
CmdList []Command
}

func (n *Nurse) Notify() {
if len(n.CmdList) == 0 {
return
}
for _, cmd := range n.CmdList {
cmd.Treat()
}
}

func main() {
doctor := new(Doctor)
cmdEye := CommandTreatEye{doctor}
cmdMouth := CommandTreatMouth{doctor}
nurse := new(Nurse)
nurse.CmdList = append(nurse.CmdList, &cmdEye)
nurse.CmdList = append(nurse.CmdList, &cmdMouth)
nurse.Notify()
}

优缺点
优点:
1.降低系统的耦合度
2.新的命令很容易添加到系统中,满足开闭原则
3.可以比较容易地设计一个命令队列或者宏命令
缺点:
使用命令模式可能会导致某些系统有过多的具体命令类,因为针对每一个对请求接收者的调用操作都需要设计一个具体命令类,因此在某些系统中可能需要提供大量的具体命令类,这将影响命令模式的使用
适用场景
1.系统需要将请求调用者和请求接收者解耦,使得调用者和接收者不直接交互。请求调用者无需知道接收者的存在,也许无知道接收者是谁
2.系统需要在不同的时间指定请求、将请求排队和执行请求
3.系统需要将一组操作组合在一起形成宏命令

策略模式

  • 环境类:环境类是使用算法的角色,它在解决某个问题时可以采用多种策略,在环境类中维持一个对抽象策略类的引用实例,用于定义所采用的策略
  • 抽象策略类:它为所支持的算法声明了抽象方法,是所有策略类的父类,可以是抽象类或者具体类,也可以是接口
  • 具体策略类:它实现了在抽象策略类中声明的算法,在运行时,具体策略类将覆盖在环境类中定义的抽象策略类,使用一种具体的算法实现某个业务处理

例图
image.png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
type WeaponStrategy interface {
UseWeapon()
}

type AK47 struct {
}

func (ak *AK47) UseWeapon() {
fmt.Println("使用AK47战斗")
}

type Knife struct {
}

func (k *Knife) UseWeapon() {
fmt.Println("使用匕首战斗")
}

type Hero struct {
strategy WeaponStrategy
}

func (h *Hero) SetWeaponStrategy(s WeaponStrategy) {
h.strategy = s
}

func (h *Hero) Fight() {
h.strategy.UseWeapon()
}

func main() {
hero := Hero{}
hero.SetWeaponStrategy(new(AK47))
hero.Fight()
hero.SetWeaponStrategy(new(Knife))
hero.Fight()
}

优缺点
优点:
1.策略模式提供了对开闭原则的完美支持,用户可以在不修改原有系统的基础上选择算法或者行为
2.使用策略模式可以避免多重条件选择语句
3.策略模式提供了一种算法的复用机制
缺点:
1.客户端必须知道所有的策略类,并自行决定使用哪一个策略类
2.策略模式将造成系统产生很多具体策略类,任何细小的变化都将导致系统要增加一个新的具体策略类
适用场景
准备一组算法,并将每一个算法封装起来,使得它们可以互换

观察者模式

观察者模式是用于建立一种对象与对象之间的依赖关系,一个对象发生改变时将自动通知其他对象,其他对象将相应作出翻译。在观察者模式中,发生改变的对象称为观察目标,而被通知的对象称为观察者,一个观察目标可以对应多个观察者,而且这些观察者之间可以没有任何相互联系,可以根据需要增加和删除观察者,使得系统更易于扩展。

  • 抽象主题:被观察的对象
  • 具体主题:被观察者的具体实现
  • 观察者:接口或者抽象类
  • 具体观察者:观察者的具体实现

image.png

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

type Notifier interface {
AddListener(l Listener)
RemoveListener(l Listener)
Notify()
}

type StuZhang3 struct {
Badthing string
}

func (s *StuZhang3) OnTeacherComing() {
fmt.Println("zhang3 stop", s.Badthing)
}

type StuZhao4 struct {
Badthing string
}

func (s *StuZhao4) OnTeacherComing() {
fmt.Println("zhao4 stop", s.Badthing)
}

type StuWang5 struct {
Badthing string
}

func (s *StuWang5) OnTeacherComing() {
fmt.Println("wang5 stop", s.Badthing)
}

type ClassMonitor struct {
listenerList []Listener
}

func (m *ClassMonitor) AddListener(l Listener) {
m.listenerList = append(m.listenerList, l)
}

func (m *ClassMonitor) RemoveListener(l Listener) {
for index, li := range m.listenerList {
if li == l {
m.listenerList = append(m.listenerList[:index], m.listenerList[index+1:]...)
break
}
}
}

func (m *ClassMonitor) Notify() {
for _, listener := range m.listenerList {
listener.OnTeacherComing()
}
}

func main() {
s1 := &StuZhang3{
Badthing: "抄作业",
}
s2 := &StuZhao4{
Badthing: "玩手机",
}
s3 := &StuWang5{
Badthing: "看别人玩手机",
}
classMonitor := new(ClassMonitor)
classMonitor.AddListener(s1)
classMonitor.AddListener(s2)
classMonitor.AddListener(s3)
classMonitor.Notify()
}

优缺点
优点

  1. 观察者模式可以实现表现层和数据逻辑层的分离,定义了稳定的消息更新传递机制,并抽象了更新接口,使得可以有各种各样不同的表示层充当具体观察者角色
  2. 观察者模式在观察目标和观察者之间建立了一个抽象的耦合。观察目标只需要维持一个抽象观察者的集合,不需要了解其具体的观察者
  3. 观察者模式支持广播通信,观察目标会向所有已注册的观察者对象发送通知,简化了一对多系统设计的难度
  4. 观察者模式满足开闭原则

缺点

  1. 如果一个观察者对象有很多直接和间接的观察者,将所有的观察者都通知到会花费很多时间
  2. 如果在观察者和观察目标之间存在循环依赖,系统可能会发生崩溃
  3. 观察者模式没有响应的机制让观察者知道所观察到的对象是怎样发生变化的

适用场景

  1. 一个抽象模型有两个方面,其中一个方面依赖于另一方面,将这两个方面封装在独立的对象中使得它们可以各自独立地改变和复用
  2. 一个对象的改变将导致一个或者多个其他对象也发生改变
  3. 需要在系统中创建一个触发链路,A对象的行为影响B,B对象的行为影响C

toms初识

tmos一个特征和功能的集和,为了满足当今市场而出现的一个代理、高性能的一系列操作系统和固件,他们都在big-ip硬件设备上或BIG-IP虚拟版中运行

基于包设计和基于代理(全权代理)设计的区别
- 基于包:这些网络设备都是网络数据流中间的节点,并不是数据交互流的终点或起点,并且这些设备只需要满足部分协议而不是整个协议栈,比如一个位于第三层(IP)的设备,可能只需要会读写IP地址TCP地址即可,至于第二层第一层的协议将与其无关。现在这些设备越来越智能了,可以修改tcp数据流,甚至能修改tcp数据包的头部(seq ack)字段,而这些是通过一个状态跟踪引擎来实现的这些设备可以识别出哪些是单独的http请求(第一次的)这些基于包设计的设备是比后者快的,而且也没有那么复杂,最根本的原因就是它们只需要理解一部分协议

- 基于代理: 全权代理和基于包的设计刚好是相反的,它需要理解整个网络传输的协议栈,同时他们也是协议的发起者和接收者,而一个代理服务器和客户端之间的通信与一个代理服务器和后段服务器之间的通信是不同的(反向代理、正向代理)
二者之间的矛盾基于代理的设计比基于包的设计更加智能,但是目前来说基于代理的设计性能更强。当流量激增的时候,基于包管理的性能也会被限制,所以在企业进行选型的时候,二者都可以被考虑

什么是tmos
- 模块的集和 每个模块提供一部分功能 比如arp ip tcp各自有各自的模块,每个模块都是自称一体的,这样将可以减少系统的复杂性,类似于搭积木一样,当需要增加对某个协议的支持时,我再去构建这么一个单独的模块
- 自主&独立的 tmos具有自己的CPU 内存 和外围设备的接口,当一个基于tmos的设备受到数据包的时候,这些线路的子管理系统是完全在tmos中独立的,linux是不参与这个过程的,也就是说tmos并不与linux操作系统发生耦合,linux只用来管理程序
- 实施操作系统 (非抢占式的操作系统) 对于tmos这样高性能、大流量的操作系统来说,抢占式的os会浪费掉cpu的部分性能(高优先级插队),tmos的设计是每个组件执行必要的操作,然后把cpu让出去给下一个组件运行。这样将消除了中断、上下文切换等cpu开销,并且让程序有序运行
- 基于软件与硬件 它可以从软件和硬件两方面来解决问题,如果是难度比较高的,可能优先走的就是软件,如果是资源密集的,那么优先丢给硬件执行
- 事件驱动 因为tmos是模块化并且实时的,也就意味着它能够在任何时间内切换任务,也能够在短时间内改变性能来满足当前的需求
- 状态检测 tmos的内核是基于高速代理的,
- 动态数据包筛选 tmos能够筛选数据包,它有一个重要优势就是全权代理隐藏了后端服务的网络对战,f5的iRules允许动态控制应用程序流量,可以重定向,动态过滤,或者阻止流量等等
以上:tmos三一个功能强大、适应性强的解决反感,模块化与字包含、实时、事件驱动的操作系统相结合,为tmos提供了前所未有的功能
tmos体系中的特殊模块
tcp express 基于包管理的设计无法提供这个功能,tcp express包括了 延迟和选择性确认、显示拥塞统治、快重传,拥塞避免等等算法,tmos基本上把tcp能优化的地方全部进行了优化,这才能实现显著的加速

tmm(多线程进程):流量管理微内核处理big-ip系统上的所有负载均衡流量,在tmos中作为额u一个实时用户进程运行,cpu和内存资源在big-ip配置中明确配置

影响tmm使用cpu资源的因素

每个处理器的内核

每个核心的超线程数

big-ip版本

big-ip系统许可的模块

单cpu

单核的cpu使用率
在big-ip 11.5.0之前,每个cpu内核被分配一个单独的tmm实例,每个内核处理数据平面任务(tmm特定)和控制平面任务(非tmm特定) 。从big-ip11.5.0开始,数据平面任务和控制平面任务都采用英特尔超线程技术在CPU的系统上使用单独的逻辑内核,偶数编号给TMM,奇数编号给其他进程。

Map详解

什么是Map

基本上每种计算机语言里面都会内置一个map类型,map是一个由一组key,value组成的数据类型,并且同一个key只会出现一次。同时map支持增删改查四种操作,map的主要实现方式有两种,分别是哈希查找表(hash table)和搜索树(search tree)

哈希表:使用一个哈希函数将key分配到不同的bucket(桶,可以理解成数组中的索引),开销主要是在哈希函数的计算和数组的常数访问时间,很多场景下,我们可以简单的把哈希表的时间复杂度看成O(1)。哈希表通常还会有一个碰撞的问题,所谓的哈希碰撞就是多个key被哈希函数分配到了同一个bucket。一般有两种解决方法:链表法和开放地址法。链表法是将一个bucket实现程一个链表,落在同一个bucket中的key会插入这个链表。而开发地址法则是在发生碰撞之后,通过一定的规律,在空着的bucket里面挑选,用来放置新的key

搜索树:一般使用自平衡二叉树,比如AVL树和红黑树

二者的区别是,自平衡搜索树的时间复杂度最低为O(logN),而哈希表的最差情况是O(N),哈希表平均查找效率是O(1)。 还有一个区别是,遍历自平衡搜索树,返回的key是有序的,而哈希表则是乱序的

Go Map

Go中的Map使用的是哈希表,并且使用链表的方式解决哈希冲突

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// A header for a Go map.
type hmap struct {
// 元素个数,调用 len(map) 时,直接返回此值
count int
flags uint8
// buckets 的对数 log_2
B uint8
// overflow 的 bucket 近似数
noverflow uint16
// 计算 key 的哈希的时候会传入哈希函数
hash0 uint32
// 指向 buckets 数组,大小为 2^B
// 如果元素个数为0,就为 nil
buckets unsafe.Pointer
// 等量扩容的时候,buckets 长度和 oldbuckets 相等
// 双倍扩容的时候,buckets 长度会是 oldbuckets 的两倍
oldbuckets unsafe.Pointer
// 指示扩容进度,小于此地址的 buckets 迁移完成
nevacuate uintptr
extra *mapextra // optional fields
}

B就是buckets数组的长度对数,也就是buckets数组长度为2^B,buckets里面存放着key-value对,buckets也是一个指针,类似于slice中指向数组的指针。

buckets指向的是下面这个东西

1
2
3
type bmap struct {
tophash [bucketCnt]uint8
}

编译之后就变成了

1
2
3
4
5
6
7
type bmap struct {
topbits [8]uint8
keys [8]keytype
values [8]valuetype
pad uintptr
overflow uintptr
}

其中bmap就是我们所说的哈希桶,每个桶里面最多装下8个key-value对,这些key之所以会落入同一个桶中,是因为他们经过哈希计算之后,得到的结果是一类的(哈希结果得到的后八位相同),之后会根据key计算出来的hash值的高8位来决定key到底落入桶内的那个位置(哪个槽)

其中每个bmap的key1…key8是存放在一起的,value1….value8是存放在一起的,这样的目的是为了节省因为内存对齐造成的空间浪费。

每个bucket设计成最多只能存放8个key-value对,如果有第九个key-value落入当前的bucket,则需要再构建一个bucket,然后通过overflow指针连接起来。

创建map

底层调用的是makemap函数,主要做的工作就是初始化hamp结构体的各种字段,比如计算B的大小,设置哈西种子hash0等等

哈希函数

map的一个关键点在于哈希函数的选择,在程序启动时会检测cpu是否支持aes,如果支持则使用aes hash,否则使用memhash。hash函数有加密型和非加密型,加密型的一般用于加密数据、数字摘要等,典型代表就是md5 sha1 sha256 aes256这种,非加密型的就是查找,而map就是使用的查找hash函数

key定位过程

key经过哈希计算后得到哈希值,共64bit,计算它落到那个桶的时候,会用到最后B个bit位,最后B个bit位的值,就是落入桶的序号、当两个不同的key落在同一个桶中,也就是发生了哈希冲突,解决手段是链表法,从前往后找到第一个空位,这样,在查找某个key的时候先找到对应的桶,然后再去遍历bucket里的key。

在槽内的查找过程:使用高八位的bit值,高八位的值就是槽位,如果在bucket中没找到,并且overflow不为空,还要继续去overflow bucket中查找,直到找到或是所有的key槽位都遍历完

综上,这是一个双重循环的过程,外层循环查找所有bucket和overflow bucket,内层循环遍历单个bucket的所有槽位

get

Go中读取map有两种语法,带comma和不带comma,当要查询的key不在map里,带comma的用法会返回一个bool型变量提示key是否在map中,而不带comma的语法则会返回一个对应类型的零值

遍历

本来map的遍历过程比较简单:遍历所有的bucket以及它后面挂的overflow bucket(第一层遍历),然后挨个遍历bucket中的所有cell(槽),每个bucket包含8个cell,从有key的cell中取出key value

但是现实并没有这么简单,因为扩容并不是一个原子的操作,每次最多只搬运两个bucket,所以如果触发了扩容操作,那么很长时间内,map状态都是处于一种中间态,有些bucket已经搬迁到新家,有些bucket还呆在老地方

因此,遍历如果发生在扩容的过程中,就会涉及到遍历新老bucket的过程

具体是遍历老的bucket,然后再遍历老的bucket裂变到新的bucket里的元素

赋值

调用的是mapassign函数,语法和插入key的过程一样,只不过前者的key在map在不存在,后者存在

具体过程

1.检查map标志位flgas,如果为1则说明其他协程在执行写操作,导致程序panic

2.如果map正在扩容,那么当key定位到了某个bucket后,需要确保这个bucket对应的老bucket完成了迁移,即老bucket的key都要迁移到新的bucket中之后,才能在新的bucket中进行插入或者更新的操作

删除

调用的是底层的mapdelete函数

1.检查标志位flags,如果发现标志位为1,说明其他协程在执行写操作,直接panic

2.计算key的哈希,找到落入的bucket,如果正在扩容中,直接完成一次扩容

3.同样是两层循环,找到key的具体位置,然后删掉

扩容

使用哈希表的目的是快速找到目标key,随着map中添加的key越来越多,key发生碰撞的概率也越来越大,当bucket中的8个cell倍塞满的时候,效率就是最低的,最理想的情况是一个bucket只装一个key,这样就能达到o1的效率,当然这样空间消耗太多了

触发扩容的条件

1.装载因子超过阈值6.5

2.overflow的bucket数量过多,当B<15如果overflow的数量大于2^B,当B>=15,如果overflow的数量大于2^15(装载因子比较小,map的插入和查找效率也很低,但是bucket的数量很多)

两种扩容方式

1.元素太多,bucket太少,将B+1,让bucket的数量翻倍

2.元素不多,但overflow bucket很多,说明很多bucket都没满,开辟一个新的bucket空间,将老bucket中的元素移动到移动到新的bucket,是的一个bucket中的key排列更加紧密

Slice详解

slice和数组的区别

众所周知,go是一门强类型的语言,什么是强类型呢?就是对类型要求非常严格(在运算时),所以go中的float不能和int进行运算,甚至int和int64也不能进行运算。

数组是指长度固定的数据集合,比如 [3] int 指的就是长度为3的int类型集合,它和[4] int是两个完全不同的类型,所以不能作比较,比较也是一种运算。

而slice则是动态数组,长度不固定,可以动态扩容,slice的类型和长度没有关系,所以不同的slice可以进行比较(但这个操作通常没有意义)

slice本质

slice其实就是一个结构体,里面有着对数组的封装,还有len和cap两个字段来描述数组的长度和容量

1
2
3
4
5
6
// runtime/slice.go
type slice struct {
array unsafe.Pointer // 元素指针 指向的其实是一个底层的数组
len int // 长度
cap int // 容量
}

如果要判断slice是否为空,要使用

len(slice)==0

而不能使用

slice==nil

因为slice=0判断的是数组内是否有元素,如果没有元素则为空,而slice==nil判断的是一整个结构体是否为Nil 如果我们用var 的方式声明,不会给slice分配内存,那么slice确实=nil。但如果我们使用make的方式进行声明,那么就会给slice分配内存,所以slice就!=nil了

同理的 要判断两个slice是否相同,不能简单的通过slice1==slice2进行判断,而是要循环切片进行判断

slice传参

在我们把slice作为参数传递出去的时候,传的是,这也就是为什么我们在被调函数中对数组进行append,在主调函数中看不到这个变化。但是如果直接通过下标的方式对slice进行修改,那么是可以反映到主调函数中的(因为下标修改是直接对底层的数组进行修改)

准确的来说,go中所有的参数传递,都是值传递,并没有引用传递,那有的同学可能就疑惑了,我传递map的话不是在被调函数中的改变可以反映到主调函数中吗?

因为进行函数调用的时候,slice类型会调用runtime.makeslice函数,这个函数的返回值类型是值,而map类型会调用runtime.makemap函数,这个函数的返回值类型是一个指针

slice的扩容过程

网上流传的版本是:当slice容量小于1024的时候,每次扩容翻倍,在1024长度之后,每次扩容1.25倍,而在1.18版本之后变为了,当容量小于256的时候,扩容为两倍,超过256,newcap=oldcap+(oldcap+3*256) /4

这个说法不对,或者说只对了一半

go 1.9.5源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// go 1.9.5 src/runtime/slice.go:82
func growslice(et *_type, old slice, cap int) slice {
// ……
newcap := old.cap
doublecap := newcap + newcap
if cap > doublecap {
newcap = cap
} else {
if old.len < 1024 {
newcap = doublecap
} else {
for newcap < cap {
newcap += newcap / 4
}
}
}
//
//内存对齐
capmem = roundupsize(uintptr(newcap) * ptrSize)
newcap = int(capmem / ptrSize)
}

go 1.18源码

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
// go 1.18 src/runtime/slice.go:178
func growslice(et *_type, old slice, cap int) slice {
// ……
newcap := old.cap
doublecap := newcap + newcap
if cap > doublecap {
newcap = cap
} else {
const threshold = 256
if old.cap < threshold {
newcap = doublecap
} else {
for 0 < newcap && newcap < cap {
// Transition from growing 2x for small slices
// to growing 1.25x for large slices. This formula
// gives a smooth-ish transition between the two.
newcap += (newcap + 3*threshold) / 4
}
if newcap <= 0 {
newcap = cap
}
}
}

//内存对齐
capmem = roundupsize(uintptr(newcap) * ptrSize)
newcap = int(capmem / ptrSize)
}

如果只看前半部分,那么网上的各种文章说的是对的,现实是,后半部分还对newcap做了一个内存对齐,这个和内存分配策略有关,进行内存对齐之后,新slice的容量>=未进行内存对齐之前的cap

new和make的区别

首先new和make都是Go内置的用来分配内存的函数,区别是make用来给slice map channel等引用类型分配内存,返回值是一个值类型,而new用来给数组、结构体值类型来分配内存,后者返回值是一个指针

Go web与Content-Type

首先要对http的工作方式有一定的了解,这里默认大家都会。这里只是随手写了点东西记录一下Content-Type的类型和内容。

http请求与响应

http request的具体组成

  • 请求行:请求类型+url+协议版本
  • 请求头:Host(表明请求目的地 主机域名) User-Agent(客户端的信息 由浏览器定义) Content-Type Content-Length等等
  • 空行
  • 请求体:实体数据

http response的具体组成

  • 状态行:由协议版本号、状态码和状态消息组成
  • 响应头: 客户端可以使用的一些信息 比如Date Content-Type Connection(是否为长链接) 等等
  • 空行
  • 响应体:实体数据

net/http

这个库里面主要是与http相关的一些API,可以分为客户端和服务端两个部分

  • 服务端相关:server serverMux Handler/HandlerFunc
  • 客户端相关: client response header request cookie

具体过程如下

首先服务端创建Listen Socket监听指定的端口,等待客户端请求到来,之后Listen socket接受客户端的请求,得到Client Socket,通过这个socket和客户端进行通信;每次处理客户端的请求时,首先从Client Socket读取HTTP协议头,如果是POST可能还要读取数据,然后交给相应的Handler处理,处理完毕之后准备好客户端需要的数据,通过Client Socket写回给客户端。

ListenAndServe函数的具体流程

  • 监听某个端口 如果有新连接进来 创建一个goroutine处理新的连接
  • 在goroutine中 将请求和响应分别封装为http.Request和http.ResponseWriter对象,将这两个对象作为参数传递给server.Handler的serveHTTP,这个server.handler就是listenandserve函数的参数(http.servemux)
  • 查找http.Request对象的url(在servemux中的map中进行) 然后执行
  • 监听之后内部有一个for循环不断接受请求,每个请求创建一个conn,最后创建一个goroutine

Content-Type初识

Content-Type 是http的首部字段,用于说明请求或者返回的消息body是以何种方式进行编码,在request header和response header里都存在,网页通过这个类型定义网络文件的类型和网页的编码,决定浏览器将以什么形式、什么编码读取这个文件。

常见的媒体格式类型如下

  • text/html html格式
  • text/plain 纯文本格式
  • text/xml xml格式
  • text/gif gif图片格式
  • text/jpeg jpg图片格式
  • text/png png图片格式

以application开头的媒体格式类型

  • application/xhtml+xml XHTML格式
  • application/xml XML格式
  • application/atom+xml Atom XML聚合格式
  • application/json JSON数据格式
  • application/pdf pdf格式
  • application/msword Word文档格式
  • application/octet-stream 二进制数据流格式
  • application/x-www-form-unlencoded 表单默认的提交数据格式(被编码为key/value的格式)

另一种常见的媒体格式的上传文件之时使用的

  • multipart/form-data 需要在表单中进行文件上传时,就需要使用到这个格式