Docker镜像优化

一 Docker镜像优化层面

  • 空间优化,也就是优化 Docker 镜像体积(也称呼为“镜像瘦身”)
  • 优化 Docker 镜像构建速度,是优化构建的时间

二 Docker镜像优化方式

    1. 选择最精简的基础镜像
    1. 合理规划镜像的层数
    1. 清理镜像构建的中间产物
    1. 优化网络需求
    1. 构建缓存
    1. 使用多阶段构建镜像
    1. 编写 .dockerignore 文件

上述中: 空间优化(1、3、6)时间优化(4、5、7)

三 时间优化

​ 时间层面的优化方式有网络优化、构建缓存、编写 .dockerignore 文件 3 种方式;

  1. 网络优化

    ​ 优化网络主要是为了让镜像下载或者依赖的安装、代码下载在网络通道上更加通畅。常用的优化方式例如:调整构建机器网络质量、配置就近的加速仓库地址等等。

  2. 构建缓存

    • 本地缓存

      ​ Docker 构建时下载基础镜像文件会进行缓存,所以构建缓存优化时,为了减少镜像的传输下载时间,建议使用固定的机器来专门进行镜像的构建。

    • 镜像分层缓存

      ​ Docker 的一大特色就是镜像的存储分层,在 dockerfile 中的每一个指令会对应到镜像的每一层,并且默认启用缓存,所以构建时每一层是否会缓存取决于三个关键因素:

      • 镜像父层没有发生变化

      • 构建指令不变

      • 添加文件校验和一致

      只要一个构建指令满足这三个条件,这一层镜像构建就不会再执行,而是直接利用之前构建的结果。

  3. 编写 .dockerignore 文件

    ​ 编写 .dockerignore 文件用于过滤构建过程中不必要的文件,或者创建单独的目录(如 .git 文件夹,markdown 文档等),从而减小目标镜像大小,加快构造速度。

四 空间优化

  1. 选择最精简的基础镜像

    ​ Docker 的仓库中存在同一个基础镜像的多个版本,为了优化 Docker 体积,我们通常会建议使用 Alpine 类型的版本,因为 Alpine 镜像和类似的其他镜像都经过了优化,其中仅包含最少、必须的软件包,所以它能够节省很多体积。

    ​ Alpine 类型的镜像被很多开发技术人员优先推荐。但是Alpine镜像也存在很多问题:

    • 使用 Alpine 镜像程序容易报错

      因为 Alpine 为了追求精简,很多依赖库都没有,需要一些依赖动态链接库的程序运行时就容易报错,比如 Go 的 cgo 调用。

    • 域名解析行为跟 glibc 有差异

      Alpine 镜像的底层库是 musl libc,域名解析行为跟标准 glibc 有差异,需要特殊作一些修复配置,并且有部分选项在 resolv.conf 中配置不支持。

    • 运行 bash 脚本不兼容

      因为没有内置 bash,所以运行 bash 的 shell 脚本会不兼容。

    ​ 除此之外,使用 Alpine 镜像还会导致时区不一致、无法通过 lxcfs 提升容器资源可见性等问题。所以,你在真正使用之前需要评估好 Alpine 的弊端和风险,如果一定需要使用 Alpine 镜像,最好的方式是先使用 Alpine 镜像做基础镜像,在项目直接使用 Alpine 镜像之前进行一次初始化,然后再用初始化后的版本作为通用的基础镜像。

  1. 合理规划镜像的层数

    ​ 除了精简的基础镜像,你还可以在编写 dockerfile 时,根据实际情况去合并一些指令,尽量减少镜像层级,以此来优化体积。

    ​ 在 dockerfile 中每执行一条指令,就会提交一次修改,这次修改会保存成一个只读层挂载到联合文件系统,上面层的文件如果和下面层有冲突或不同,会覆盖隐藏底层的文件,所以每增加一层,镜像大小就会增加,但是在 Docker1.10 后有所改变,只有 RUN、COPY、ADD 指令会创建层,其他指令会创建临时的中间镜像,不会直接增加构建的镜像大小 。

  1. 使用多阶段构建镜像

    ​ 将 dockerfile 的构建配置环境分为软件的编译环境和运行环境,这种方式就是多阶段构建思路。

位运算相关知识

前言:计算机是用补码进行计算与存储

  1. 对于有符号数而言,二进制的最高位是符号位 ,0表示正数, 1表示负数;

  2. 正数的原码、反码、补码都一样;

  3. 负数的原码是其正数符号位为1,其他位和正数的二进制位相同;负数的反码等于负数原码符号位不变,其他位按位取反;负数的补码等于负数的反码+1;

  4. go中的取反操作^,c、java、c++中取反操作~

  5. 按位与 &运算,两个全为1,结果为1,否则为0;

  6. 按位或运算,一个为1就为1;

  7. 按位异或^运算,两个不同,结果为1 ,相同则为0;

  8. 左移运算<<,符号位不变,低位补 0;

  9. 右移运算>>,低位溢出,符号位不变,并用符号位补溢出的高位;

  10. go中位清除&^

Linux小工具sshpass实现命令行密码验证功能

  1. 下载源码安装包

    1
    curl -O -L http://downloads.sourceforge.net/project/sshpass/sshpass/1.05/sshpass-1.05.tar.gz && tar xvzf sshpass-1.05.tar.gz
  2. 编译安装

    1
    2
    3
    cd sshpass-1.05
    ./configure
    sudo make install
  3. macOS也可使用brew安装

    1
    brew install hudochenkov/sshpass/sshpass
  4. 使用

    1
    2
    3
    4
    在二进制文件中使用,准备upload.sh文件,写入内容:

    #!/bin/bash
    sshpass -p 'pwd' scp -r ${sourceDir} ${username}@{IP}:${targetDir}

K8s一主两从搭建

零 服务器准备

IP 类别
172.16.56.102 master
172.16.56.103 Node1
172.16.56.104 Node2

一 准备工作

准备好一个主节点master的centos7.5以上的机器,准备相关相关环境的node节点机器

下载一个可以批量运维服务器的工具mobaXterm,这个工具暂时只支持windows,其下载地址为:

1
https://mobaxterm.mobatek.net/download-home-edition.html

二 初始化机器的配置信息[9步]

  1. 检查操作系统版本,确保操作系统版本是一致,并且大于7.5版本;

    1
    cat /etc/redhat-release

    输出:

    version

  2. 主机名解析

    为了在集群环境中方便节点调用,配置主机解析功能,在企业内部应该使用DNS解析;

    1
    2
    3
    4
    在 /etc/hosts 文件中,添加主机名解析,如下
    172.16.56.102 master
    172.16.56.103 node1
    172.16.56.104 node2

    上面的地址,就是我们给每个主机分配的ip地址,这个是固定的,我使用102做master节点,使用103、104分别做node1、node2节点;

  3. 时间同步

    kebernetes在内部要求时间必须精确一致,这里使用chronyd服务从网络上同步时间;在生产环境中,应该使用企业内部的时间服务器,保证各个服务器时间一致。

    1
    2
    3
    4
    5
    6
    1 启动chrondy服务
    systemctl start chronyd
    2 设置服务开机自启动
    systemctl enable chronyd
    3 查看时间是否一致,使用date验证
    date

    时间同步

  4. 禁用系统的iptables规则和firewalld规则

    由于docker 和 kebernetes在运行时,会产生大量的iptables规则,为了不让系统的iptables规则和运行时产生的混淆,则关闭系统的iptables规则使用。

    1
    2
    3
    4
    5
    6
    7
    8
    1 关闭firewalld
    systemctl stop firewalld
    2 重启取消自动重启
    systemctl disable firewalld
    3 关闭iptables
    systemctl stop iptables
    4 启动时取消自动重启
    systemctl disable iptables

    本项目中时没有iptables的,所有关闭时会报错,直接跳过即可。

  5. 禁用selinux

    selinux时linux的一个安全服务,如果不关闭,在安装集群中就会出现各种奇葩问题。

    查看:

    getenforce 输出 Enforcing

    关闭:

    1
    2
    3
    vim /etc/selinux/config
    设置:
    SELINUX=disable

    重启操作系统后,则会生效。

  6. 禁用swap分区

    swap分区虚拟内存分区,是指在物理内存耗尽情况下,会将磁盘空间虚拟为内存使用。启用swap设备会对系统性能产生非常负面的影响。因此,kebernetes要求每个节点都禁用swap分区。如果关闭不了分区,则需要在集群安装过程中明确参数进行配置说明。

    1
    2
    3
    分区的配置文件: /etc/fstab
    注释掉swap分区最后一行
    修改完毕后,需要重启linux操作系统
  7. 修改linux内核参数

    1
    2
    3
    4
    5
    6
    # 修改linux内核参数,添加网桥过滤和地址转发功能
    # 编辑/etc/sysctl.d/kubernetes.conf,添加配置项如下:

    net.bridge.bridge-nf-call-ip6tables = 1
    net.bridge.bridge-nf-call-iptables = 1
    net.ipv4.ip_forward = 1

    重新加载配置项:

    sysctl -p

    加载网桥过滤模块:

    modprobe br_netfilter

    查看网桥过滤模块是否加载成功:

    lsmod | grep br_netfilter

    结果如下:

    linux内核参数修改

  8. 配置ipvs功能

    kubernetes中service对外提供服务,有两种代理模式,iptables和ipvs,从性能角度来看,ipvs性能优于iptables,如果要使用,需要手动载入ipvs模块。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    # 手动载入ipvs模块
    1. 安装ipset 和ipvsadm
    yum install ipset ipvsadm -y
    2. 添加需要加载的模块脚本
    cat <<EOF > /etc/sysconfig/modules/ipvs.modules
    #!/bin/bash
    modprobe -- ip_vs
    modprobe -- ip_vs_rr
    modprobe -- ip_vs_wrr
    modprobe -- ip_vs_sh
    modprobe -- nf_conntrack_ipv4
    EOF

    3. 为添加的脚本添加执行权限
    chmod +x /etc/sysconfig/modules/ipvs.modules

    4. 执行脚本
    /bin/bash /etc/sysconfig/modules/ipvs.modules

    5. 查看对应的模块是否加载成功
    lsmod |grep ip_vs

    如下图:

    添加ipvs

  9. 重启服务器

    reboot

    检查selinux安全检查,检查swap分区状态是否满足要求;

    检查

三 安装Kubernetes相关应用

  1. docker安装

    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
    # 1. 切换镜像源
    wget https://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo -O /etc/yum.repos.d/docker-ce.repo

    # 2. 查看当前镜像源中支持的docker版本
    yum list docker-ce --showduplicates

    # 3. 安装特定版本的docker-ce
    # 必须制定 —setopt=obsoletes=0, 否则yum会自动安装最高版本
    yum install —setopt=obsoletes=0 docker-ce-18.06.3.ce-3.el7 -y

    # 4. 添加一个配置文件
    # Docker在默认情况下使用的Cgroup Driver为cgroupfs, 而kubernetes推荐使用systemd来代替cgroupfs
    mkdir /etc/docker
    cat <<EOF > /etc/docker/daemon.json
    {
    "exec-opts":["native.cgroupdriver=systemd"],
    "registry-mirrors": ["https://kn0t2bca.mirror.aliyuncs.com"]
    }
    EOF

    # 5. 查看docker版本
    docker version

    # 6. 重启docker
    systemctl restart docker

    # 7. 设置docker开机自动重启
    systemctl enable docker
  1. 安装kubernetes组件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    # 1. 切换国内的镜像源
    # cd /etc/yum.repos.d/
    vim kubernetes.repo

    # 在文件中添加下列信息
    [kubernetes]
    name=Kubernetes
    baseurl=http://mirrors.aliyun.com/kubernetes/yum/repos/kubernetes-el7-x86_64
    enabled=1
    gpgcheck=0
    repo_gpgcheck=0
    gpgkey=http://mirrors.aliyun.com/kubernetes/yum/doc/yum-key.gpg
    http://mirrors.aliyun.com/kubernetes/yum/doc/rpm-package-key.gpg

    # 2. 安装kubeadm、kubelet和kubectl
    yum install —-setopt=obsoletes=0 kubeadm-1.17.4-0 kubelet-1.17.4-0 kubectl-1.17.4-0 -y

    # 3. 配置kubelet的cgroup
    # 编辑/etc/sysconfig/kubelet,添加下面的配置
    KUBELET_CGROUP_ARGS="--cgroup-driver=systemd"
    KUBE_PROXY_MODE="ipvs"

    # 4. 设置kubelet开机自启
    systemctl enable kubelet
  1. 准备集群镜像

    1
    2
    # 1. 查看kubeadm 目前需要的组件依赖
    kubeadm config images list

    输出结果:

    kubeadm依赖组件

    编写脚本:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    cd ~
    cat <<EOF > /root/kubernetes.sh
    #!/bin/bash
    images=(
    kube-apiserver:v1.17.4
    kube-controller-manager:v1.17.4
    kube-scheduler:v1.17.4
    kube-proxy:v1.17.4
    pause:3.1
    etcd:3.4.3-0
    coredns:1.6.5
    )
    for imageName in ${images[@]} ; do
    docker pull registry.cn-hangzhou.aliyuncs.com/google_containers/$imageName
    docker tag registry.cn-hangzhou.aliyuncs.com/google_containers/$imageName k8s.gcr.io/$imageName
    docker rmi registry.cn-hangzhou.aliyuncs.com/google_containers/$imageName
    done
    EOF

    添加文件可执行权限:
    chmod +x kubernetes.sh

    运行添加镜像:
    ./kubernetes.sh

    等待安装成功,截图如下:

    拉k8s镜像

  1. 集群的初始化

    4.1. 在master节点上进行集群的初始化

    1
    2
    3
    4
    5
    kubeadm init \
    --kubernetes-version=v1.17.4 \
    --pod-network-cidr=10.244.0.0/16 \
    --service-cidr=10.96.0.0/12 \
    --apiserver-advertise-address=172.16.56.102

    上述必须要保证有2核,如果一核就会报错。

    要保证 kubelet是启动状态

    内存一定要大一点,起码4G,否则会报错

    重制: kubeadm reset

    看节点: kubectl get pods -n kube-system

4.2. 创建必要文件

1
2
3
mkdir -p $HOME/.kube
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown $(id -u):$(id -g) $HOME/.kube/config

4.3. 让从节点加入到主节点中

1
2
kubeadm join 172.16.56.102:6443 --token dy1nfq.xhcyreop4t3me22k \
--discovery-token-ca-cert-hash sha256:e020f149af492b4f780e19a3fecdf0271a7f3b37583f8124ad712ed30099ec82

4.4. 安装网络插件(在主节点上操作)

NotReady 未就绪,网络插件有fannel calico canal,只要给主节点配置即可。

使用flannel网络插件, 由于flannel插件使用官方的方式安装会报错,所以提前下载好flannel的包,然后倒入到docker环境中。

​ 下载flannel.tar包,然后倒入:

​ docker load -i flannel.tar

​ 下载flannel的yuml配置文件:

​ wget https://raw.githubusercontent.com/coreos/flannel/master/Documentation/kube-flannel.yml

​ 然后安装:

​ kubectl apply -f kube-flannel.yml

​ 查看pods:

​ kubectl get pods -n kube-system -o wide

​ 删除多余的pods:

​ kubectl delete pod Pod的名字 -n kube-system

​ 查看pod的安装描述信息:

​ kubectl describe pod 名称 -n kube-system

​ 查看nodes:

​ kubectl get nodes

四 测试Nginx

  1. 创建应用

    1
    kubectl create deployment nginx --image=nginx:1.14.2
  2. 暴露端口

    1
    kubectl expose deployment nginx --port=80 --type=NodePort

  3. 应用访问

    1. 删除应用

      kubectl delete deployment nginx

    删除serive

    kubectl delete service,pods nginx -n kube-system

  1. 在node节点上查看

    1
    scp ~/.kube/ root@172.16.56.103:/root/

The Go Memory Model

一 介绍

​ go的内存模型指定一种条件,在这种条件下,可以保证在一个goroutine中读取变量可以观测到不同goroutine对同一变量的写操作所产生的值。

二 忠告

​ 多goroutine并发访问修改数据的程序必须串行化访问。

​ 利用channel操作、sync包中的同步原语以及sync/atomic包中的原子操作保证串行化访问,保护数据。

​ 如果你一定要阅读该文档的其余部分去理解你编程的行为,这是非常明智的。

​ 但是不要自作聪明。

三 happens before

​ 在单一的goroutine内部,读取和写入的行为必须和程序中代码指定的执行顺序保持一致。也就是说,在重排不会改变语言规范所定义的goroutine行为时,编译器和处理器才可以对单个goroutine进行读取、写入指令的重排。因为指令重排,一个goroutine所观测到的执行顺序可能和其他goroutine所感知到的执行顺序不同。例如,当一个goroutine执行a=1;b=2,其他goroutine可能观测到b的值比a的值提前更新。

​ 为了指定读取和写入的要求,我们在go程序中定义了执行内存操作之前的部分执行顺序。如果事件e1发生在事件e2之前,那么可以说事件e2在事件e1之后发生;同理,如果事件e1既不发生在事件e2之前,也不发生在事件e2之后,那么我们就说事件e1和事件e2同时发生。

在单goroutine内部,事前发生的顺序是程序表示的执行顺序

​ 一个v变量的读操作r要观测到对v变量的写操作w,需要满足以下两个条件:

​ 1.r不能先于w发生;

​ 2.没有其他的对v的写操作w’发生在w之后,r之前;

​ 为了保证变量v的读操作r可以观测到特定关于v的写操作w,必须要确保w是唯一一个能被r所观测,也就是说要保证r观测到w,必须满足以下两个条件:

​ 1.w先于r发生;

​ 2.其他对共享变量v的写操作要么先于w发生,要么后于r发生;

​ 后者这一对条件比第一对条件更强,它要求没有其他写操作和w或r同时发生。

​ 在单个goroutine中,没有并发性,所以两种定义是相等的,v的读操作r总能观测到最近一次对v的写操作w的值。当多goroutine访问共享变量v时,要确保读取到期望写入的值,就必须使用同步事件建立事件发生顺序的条件。

变量v的零值初始化行为在内存模型中就是一个写操作

超过单个机器字的值的读取或写入会被拆成多个没有指定顺序的机器字大小的操作

四 Go中Happens before关系保证

  1. Initialization(初始化)

    程序在单个运行的goroutine中初始化,但是该goroutine可以创建其他的goroutine,同时执行。

    如果包p导入了包q,那么q的初始化函数init完成先于任何p的开始之前。

    main.main函数的执行发生在所有初始化函数执行完成之后。

  1. Goroutine

    启动新goroutine的go申明语句在该goroutine执行开始前发生。

    例如在下面这段程序中:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    var a string
    func f() {
    fmt.Println(a)
    }

    func hello() {
    a = "hello world"
    go f()
    }

    调用hello函数,在将来某个点打印出”hello world”,(可能在hello返回之后)

在程序中,不能保证一个goroutine的退出发生在任何事件之前,例如下面的这个程序:

1
2
3
4
5
6
7
var a string
func hello() {
go func(){
a = "hello"
}()
fmt.Println(a)
}

a的赋值没有跟随任何同步事件,所以不能保证其他的goroutine能观测到a的赋值。事实上,编译器可能会主动删除掉整个go的申明。

如果一个goroutine的影响必须让其他的goroutine观测到,就要使用一些如锁、channel通讯等同步机制建立相对的顺序。

  1. Channel

    通道通行是goroutine之间同步的主要方法。通常在不同的goroutine中,在特定通道上的每一个发送操作与该通道上的接收操作进行匹配。

    在一个通道上的发送操作发生在该通道上相应的接收操作完成之前

    这个程序:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    var c = make(chan int, 10)
    var a string
    func f() {
    a = "hello world"
    c <- 0
    }
    func main() {
    go f()
    <- c
    fmt.Println(a)
    }

    上面保证了一定会输出”hello world”。a的写入happens before通道c的发送,c的send操作happens before从通道c匹配接收操作完成,a的打印操作happens after通道的接收操作。

通道关闭发生在因为通道关闭返回一个零值的接收操作之前

在上面那个例子中,将c <- 0的操作替换为close(c),能得到一个和上述相同行为的程序。

从一个无缓冲通道的接收操作发生在该通道的发送操作完成之前

这个程序(如上所示,使用一个无缓冲通道,并交互了发送和接收操作):

1
2
3
4
5
6
7
8
9
10
11
12
13
var c = make(chan int)
var a string

func f() {
a = "hello world"
<- c
}

func main() {
go f()
c <- 0
fmt.Println(a)
}

也能保证”hello world”输出,变量a的写操作发生在通道c的接收操作之前,通道c的接收操作发生在匹配发送操作完成之前,而发送操作发生在打印操作之前。

如果是有缓存通道,例如(var c = make(chan int, 1)),上面程序不能保证输出”hello world”。(可能会输出空的字符串、崩溃、或执行其他操作)

容量为C的通道上的第K个接收操作发生在该通道的K+N个发送完成操作之前

这个规则将前面规则推广到缓冲通道,通过缓存通道对计数信号量进行建模,通道中元素的数量对应了正在使用的数量,通道的容量对应了并发使用的最大数量。发送一个元素获取一个信号量,接收一个元素释放一个信号量。这是限制并发常用的方法。

该程序为工作列表中每一个元素启动一个goroutine,但是goroutine使用通道进行任务协调,以确保每个时刻最多有三个工作函数执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var limit = make(chan int, 3)

func main() {
for _, w := range work {
go func(w){
limit <- 1
go w()
<- limit
}(w)
select {

}
}
}
  1. Locks

    go中sync包提供了两种锁数据类型,sync.Mutex、sync.RWMutex。

    对于任何类型sync.Mutex, sync.RWMutex类型变量l,当n<m, 第n次l.UnLock()的调用发生在第m次l.Lock()返回之前。

    这个程序:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    var l sync.Mutex
    var a string

    func f() {
    a = "hello, world"
    l.Unlock()
    }

    func main() {
    l.Lock()
    go f()
    l.Lock()
    fmt.Println(a)
    }

    能够保证”hello,world”的正常输出。第一次在f函数中调用的l.UnLock()发生在main.main函数中第二次调用l.Lock()返回结果之前。而第二次调用l.Lock()发生在打印操作操作之前。

    对于sync.RWMutex变量l,第n次的l.RLock()发生在n次的l.UnLock()之后,并且与之匹配的l.RUnLock()发生在第n+1次l.Lock()之前。(ps:go中读写锁是写优先模型)

  1. Once

    sync包通过使用Once类型提供了一种安全的机制,用于在存在多个goroutine的情况下进行初始化。多线程可以为了特定函数执行once.Do(f),但是只会运行一个f()函数,其他线程调用将会被阻塞,直到f()函数执行返回。

    对于once.Do(f)的调用,f函数的单次调用一定happens before任何once.Do(f)调用返回

    下面程序:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    var a string
    var once sync.Once

    func setup() {
    a = "hello, world"
    }

    func doprint() {
    once.Do(setup)
    print(a)
    }

    func twoprint() {

    go doprint()
    go doprint()
    }

    调用twoprint程序运行,setup函数只会被调用一次,设置函数会在两次打印操作之前完成变量的赋值,”hello,wrold”的结果将会被打印两次。

IoC在Go中的应用

一 控制反转与依赖注入

​ IoC(Ioc—Inversion of Control)控制反转,是一种设计思想,控制反转意味着可以不用在内部进行业务对象的实例化,而是将控制逻辑进行高度抽象,抽象成为一种具有一定签名的协议,将业务对象进行注入,在内部消除了控制逻辑对某一种具体对象的依赖。

​ 在这样的设计方式中,直接将业务对象中进行控制抽象,转化为业务对象对控制协议的依赖。而控制逻辑通过抽象成协议,将对具象业务对象的依赖进行解耦,控制协议不关心到底是什么样的业务对象,只要满足协议签名的任何对象,通过依赖注入的方式,进行具象业务控制。

二 Go中控制反转的案例分析

  • 实现一个数据存放和获取的容器,这个容器具有获取(get)、存放(put)的方法,其实现代码如下。

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

    import (
    "fmt"
    "sync"
    )

    /*
    @Time : 2021/1/19 21:13
    @Author : austsxk
    @Email : austsxk@163.com
    @File : container.go
    @Software: GoLand
    */

    // 容器,可以存放任何类型的数据信息
    type Container struct {
    len int
    mtx sync.Mutex
    dt []interface{}
    }

    // 存放的方法
    func (c *Container) Put(d interface{}) {
    c.mtx.Lock()
    defer c.mtx.Unlock()
    c.dt = append(c.dt, d)
    c.len++

    }

    // 取出获取的最后一个方法
    func (c *Container) Pull() interface{} {
    c.mtx.Lock()
    defer c.mtx.Unlock()
    if c.len == 0 {
    return nil
    }

    result := c.dt[len(c.dt)-1]
    c.dt = c.dt[:len(c.dt)-1]
    c.len--
    return result

    }

    func NewContainer() *Container {
    return &Container{}
    }
  • 在上面容器中增加可撤销的控制逻辑 CancelAble,就是存放之后,如果撤销就是取出之前存放的元素,如果是取出,撤销操作就是将元素放回,最多可撤销10次;

    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
    // CancelAbleContainer 组合原来的struct
    // 由于在放和拿有了可以撤销的操作,也就是需要重载存放和获取的方法
    type CancelAbleContainer struct {
    Container
    fns []func()
    }

    // 重载Put方法,如果不重载,则调用的是嵌套对象的方法 obj.Put 就是 obj.Container.Put
    func (c *CancelAbleContainer) Put(in interface{}) {
    // 存放数据
    c.Container.Put(in)
    // 可撤销函数
    fn := func() {c.Container.Pull()}
    if len(c.fns) < 10 {
    c.fns = append(c.fns, fn)
    } else {
    c.fns = append(c.fns[1:], fn)
    }
    }

    func (c *CancelAbleContainer) Pull() interface{} {
    data := c.Container.Pull()
    // 可撤销函数
    fn := func() {c.Container.Put(data)}
    if len(c.fns) < 10 {
    c.fns = append(c.fns, fn)
    } else {
    c.fns = append(c.fns[1:], fn)
    }
    return data
    }

    // 可撤销操作
    func (c *CancelAbleContainer) Cancel() error {
    if len(c.fns) == 0 {
    return errors.New("No Data Can Canceled!")
    }
    index := len(c.fns) - 1
    c.fns[index]()
    c.fns = c.fns[:index]
    return nil
    }

    func NewCancelAbleContainer() *CancelAbleContainer {
    return &CancelAbleContainer{}
    }

    说明: 上面我们重载了获取和存放的方法,实例对象直接调用Put或者Pull方法就是调用的CancelAbleContainer.Put 和CancelAbleContainer.Pull方法。新增加了撤销操作,可以撤销10次,这其实是对Container对象新加的控制逻辑,在上面内容中,显示在控制逻辑中,强耦合了业务对象Container的业务逻辑,如果需要对其他对象进行控制,显示是存在难拓展性。

  • 通过对控制逻辑进行抽象,将其抽象为一种协议,然后让业务对象对控制进行依赖,将依赖对象进行注入方式实现可撤销的存储容器。

    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
    // 自定义一个撤销类型 其类型为函数切片
    type Cancel []func()

    // 对控制逻辑抽象为Cancel协议,有撤销函数,与给类型添加需要注入的依赖
    func (c *Cancel) Add(f func()) {
    *c = append(*c, f)
    }

    // Cancel函数,用来进行撤销的操作,这个方法是抽象协议的方法,不属于具象对象,不依赖任何业务对象
    func (c *Cancel) CancelFunc() error {
    p := *c
    if len(p) == 0 {
    return errors.New("No Data Can Canceled!")
    }
    p[len(p)-1]()
    *c = p[:len(p)-1]
    return nil
    }

    type NCancelAbleContainer struct {
    len int
    mtx sync.Mutex
    dt []interface{}
    // 控制逻辑
    cancel Cancel
    }

    // 此处业务对象撤销需要依赖控制抽象对象,进行控制反转
    func (n *NCancelAbleContainer) Cancel() error {
    return n.cancel.CancelFunc()
    }

    func (n *NCancelAbleContainer) Put(i interface{}) {
    n.mtx.Lock()
    defer n.mtx.Unlock()
    n.dt = append(n.dt, i)
    n.len++
    // 添加可撤销操作
    fn := func() { n.Pull() }
    n.cancel.Add(fn)
    }

    func (n *NCancelAbleContainer) Pull() interface{} {
    n.mtx.Lock()
    defer n.mtx.Unlock()
    if n.len == 0 {
    return nil
    }

    result := n.dt[len(n.dt)-1]
    n.dt = n.dt[:len(n.dt)-1]
    n.len--
    fn := func() { n.Put(result) }
    n.cancel.Add(fn)
    return result
    }

    func CreateCancelContainer() *NCancelAbleContainer {
    return &NCancelAbleContainer{}
    }

    说明一下:

    ​ 我们定义一个Cancel类型,抽象控制协议,将控制需要传入的依赖,全部抽象,在这个类型中,我们只要满足func()类型的签名即可,不用管是什么业务对象都可以进行注入,这就是将业务对象进行解耦,同时增加了可拓展性,任何满足业务需求的,处理成协议格式的,都可以使用Cancel函数进行实现。在业务对象中,我们通过cancel进行嵌入,使得业务对象对控制对象的依赖,在实现功能的完备性上,必须依赖控制对象,而控制对象,只需要将注入进行的业务对象添加到抽象协议中,即可,简化了业务和控制逻辑。

Go实现23种设计模式—模版方法模式

一 类别

​ 行为型模式

二 概述

​ 定义一个操作的基本框架,框架中的方法有基本方法和模版方法。模版方法是对基本方法的组装与调用,从而完成固定的逻辑,基本方法是其他子类对接口实现的重载,从而使用相同的模版,产生不同的结果。

三 使用场景

​ A. 多个子类有共有的方法,并且逻辑基本相同;

​ B. 很多依赖与某一核心算法,可以将该算法设为模版算法;

​ C. 重构中对于重复业务逻辑的代码通过设置模版方法如钩子函数、装饰器进行设计;

四 优点

​ A. 将共有的部分进行封装、减少代码的重复性,以便于系统的高可维护性。

​ B. 基本方法的实现延迟到子类进行具体实现,控制的行为由父类约束。

五 实现

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

import "fmt"

/*
@Time : 2021/1/12 13:25
@Author : austsxk
@Email : austsxk@163.com
@File : template.go
@Software: GoLand
*/

// 定义一个基类接口,该接口包含了一系列相关的操作
type WorkInterface interface {
GetUp()
Working()
Sleep()
}

// 定义一个工作者的接口,每个工作者都有这些基本操作,然后将具体的实现交给子类
type Worker struct {
WorkInterface
}

// 定义一个工作者的日常行为
func (w *Worker) Daily() {
w.GetUp()
w.Working()
w.Sleep()
}

// 任何实现了WorkInterface的子类,都可以传入到其中,然后调用其日常方法,就能实现子类的三个具体实现,即多态的实现
func NewWorker(w WorkInterface) *Worker {
return &Worker{w}
}

// 定义一个清洁员类
type EnvironmentalSanitationWorker struct {
}

func (e *EnvironmentalSanitationWorker) GetUp() {
fmt.Println("环卫阿姨起床了...")
}

func (e *EnvironmentalSanitationWorker) Working() {
fmt.Println("环卫阿姨正在打扫卫生...")
}

func (e *EnvironmentalSanitationWorker) Sleep() {
fmt.Println("环卫阿姨要休息了...")
}

// 定义一个程序员类
type Programmer struct {
}

func (e *Programmer) GetUp() {
fmt.Println("程序员起床了...")
}

func (e *Programmer) Working() {
fmt.Println("程序员正在疯狂敲代码...")
}

func (e *Programmer) Sleep() {
fmt.Println("程序员睡觉了...")
}

测试:

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

import "testing"

/*
@Time : 2021/1/12 18:14
@Author : austsxk
@Email : austsxk@163.com
@File : template_test.go
@Software: GoLand
*/

func TestNewWorker(t *testing.T) {
ewer := NewWorker(&EnvironmentalSanitationWorker{})
ewer.Daily()
programmer := NewWorker(&Programmer{})
programmer.Daily()
}

// go test -v template.go template_test.go

/*
test result:
=== RUN TestNewWorker
环卫阿姨起床了...
环卫阿姨正在打扫卫生...
环卫阿姨要休息了...
程序员起床了...
程序员正在疯狂敲代码...
程序员睡觉了...
--- PASS: TestNewWorker (0.00s)
PASS
ok command-line-arguments 0.647s
*/

说明: WorkInterface 接口抽象出一系列基本方法如GetUp、Working、Sleep,这些方法的具体实现延迟到由各个子类中;Worker 嵌入该接口,也就意味着,可以对该接口的方法进行组装和逻辑控制,即抽象的父类。在这个结构体中,有一个指针方法为Daily日常,就是模版方法,通过调用基本方法从而实现控制逻辑。然后通过实例化Worker对象时,将实现了WorkInterface接口的任何对象传入,就可以实现相同业务的控制,与分别各自实现相同方法而言,消除了重复的代码量。

Go实现23种设计模式—门面模式(外观模式)

一 类别

​ 门面模式又叫外观模式,是一种常用的封装模式

二 概述

​ 给一系列具有关联性的子系统的集合提供对外访问的一组接口,调用者不用明确内部具体的业务逻辑,只需要调用这组接口达到目的即可。也就是要求子系统外部与内部不能直接进行通讯,必须通过一个统一的对象进行,而这个统一的对象就是门面。门面模式通过只提供高层次的接口,从而降低了外部与子系统调用的复杂性。

三 关系

​ 门面角色: 了解子系统内部的全部业务逻辑与实现。

​ 子系统角色: 被门面角色的实现对象所调用,等价于外界的客户端。

​ Client角色: 只需要传入相对应的签名,调用相关方法,就能得到目标结果。

​ 子系统内部: 在实现Clinet调用方业务需求的同时,可以对具体业务进行功能拓展,对外不变,对内多变,增加额外的功能,比如说检验等。

四 门面模式的优点

​ A. 减少了系统的相互依赖性;在我看来通过门面的代理,直接切断了调用方与各个子系统内部的调用关系,降低了双方的耦合关系,从而降低了系统混乱、依赖。直接将两者的依赖关系转移到门面对象之间的依赖,减少了直接与子系统间的相互依赖关系,符合设计原则。

​ B. 功能拓展性更灵活,只要不影响门面对象,子系统内部可以多样性。

​ C. 通过门面对外只提供必要的服务与直接对外暴露子系统服务而言,系统的安全性更高。

五 门面模式的缺点

​ 门面模式不满足开闭原则,门面的修改风险性很大。

六 适用场景

​ A. 为一组子系统或一系列复杂的模块提供一个统一的外部访问的接口。

​ B. 调用方与子系统依赖性很强,通过门面将client与子系统强依赖转化门面对象间的依赖。

七 代码实现

​ 实现一个邮寄信件的功能,用户只需要输入信的内容与地址,即可实现写信、填写地址、封装、邮寄的功能。此外在拓展一个公安机关对文件内容检查功能。

  1. 代码清单

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    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 SampleLetters struct {
    letter string
    }

    func (l *SampleLetters) WriteLetters(content string) {
    l.letter = fmt.Sprintf("写信内容为:%s", content)
    }

    func (l *SampleLetters) AddAddress(address string) {
    l.letter = l.letter + "\n" + fmt.Sprintf("邮寄至: %s", address)
    }

    func (l *SampleLetters) PutInToEnvelope() {
    fmt.Println("信件存放到信封中...")
    }

    func (l *SampleLetters) SendLetters() {
    // fmt.Println("信件内容:", l.letter)
    fmt.Println("信件已发送...")
    }

    type Facade interface {
    sendLetter(context, address string)
    }


    func NewSampleFacade() *SampleFacade {
    return &SampleFacade{}
    }

    // 如果此时需要新拓展功能,对信的内容进行检查
    // 则继续添加SampleFacade的组合类
    type PoliceInspection struct {
    }

    func (p *PoliceInspection) CheckLetterSecurity(content string) bool {
    // 如果检查内通通过,则返回true
    fmt.Printf("被检查内容: %s\n", content)
    fmt.Println("内容检查通过...")
    return true
    }

    // 将相关联性的子系统的集合进行综合,完成一系列的业务逻辑
    type SampleFacade struct {
    SampleLetters
    PoliceInspection
    }

    func (f *SampleFacade) sendLetter(context, address string) {
    // 此处进行业务逻辑的实现
    // 1. 写信
    f.WriteLetters(context)
    // 2. 写地址
    f.AddAddress(address)
    // 此处新增检查信件安全性
    ok := f.CheckLetterSecurity(f.letter)
    if ok {
    fmt.Println("this is safe!")
    } else {
    fmt.Println("this is danger!")
    return
    }
    // 3. 封装
    f.PutInToEnvelope()
    // 4.发送
    f.SendLetters()
    }

    // 由此可见,在不改变用户调用的接口的情况下,已经完成了逻辑的修改,增加的新的功能,这就是门面模式带来的好处
  2. 代码测试

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

    import "testing"

    /*
    @Time : 2021/1/12 12:57
    @Author : austsxk
    @Email : austsxk@163.com
    @File : facade_test.go
    @Software: GoLand
    */

    func TestNewSampleFacade(t *testing.T) {
    facade := NewSampleFacade()
    facade.sendLetter(
    "hello Song, this is facade pattern testing...",
    "austsxk@vip.qq.com")
    }

    // go test -v facade_test.go facade.go
  3. 结果输出

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    === RUN   TestNewSampleFacade
    被检查内容: 写信内容为:hello Song, this is facade pattern testing...
    邮寄至: austsxk@vip.qq.com
    内容检查通过...
    this is safe!
    信件存放到信封中...
    信件已发送...
    --- PASS: TestNewSampleFacade (0.00s)
    PASS
    ok command-line-arguments 0.897s
  4. 说明

    ​ 将用户写信的WriteLetters、AddAddress、PutInToEnvelope、SendLetters等四个操作,全部交给了SampleFacade 简单门面实现,该门面实现了Facade的接口,只提供一个写信的方法。SampleFacade结构体组合了写信的SampleLetters对象和安全检查的PoliceInspection对象,调用者将之前对子系统SampleLetters的四个操作直接转化成对门面对象的操作,并且只有一个方法可以调用,就是调用实例对象的sendLetter方法,就能达到调用者的目的。此外调用者并不知道子系统内部对信件内容进行安全检测,即对子系统功能的拓展非常方便。

go应用性能分析工具

一 Go性能调优

​ Go语言的性能主要是体现在四个方面:

​ A. CPU profile :报告cpu的使用情况,使用一定频率采集程序在cpu和寄存器上的数据;

​ B. 内存 profile(heap profile):报告程序内存使用情况;

​ C. Block profiling: 报告goroutine阻塞,不再运行的情况;用来查找和分析死锁;

​ D. Groutine profling: 报告goroutine的使用情况,区分调用关系;

​ 常用工具:

​ runtime/pprof:采集工具型应用的数据;

​ net/http/pprof:采集服务型应用运行数据信息;

二 工具型应用性能分析(runtime/pprof)

A. CPU分析

1
2
3
4
5
6
7
8
9
10
11
import (
"runtime/pprof"
)

f, err := os.Create("./cpu.pprof")
err = pprof.StartCPUProfile(f)
defer pprof.StopCPUProfile()
if err != nil {
fmt.Println("pprof error")
return
}

然后使用 go tool pprof *.pprof 即可进行数据分析

B. 内存分析

1
2
f2, err := os.Create("./memory.pprof")
err = pprof.WriteHeapProfile(f2)

然后采用 go tool工具分析,默认采用-sample_index=inuse_space, 可以使用inuse_objects查看分配对象数量

三 服务型性能分析(net/http/pprof)

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

import (
"fmt"
"log"
"net/http"
_ "net/http/pprof"
)

func index(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Hello sxk!")
}



func main() {
mtx := http.DefaultServeMux

// 自定义mtx
// mtx := http.NewServeMux()
//mtx.HandleFunc("/debug/pprof/", pprof.Index)
//mtx.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline)
//mtx.HandleFunc("/debug/pprof/profile", pprof.Profile)
//mtx.HandleFunc("/debug/pprof/symbol", pprof.Symbol)
//mtx.HandleFunc("/debug/pprof/trace", pprof.Trace)

mtx.HandleFunc("/index", index)

server := &http.Server{
Addr: "127.0.0.1:8088",
Handler: mtx,
}

if err := server.ListenAndServe(); err != nil {
log.Fatal("http error!")
}
}

只需要在服务上放导入_ “net/http/pprof”即可;

然后在浏览器上访问: Addr/debug/pprof 即可,得到下图:

net/http/pprof

四 内存使用矢量图查看工具

1
brew install graphviz

安装完成后,需要讲其添加到执行文件目录下,通常是/usr/local/bin目录。

使用 dot -version判断是否安装成功。

graphviz

在使用 go tool pprof *.pprof 打开交互界面后:

pprof交互页面

输入web后,将自动保存一个pprof001.svg矢量图。

文件位置:

1
file:///private/var/folders/g4/40wbk_1d10g7p0g9jsz2t_z40000gn/T/pprof001.svg

在浏览器中打开如下图所示:

pprof001.svg

在上述图纸中(没有函数调用):

每个框代表一个函数,理论上框的越大表示占用的CPU资源越多。 方框之间的线条代表函数之间的调用关系。 线条上的数字表示函数调用的次数。 方框中的第一行数字表示当前函数占用CPU的百分比,第二行数字表示当前函数累计占用CPU的百分比。

五 火焰图性能分析

  1. 安装FlameGraph,FlameGraph是profile数据的可视化层工具。

    1
    git clone https://github.com/brendangregg/FlameGraph.git
  2. 将应用拷贝在可执行目录下

    1
    cp ./flamegraph.pl /usr/local/bin
  3. 终端测试是否安装成功

    1
    flamegraoh.pl -h
  4. 安装成功的结果

    framegraph.pl

  5. 安装go-torch,在有framegraph的支持下,安装go-torch展示profile的输出

    1
    go get -v github.com/uber/go-torch
  6. 使用go-torch -h查看是否安装完成

    go-torch

  7. 准备压测工具 wrk

    1
    推荐使用https://github.com/wg/wrk 或 https://github.com/adjust/go-wrk
    1
    go get -v github.com/adjust/go-wrk

    压测使用方法:

    1
    go-wrk -n 100 http://127.0.0.1:8088/index

    结果:

  8. 开启go-torch 检查并生成矢量图

    1
    go-torch -u http://127.0.0.1:8088 -t 30

    就是在30秒,生成一个torch.svg矢量图;

    参数说明:

    -u: 指定访问的url和端口 (-url=)

    -s: 指定pprof proflie的存储路径,默认 /debug/pprof/profile

    -seconds: 指定profiling监控的时间长度,默认30s

  9. 执行结束后,生成torch.svg矢量图

  10. 打开火焰图结果如下(右键/打开方式/浏览器打开)

    火焰图

    如上图所示:

    火焰图的y轴表示cpu调用方法的先后,x轴表示在每个采样调用时间内,方法所占的时间百分比,越宽代表占据cpu时间越多。通过火焰图我们就可以更清楚的找出耗时长的函数调用,然后不断的修正代码,重新采样,不断优化。

六 pprof与性能测试结合

go test命令有两个参数和 pprof 相关,它们分别指定生成的 CPU 和 Memory profiling 保存的文件:

参数1: -cpuprofile:cpu profiling 数据要保存的文件地址

参数2: -memprofile:memory profiling 数据要报文的文件地址

在进行性能测试,可以执行CPU和Mem的profiling,并保存在文件中:

如下:

CPU profiling

1
go test -bench . -cpuprofile=cpu.prof

Mem profiling

1
go test -bench . -memprofile=./mem.prof

Go可选参数的优雅使用技巧

  • 前言

    ​ 在Go语言中,是不支持类似Python的可选参数,在函数进行参数拓展或者配置参数的默认参数就比较麻烦。如果使用不当,会在将来接口拓展中造成很大的影响。

    主要影响:

    1. 破坏兼容性

      直接在函数定义处,添加拓展参数,会导致所以调用处都要进行修改,破坏了代码的完整性,因为有的逻辑中可能不需要该参数。

    2. 不破坏兼容性,重新添加一个新的初始化的函数,随着参数添加的越来越多,初始化函数越来越多,非常冗余。

    ​ 因此使用可选参数配置的方式进行初始化和默认参数的设置就优雅的解决了参数拓展带来的一系列问题。

  • 实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    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
    package main

    import "fmt"

    // 本文是实现可选参数的最佳实现
    // 对于函数的可选参数和默认参数配置,通常要在不影响不破坏现有逻辑基础上进行参数的添加
    // 有一种优秀的操作是通过options选项,使用函数进行参数的初始化和可选值的设置
    // 如下数据库的配置,我们希望可以在初始化时,使用默认的参数,也可以在测试时进行自己对可选参数的修改,不用全部输入全部数据
    type DataBaseConfig struct {
    IP string
    Port string
    Db int
    UserName string
    PassWord string
    }

    const (
    PassWord = "asd@123"
    UserName = "test"
    Db = 1
    )

    type DatabaseOptions func(*DataBaseConfig)

    // 添加数据库DB
    func WithDb(db int) DatabaseOptions {
    return func(con *DataBaseConfig) {
    con.Db = db
    }
    }

    // 添加用户名和密码
    func WithUserNamePassWord(userName, passWord string) DatabaseOptions {
    return func(con *DataBaseConfig) {
    con.UserName = userName
    con.PassWord = passWord
    }
    }

    // 可以设置默认参数
    func DefaultDatabaseConfig(con *DataBaseConfig) *DataBaseConfig {
    con.PassWord = PassWord
    con.UserName = UserName
    con.Db = Db
    return con
    }


    func NewDataBaseConnect(IP, Port string, options ...DatabaseOptions) *DataBaseConfig {
    // 本初始化方案,要自己填IP,其他参数可填可不填,也可以使用默认参数
    con := &DataBaseConfig{
    IP: IP,
    Port: Port,
    }
    // 默认值的设定
    con = DefaultDatabaseConfig(con)

    // 遍历可选参数,然后分别调用匿名函数,将连接对象指针传入,进行修改
    for _, op := range options {
    // 遍历调用函数,进行数据修改
    op(con)
    }
    return con
    }

    func main() {
    // 传入自定义参数测试可选参数的输入
    var options = []DatabaseOptions {
    WithDb(2),
    WithUserNamePassWord("hello", "passWord"),
    }
    con1 := NewDataBaseConnect("127.0.0.1", "27017", options...)
    fmt.Printf("%#v", con1)

    // 不输入可选参数的测试
    con := NewDataBaseConnect("127.0.0.1", "27017")
    fmt.Printf("%#v", con)
    }
  • 结果

    1
    2
    3
    &main.DataBaseConfig{IP:"127.0.0.1", Port:"27017", Db:2, UserName:"hello", PassWord:"passWord"}

    &main.DataBaseConfig{IP:"127.0.0.1", Port:"27017", Db:1, UserName:"test", PassWord:"asd@123"}
  • 说明

    ​ 本实现方式,是通过一个options的数组,进行解包,数组中传递就是自定义可选参数的函数,通过在初始化的函数中,进行循环遍历,然后调用给定可选参数对应的函数,进行返回对象的修改。如果以后需要拓展,只要在目标结构体中添加字段,然后添加With字段函数即即可,非常简单实用。在源码包中就有很多的实现案例。本文只是简单赘述。

本站总访问量 本站总访客数 本文总阅读量
载入天数...载入时分秒...