HAProxy 简介

血衫平时会玩一些日服韩服游戏,有的游戏还会限定某个地区的IP才能够接入。对于一些对丢包率特别敏感的游戏(比如舰队Collection),在游戏时就必须搭建一个中继服务器,从本地直连到丢包率低的中继服务器,再连接到目标服务器,这样我们就能够形成一条丢包率较低的线路。

HAProxy是一款HTTP/TCP负载均衡器,使用 HAProxy 可以达到中继服务器的效果(算是杀鸡用牛刀的一个案例Orz)。

这篇文章介绍 Haproxy 的基本功能特性和它的简单应用。

介绍

HAProxy提供了L4(TCP)和L7(HTTP)两种负载均衡能力,具备丰富的功能。HAProxy的社区非常活跃,具备媲美商用负载均衡器的性能和稳定性,它当前不仅仅是免费负载均衡软件的首选,更几乎成为了唯一选择。

HAProxy的核心功能

  • 负载均衡:L4和L7两种模式,支持RR/静态RR/LC/IP Hash/URI Hash/URL_PARAM Hash/HTTP_HEADER Hash等丰富的负载均衡算法
  • 健康检查:支持TCP和HTTP两种健康检查模式
  • 会话保持:对于未实现会话共享的应用集群,可通过Insert Cookie/Rewrite Cookie/Prefix Cookie,以及上述的多种Hash方式实现会话保持
  • SSL:HAProxy可以解析HTTPS协议,并能够将请求解密为HTTP后向后端传输
  • HTTP请求重写与重定向
  • 监控与统计:HAProxy提供了基于Web的统计信息页面,展现健康状态和流量数据。基于此功能,使用者可以开发监控程序来监控HAProxy的状态

HAProxy的关键特性

  • 采用单线程、事件驱动、非阻塞模型,减少上下文切换的消耗,能在1ms内处理数百个请求。并且每个会话只占用数KB的内存。
  • 大量精细的性能优化,如O(1)复杂度的事件检查器、延迟更新技术、Single-buffereing、Zero-copy forwarding等等,这些技术使得HAProxy在中等负载下只占用极低的CPU资源。
  • HAProxy大量利用操作系统本身的功能特性,使得其在处理请求时能发挥极高的性能,通常情况下,HAProxy自身只占用15%的处理时间,剩余的85%都是在系统内核层完成的。
  • HAProxy作者在8年前(2009)年使用1.4版本进行了一次测试,单个HAProxy进程的处理能力突破了10万请求/秒,并轻松占满了10Gbps的网络带宽。

HAProxy的大部分工作都是在操作系统内核完成的,所以HAProxy的稳定性主要依赖于操作系统,作者建议使用2.6或3.x的Linux内核,对sysctls参数进行精细的优化,并且确保主机有足够的内存。这样HAProxy就能够持续满负载稳定运行数年之久。

安装

Debian 系的安装非常简单:

sudo apt-get -y install haproxy

编辑文件/etc/haproxy/haproxy.cfg,全文替换:

global
    ulimit-n  51200

defaults
    log    global
    mode    tcp
    option    dontlognull
        timeout connect 5000
        timeout client  50000
        timeout server  50000

frontend ss-in
    bind *:relay_server_port  # 中继端口,或端口范围,30000-40000
    default_backend ss-out

backend ss-out
    server server1 proxy_server_ip:proxy_server_port maxconn 20480 # 目标服务器,可以带上对方的端口号。不带的话则是与中继端口一一对应

使用命令

启动:service haproxy start
停止:service haproxy stop
重启:service haproxy restart
重载:service haproxy reload
状态:service haproxy status

参考资料

以下内容是haproxy 更详细的描述,转载自 keontang - notes

HAProxy 反向代理的使用


HAProxy 是一款高性能的反向代理软件,它可以基于四层或七层进行反向代理,尤其适合于高负载且需要进行七层处理的 Web 站点。
相较与 Nginx,HAProxy 更专注与反向代理,因此它可以支持更多的选项,更精细的控制,更多的健康状态检测机制和负载均衡算法。

性能

HAproxy 主要借助于现代操作系统上几种常见的技术来实现性能的最大化。

  • 单进程、事件驱动模型,降低了上下文切换和内存的开销
  • O(1) 时间检查器,允许其在高并发连接中对任何连接的时间实现即使探测。
  • 单缓冲(single buffering)机制能以不复制任何数据的方式完成读写操作,借助 splice() 系统调用,可以实现零复制转发。
  • 树型存储,使用作者多年前开发的弹性二叉树,实现 O(log(N)) 的低开销

在生产环境中,通常将 HAProxy 作为七层负载均衡器,实现 Web 集群的负载均衡,动静分离等功能。

配置 HAProxy

HAProxy 的配置文件位于 /etc/haproxy/haproxy.cfg。HAProxy 配置处理主要来源有三类:

1. 命令行参数
2. global 配置端,用于设定全局配置参数
3. proxy 相关配置端,如 defaultlistenfrontendbackend

HAProxy 中,涉及时间单位的配置参数的默认后缀一般是毫秒,也可以使用 s(秒),m(分钟),h(小时),d(天)等单位

全局配置

全局配置为 global 配置中的参数,有进程管理及安全相关的参数

chroot <DIR>
修改 HAProxy 的工作目录至指定的目录并在放弃权限之前执行 chroot() 操作,可以提升 haproxy 的安全级别

daemon
以守护进程方式运行

log <address> <facility> [max level [min level]]
定义日志位置

nbproc <NUMBER>
指定启动的 HAProxy 进程个数,默认启动一个进程,建议启动一个进程即可

uid <UID>
指定以 UID 身份的用户运行 HAProxy

maxconn <NUMBER>
设定每个 HAProxy 进程所接受的最大并发连接数

spread-checks <0..50, inpercent>
在haproxy后端有着众多服务器的场景中,在精确的时间间隔后统一对众服务器进行健康状况检查可能会带来意外问题;此选项用于将其检查的时间间隔长度上增加或减小一定的随机时长

ulimit-n
设定每进程能够打开的最大文件描述符数,默认情况下棋会自动进行计算,不推荐手动设置

代理配置

代理配置可在一下配置段中定义:

  • default:用于为所有其它配置段提供默认参数
  • frontend:用于定义一系列监听的套接字,这些套接字可接受客户端请求并与之建立连接
  • backend:用于定义一系列后端服务器,代理将会将对应客户端的请求转发至这些服务器
  • listen: 通过关联前端和后端定义了一个完整的代理
前后端连接模型

在 HTTP 模式下,HAProxy 与前后端的连接方式取决于 frontend 和 backend 的连接选项。HAProxy 支持 5 中连接模型:

  • KAL: keep alive(option http-keep-alive),这是默认的模式,所有的请求和响应都会被 HAProxy 处理,且允许在没有请求和响应时保持空闲的连接
  • TUN:tunnel(option http-tunnel):这是 1.0 ~ 1.5-dev21 的默认模式,类似于隧道,HAProxy 仅处理第一个请求和响应,剩余的报文将直接转发而不进行处理。尽量不要使用这个模式,因为它在日志记录和 HTTP 处理上有很多问题。
  • PCL:passive close(option httpclose),这和 tunnel 模式类似,区别是 HAProxy 会在发往客户端的响应报文和发往服务器的请求报文中加入 “Connection: close” 首部,使得客户端和后端主机在完成与 HAProxy 的一次通信后主动的关闭连接。
  • SCL:server close(option http-server-close),HAProxy 在接收到后端服务器的响应后就立即断开与后端服务器的连接,而与客户端的连接则使用保持连接。
  • FCL:forced close(option forceclose),HAProxy 每完成一次与客户端/服务器的通信(请求+响应)后就主动关闭连接。
  • max-keep-alive-queue <value>
    用于设定后端主机保持连接数的阈值,当某后端主机的保持连接队列超过此值后,HAProxy 会将向此主机请求的保持连接调度至其他主机。默认值 -1 表示不限制, 0 表示禁用保持连接。
负载均衡
  • balance <algorithm> [ <arguments> ]
    定义负载均衡算法,可用于“defaults”、“listen” 和 “backend”。用于在负载均衡场景中挑选一个server,其仅应用于持久信息不可用的条件下或需要将一个连接重新派发至另一个服务器时。支持的算法有:
    • roundrobin:基于权重进行轮叫,在服务器的处理时间保持均匀分布时,这是最平衡、最公平的算法。此算法是动态的,这表示其权重可以在运行时进行调整,不过,在设计上,每个后端服务器仅能最多接受4128个连接
    • static-rr:基于权重进行轮叫,与roundrobin类似,但是为静态方法,在运行时调整其服务器权重不会生效;不过,其在后端服务器连接数上没有限制
    • leastconn:新的连接请求被派发至具有最少连接数目的后端服务器;在有着较长时间会话的场景中推荐使用此算法,如LDAP、SQL等,其并不太适用于较短会话的应用层协议,如HTTP;此算法是动态的,可以在运行时调整其权重
    • source:将请求的源地址进行hash运算,并由后端服务器的权重总数相除后派发至某匹配的服务器;这可以使得同一个客户端IP的请求始终被派发至某特定的服务器;不过,当服务器权重总数发生变化时,如某服务器宕机或添加了新的服务器,许多客户端的请求可能会被派发至与此前请求不同的服务器;常用于负载均衡无cookie功能的基于TCP的协议;其默认为静态,不过也可以使用hash-type修改此特性
    • uri:对URI的左半部分(“?”标记之前的部分)或整个URI进行hash运算,并由服务器的总权重相除后派发至某匹配的服务器;这可以使得对同一个URI的请求总是被派发至某特定的服务器,除非服务器的权重总数发生了变化;此算法常用于代理缓存或反病毒代理以提高缓存的命中率;需要注意的是,此算法仅应用于HTTP后端服务器场景;其默认为静态算法,不过也可以使用hash-type修改此特性
    • url_param:通过为URL指定的参数在每个HTTP GET请求中将会被检索;如果找到了指定的参数且其通过等于号“=”被赋予了一个值,那么此值将被执行hash运算并被服务器的总权重相除后派发至某匹配的服务器;此算法可以通过追踪请求中的用户标识进而确保同一个用户ID的请求将被送往同一个特定的服务器,除非服务器的总权重发生了变化;如果某请求中没有出现指定的参数或其没有有效值,则使用轮叫算法对相应请求进行调度;此算法默认为静态的,不过其也可以使用hash-type修改此特性
    • hdr():对于每个HTTP请求,通过指定的HTTP首部将会被检索;如果相应的首部没有出现或其没有有效值,则使用轮叫算法对相应请求进行调度;其有一个可选选项“use_domain_only”,可在指定检索类似Host类的首部时仅计算域名部分(比如通过www.magedu.com来说,仅计算magedu字符串的hash值)以降低hash算法的运算量;此算法默认为静态的,不过其也可以使用hash-type修改此特性
  • hash-type <method>
    用于定义将hash码映射至后端服务器的方法:
    • map-based:hash表是一个包含了所有在线服务器的静态数组。其hash值将会非常平滑,会将权重考虑在列,但其为静态方法,当一台服务器宕机或添加了一台新的服务器时,大多数连接将会被重新派发至一个与此前不同的服务器上,对于缓存服务器的工作场景来说,此方法不甚适用。
    • consistent:hash表是一个由各服务器填充而成的树状结构;基于hash键在hash树中查找相应的服务器时,最近的服务器将被选中。此方法是动态的,添加一个新的服务器时,仅会对一小部分请求产生影响,因此,尤其适用于后端服务器为cache的场景。
健康状态检查
  • option httpchk [method] [uri] [version]
    默认情况下,HAProxy 的后端主机健康状态检查是基于 TCP 连接来检查的。当使用 option httpchk 后,将使用一个 HTTP 请求来检查后端主机健康状态,2xx 和 3xx 的响应码表示健康状态,其他响应码或无响应表示服务器故障。
    检查端口和间隔在 server 配置中指定。
    backend https_relay
      mod tcp
      option httpchk OPTIONS * HTTP/1.1
      server apache1 192.168.1.1:443 check port 80
    
  • http-check disable-on-404
    使用此选项后,返回 HTTP 404 状态码的后端主机将会从负载均衡列表中移除,但是仍能够继续处理已建立的连接。这可以用于手动的平滑下限。如果服务器重新开始返回 2xx 或 3xx 的状态码,将会重新被加入至负载均衡主机列表。

  • http-check expect [!] <match> <pattern>
    此选项用于对 HTTP 检查返回的页面进行内容匹配或返回状态码匹配

match 指定匹配方式,status 表示精确匹配状态码,rstatus 表示使用正则表达式匹配状态码,string 表示精确匹配响应实体字符串,rstring 表示使用正则表达式匹配响应实体。同时还可以使用 “!” 表示取反,如 ! status,返回内容的大小受 tune.chksize 参数限制,默认为 16384 字节。

http-check expect ! string SQL Error
如果遇到 SQL Error 的页面测健康状态为故障
端口指定
  • bind [<address>]:<port_range> [, ...]
    此指令仅能用于frontend和listen区段,用于定义一个或几个监听的套接字。
工作模式
  • mode {tcp | http | health}
    设定实例的运行模式或协议。当实现内容交换时,前端和后端必须工作于同一种模式(一般说来都是HTTP模式),否则将无法启动实例。
指定后端主机
  • use_backend <backend> <if | unless> <condition>
    用于指定匹配某 condition 时使用的后端主机

  • default_backend <backend>
    指定默认情况下的后端主机

例:

use_backend     dynamic  if  url_dyn
use_backend     static   if  url_css url_img extension_img
default_backend dynamic
  • server <name> <address>[:port] [param*]
    为后端声明一个主机,不能用于 default 和 frontend 段

name:为此服务器指定的内部名称,其将出现在日志及警告信息中;如果设定了”http-send-server-name”,它还将被添加至发往此服务器的请求首部中

address:此服务器的地址

[:port]:指定将连接请求所发往的此服务器时的目标端口

[param]:为此服务器设定的一系参数,下面仅说明几个常用的参数:

  • backup:设定为备用服务器,仅在负载均衡场景中的其它server均不可用于启用此server
  • check:启动对此server执行健康状态检查,其可以借助于额外的其它参数完成更精细的设定
  • cookie :为指定server设定cookie值,此处指定的值将在请求入站时被检查,第一次为此值挑选的server将在后续的请求中被选中,其目的在于实现持久连接的功能
  • maxconn:指定此服务器接受的最大并发连接数;如果发往此服务器的连接数目高于此处指定的值,其将被放置于请求队列,以等待其它连接被释放
  • maxqueue:设定请求队列的最大长度
  • observe:通过观察服务器的通信状况来判定其健康状态,默认为禁用,其支持的类型有“layer4”和“layer7”,“layer7”仅能用于http代理场景
  • weight:权重,默认为1,最大值为256,0表示不参与负载均衡
服务器状态输出

stats enable
开启状态输出页面

stats hide-version
隐藏 HAProxy 版本报告

stats realm
启用认证领域

stats auth <USER:PASSWORD>
认证的用户名和密码

stats uri URI
输出页面的 URI

例如:

listen status *:8080
    stats enable
    stats hide-version
    stats uri   /haproxy?stats
    stats realm  HAProxy\ Statistics
    stats auth  statsadmin:password
日志

option httplog
启用记录HTTP请求、会话状态和计时器的功能

option logasap
启用或禁用提前将 HTTP 请求记入日志,而不等待 HTTP 报文传输完毕

option forwardfor
在发往服务器的请求中插入 X-Forwarded-For 首部用于记录客户端的 IP

ACL 访问控制

haproxy的ACL用于实现基于请求报文的首部、响应报文的内容或其它的环境状态信息来做出转发决策,这大大增强了其配置弹性。其配置法则通常分为两步,首先去定义ACL,即定义一个测试条件,而后在条件得到满足时执行某特定的动作,如阻止请求或转发至某特定的后端。定义ACL的语法格式如下。

acl <aclname> <criterion> [flags] [operator] <value> ...

<aclname>:ACL名称,区分字符大小写,且其只能包含大小写字母、数字、-(连接线)、_(下划线)、.(点号)和:(冒号);haproxy中,acl可以重名,这可以把多个测试条件定义为一个共同的acl
<criterion>:测试标准,即对什么信息发起测试;测试方式可以由[flags]指定的标志进行调整;而有些测试标准也可以需要为其在<value>之前指定一个操作符[operator]
[flags]:目前haproxy的acl支持的标志位有3个:
    -i:不区分<value>中模式字符的大小写
    -f:从指定的文件中加载模式
    --:标志符的强制结束标记,在模式中的字符串像标记符时使用
<value>:acl测试条件支持的值有以下四类:
    - 整数或整数范围:如1024:65535表示从1024至65535;仅支持使用正整数(如果出现类似小数的标识,其为通常为版本测试),且支持使用的操作符有5个,分别为eq、ge、gt、le和lt
    - 字符串:支持使用“-i”以忽略字符大小写,支持使用“\”进行转义,如果在模式首部出现了-i,可以在其之前使用“--”标志位
    - 正则表达式:其机制类同字符串匹配
    - IP地址及网络地址

常用的测试标准
be_sess_rate <integer>
用于测试指定的backend上会话创建的速率(即每秒创建的会话数)

如:

backend dynamic
    mode http
    acl being_scanned be_sess_rate gt 50
    redirect location /error_pages/denied.html if being_scanned

hdr(HEADER) <string>
用于匹配请求报文中的指定首部

method <string>
用于匹配请求报文中使用的方法

path_beg <string>
用于测试请求的URL是否以 string 指定的模式开头

path_end <string>
用于测试请求的URL是否以 string 指定的模式结尾

path_reg <string>
用于测试请求的URL是否能以正则表达式 string 匹配

一个配置示例

global
    log         127.0.0.1 local2
    chroot      /var/lib/haproxy
    pidfile     /var/run/haproxy.pid
    maxconn     4000
    user        haproxy
    group       haproxy
    daemon

    # turn on stats unix socket
    stats socket /var/lib/haproxy/stats

defaults
    mode                    http
    log                     global
    option                  httplog
    option                  dontlognull
    option http-server-close
    option forwardfor       except 127.0.0.0/8
    option                  redispatch
    retries                 3
    timeout http-request    10s
    timeout queue           1m
    timeout connect         10s
    timeout client          1m
    timeout server          1m
    timeout http-keep-alive 10s
    timeout check           10s
    maxconn                 30000

listen stats
    mode http
    bind 0.0.0.0:1080
    stats enable
    stats hide-version
    stats uri     /haproxyadmin?stats
    stats realm   Haproxy\ Statistics
    stats auth    admin:admin
    stats admin if TRUE

frontend http-in
    bind *:80
    mode http
    log global
    option httpclose
    option logasap
    option dontlognull
    acl url_static       path_beg       -i /static /images /javascript /stylesheets
    acl url_static       path_end       -i .jpg .jpeg .gif .png .css .js

    use_backend static_servers          if url_static
    default_backend dynamic_servers

backend static_servers
    balance roundrobin
    server imgsrv1 172.16.200.7:80 check maxconn 6000
    server imgsrv2 172.16.200.8:80 check maxconn 6000

backend dynamic_servers
    cookie srv insert nocache
    balance roundrobin
    server websrv1 172.16.200.7:80 check maxconn 1000 cookie websrv1
    server websrv2 172.16.200.8:80 check maxconn 1000 cookie websrv2

nginx 防盗链

先补充一点HTTP的知识。

HTTP Referer是Header的一部分,当浏览器向Web服务器发送请求的时候,一般会带上Referer,告诉服务器是从哪个页面链接过来的,服务器借此可以获得一些信息用于处理。不过 HTTP Referer 可以通过程序来伪装生成的,所以通过Referer信息防盗链并非100%可靠,但是,它能够限制大部分的盗链。

用法

valid_referers [none|blocked|server_names] ...

默认值:none
使用环境:server,location
该指令会根据Referer Header头的内容分配一个值为0或1给变量 $invalid_referer。
如果Referer Header头不符合valid_referers指令设置的有效Referer,变量$invalid_referer 将被设置为1.

none:表示无Referer值的情况。
blocked:表示Referer值被防火墙进行伪装。
server_names:表示一个或多个主机名称。从Nginx 0.5.33版本开始,server_names中可以使用通配符"*"号。

配置

location ~ .*\.(gif|jpg|jpeg|png|bmp|swf)$
{
    valid_referers none *.kelu.org *.qq.com *.google.com *.baidu.com *.sinaimg.cn localhost;
    if ($invalid_referer) {
        rewrite ^/ https://wx3.sinaimg.cn/mw690/7b736eb7ly1fjr44z6lesj21hc0rs77f.jpg;
        #return 404;
    }
    expires      30d;
}

Go 语言笔记 - 复合数据类型

go 的复合类型包括:

  • 数组类型
  • 指针类型(Pointer)
  • 结构体类型(struct)
  • 切片类型 (slice)
  • Map 类型

数组

// 数组访问索引从0开始 var variable_name [SIZE] variable_type // 声明 var balance = [5] float32 {1000.0, 2.0, 3.4, 7.0, 50.0} // 初始化 var balance = […] float32 {1000.0, 2.0, 3.4, 7.0, 50.0} // 与上个语句效果相同

多维数组

var threedim [5][10][4]int

a = [3][4]int{  
	 {0, 1, 2, 3} ,   /*  第一行索引为 0 */
	 {4, 5, 6, 7} ,   /*  第二行索引为 1 */
	 {8, 9, 10, 11}   /*  第三行索引为 2 */
}

指针

var var_name *var-type
var ip *int  /* 指向整型*/  
var fp *float32 /* 指向浮点型 */

var a int=  20  /* 声明实际变量 */  
var ip *int  /* 声明指针变量 */ 
ip =  &a /* 指针变量的存储地址 */

当一个指针被定义后没有分配到任何变量时,它的值为 nil。nil 指针也称为空指针。

结构体

type Books struct {
   title string
   author string
   subject string
   book_id int
}

var Book1 Books        /* 声明 Book1 为 Books 类型 */

/* book 1 描述 */ Book1.title = “Go 语言” Book1.author = “www.runoob.com” Book1.subject = “Go 语言教程” Book1.book_id = 6495407

切片(动态数组)

切片是对数组的抽象。

Go 数组的长度不可改变,在特定场景中这样的集合就不太适用,Go中提供了一种灵活,功能强悍的内置类型切片(“动态数组”),与数组相比切片的长度是不固定的,可以追加元素,在追加时可能使切片的容量增大。

var xxx []type		// 声明,值为 nil,是空切片

或使用make()函数来创建切片,其中capacity为可选参数,说明切片可达到的最大数。

make([]type, length, capacity)

// 初始化
s :=[]  int  {1,2,3  }  

s := arr[:]   // 初始化切片s,是数组arr的引用

s := arr[startIndex:endIndex]   // 从下标startIndex到endIndex-1 下的元素创建为一个新的切片

s1 := s[startIndex:endIndex]   通过切片s初始化切片s1

s :=make([]int,len,cap)   通过内置函数make()初始化切片s,[]int 标识为其元素类型为int的切片

len() 和 cap() 函数

切片可以由 len() 方法获取长度。由 cap() 测量切片最长可以达到多少。

切片截取

可以通过设置下限、上限来设置截取切片 [lower-bound:upper-bound]

append() 和 copy() 函数

/* 同时添加多个元素 */ 
numbers = append(numbers,  2,3,4)
/* 拷贝 numbers 的内容到 numbers1 */ 
copy(numbers1,numbers)

Map(集合)

Map 是一种无序的键值对的集合,使用 hash 表来实现的。

/* 声明变量,默认 map 是 nil */  
var xxx map[key_data_type]value_data_type 

/* 使用 make 函数 */ 
xxx := make(map[key_data_type]value_data_type)

/* 创建 map */
countryCapitalMap := 
map[string]  string  {"France":"Paris","Italy":"Rome","Japan":"Tokyo","India":"New Delhi"}

/* 删除元素 */  
delete(countryCapitalMap,"France");

参考资料


Go 语言笔记 - 语法和数据类型

一、基础语法

一个标准 Go 语句如下:

fmt.Println("Hello, World!")
  • 一行代表一个语句结束。每个语句不需以分号 ; 结尾,也不是 Python 纯靠缩进来表示内容,需要大括号{}进行包裹
  • 标识符用来命名变量、类型等,第一个字符必须是字母或下划线而不能是数字。

Go 代码中会使用到的 25 个关键字或保留字:

  • default
  • func
  • interface
  • select
  • defer
  • go
  • map
  • struct
  • chan
  • if
  • else
  • switch
  • case
  • continue
  • for
  • break
  • range
  • goto
  • package
  • const
  • fallthrough
  • type
  • import
  • return
  • var

36 个预定义标识符:

  • append
  • bool
  • byte
  • cap
  • close
  • complex
  • complex64
  • complex128
  • uint16
  • copy
  • false
  • float32
  • float64
  • imag
  • int
  • int8
  • int16
  • uint32
  • int32
  • int64
  • iota
  • len
  • make
  • new
  • nil
  • panic
  • uint64
  • print
  • println
  • real
  • recover
  • string
  • true
  • uint
  • uint8
  • uintptr

二、数据类型

本部分的内容基本来自 《Go 语言设计与实现》 - draveness.me ,这里只是一些关键内容摘录,为了保持上下文内容连贯,还是推荐查看原文。

  • 布尔型 常量 true 或者 false。一个简单的例子:var b bool = true

  • 数字类型 整型 int 和浮点型 float,并且原生支持复数,其中位的运算采用补码。更详细的信息看文章末尾。

  • 字符串类型 使用UTF-8编码标识Unicode文本

  • 派生类型:

2.1 数组

arr1 := [3]int{1, 2, 3}
arr2 := [...]int{1, 2, 3}

字面量初始化方式,元素的个数小于或者等于四个时,在编译时会转换成如下命令:

var arr [3]int
arr[0] = 1
arr[1] = 2
arr[2] = 3

当前数组的元素大于四个,则会:

var arr [5]int
statictmp_0[0] = 1
statictmp_0[1] = 2
statictmp_0[2] = 3
statictmp_0[3] = 4
statictmp_0[4] = 5
arr = statictmp_0

数组在 Go 语言中没那么常用,更常用的数据结构是切片,即动态数组。

2.2 切片

[]int
[]interface{}

切片与数组的关系非常密切。切片引入了一个抽象层,提供了对数组中部分连续片段的引用,而作为数组的引用,我们可以在运行区间可以修改它的长度和范围。当切片底层的数组长度不足时就会触发扩容,切片指向的数组可能会发生变化,不过在上层看来切片是没有变化的.

2.2.1 基本操作

Go 语言中包含三种初始化切片的方式:

  1. 通过下标的方式获得数组或者切片的一部分,编译器会将 arr[0:3] 或者 slice[0:3] 等语句转换成 OpSliceMake 操作;
  2. 使用字面量初始化新的切片(编译时处理大部分工作);
  3. 使用关键字 make 创建切片(运行时处理相对多):

如果使用字面量的方式创建切片,大部分的工作都会在编译期间完成。但是当我们使用 make 关键字创建切片时,很多工作都需要运行时的参与。

arr[0:3] or slice[0:3] 
slice := []int{1, 2, 3}
slice := make([]int, 10)

使用 lencap 获取长度、容量是切片最常见的操作,编译器将这它们看成两种特殊操作,即 OLENOCAP

切片的长度是它所包含的元素个数。 切片的容量是从它的第一个元素开始数,到其底层数组元素末尾的个数

切片的操作(init、查、改、长度、容量)基本都是在编译期间完成的,编译期间也会将包含 range 关键字的遍历转换成形式更简单的循环。

2.2.2 切片扩容

切片追加和扩容时,会先解构切片结构体获取它的数组指针、大小和容量,切片容量足够时向切片中追加元素,切片容量不足时会调用 runtime.growslice 对切片进行扩容,扩容根据切片的当前容量选择不同的策略:

  1. 如果期望容量大于当前容量的两倍就会使用期望容量;
  2. 如果当前切片的长度小于 1024 就会将容量翻倍;
  3. 如果当前切片的长度大于 1024 就会每次增加 25% 的容量,直到新容量大于期望容量;

确定了切片的大致容量,此外还需要根据切片中的元素大小对齐内存,runtime.roundupsize 函数会将待申请的内存向上取整,取整时会使用 runtime.class_to_size 数组(以提高内存的分配效率并减少碎片)

var arr []int64
arr = append(arr, 1, 2, 3, 4, 5)

简单总结一下扩容的过程,当我们执行上述代码时,会触发 runtime.growslice 函数扩容 arr 切片并传入期望的新容量 5,这时期望分配的内存大小为 40 字节;不过因为切片中的元素大小等于 sys.PtrSize,所以运行时会调用 runtime.roundupsize 向上取整内存的大小到 48 字节,所以新切片的容量为 48 / 8 = 6。

2.2.3 切片拷贝

如果当前 copy 不是在运行时调用的, runtime.memmove 会负责拷贝内存。

而如果拷贝是在运行时发生的,例如:go copy(a, b),编译器会使用 runtime.slicecopy 替换运行期间调用的 copy,再通过 runtime.memmove 将整块内存的内容拷贝到目标的内存区域中。

相比于依次拷贝元素,runtime.memmove 能够提供更好的性能。需要注意的是,整块拷贝内存仍然会占用非常多的资源,在大切片上执行拷贝操作时一定要注意对性能的影响。

2.3 map

哈希表/map是除了数组之外,最常见的数据结构。几乎所有的语言都会有数组和哈希表两种集合元素,有的语言将数组实现成列表,而有的语言将哈希称作字典或者映射。无论如何命名或者如何实现,数组和哈希是两种设计集合元素的思路,数组用于表示元素的序列,而哈希表示的是键值对之间映射关系。

所谓哈希(hash),就是将不同的输入映射成独一无二的、固定长度的值(又称”哈希值”)。如果不同的输入得到了同一个哈希值,就发生了”哈希碰撞”(collision)。

img

黑客攻击的一种方法,就是设法制造”哈希碰撞”,然后入侵系统,窃取信息。防止哈希碰撞的最有效方法,就是扩大哈希值的取值空间。

16个二进制位的哈希值,产生碰撞的可能性是 65536 分之一。也就是说,如果有65537个用户,就一定会产生碰撞。哈希值的长度扩大到32个二进制位,碰撞的可能性就会下降到 4,294,967,296 分之一。

更长的哈希值意味着更大的存储空间、更多的计算,将影响性能和成本。开发者必须做出抉择,在安全与成本之间找到平衡。

哈希表是计算机科学中的最重要数据结构之一,这不仅因为它 O(1)O(1) 的读写性能非常优秀,还因为它提供了键值之间的映射。想要实现一个性能优异的哈希表,需要注意两个关键点 —— 哈希函数冲突解决方法

2.3.1 哈希函数

实现哈希表的关键点在于哈希函数的选择,哈希函数的选择在很大程度上能够决定哈希表的读写性能。在理想情况下,哈希函数应该能够将不同键映射到不同的索引上,这要求哈希函数的输出范围大于输入范围,但是由于键的数量会远远大于映射的范围,所以在实际使用时,这个理想的效果是不可能实现的。

如果使用结果分布较为均匀的哈希函数,那么哈希的增删改查的时间复杂度为 O(1);但是如果哈希函数的结果分布不均匀,那么所有操作的时间复杂度可能会达到 O(n),由此看来,使用好的哈希函数是至关重要的。

比较实际的方式是让哈希函数的结果能够尽可能的均匀分布,然后通过工程上的手段解决哈希碰撞的问题。

2.3.2 冲突解决
  • 开放寻址法 #

    开放寻址法中对性能影响最大的是装载因子,它是数组中元素的数量与数组大小的比值。随着装载因子的增加,线性探测的平均用时就会逐渐增加,这会影响哈希表的读写性能。当装载率超过 70% 之后,哈希表的性能就会急剧下降,而一旦装载率达到 100%,整个哈希表就会完全失效,这时查找和插入任意元素的时间复杂度都是 O(n)O(n) 的,这时需要遍历数组中的全部元素,所以在实现哈希表时一定要关注装载因子的变化。

  • 拉链法 #

    拉链法是哈希表最常见的实现方法,大多数的编程语言都用拉链法实现哈希表,它的实现比较开放地址法稍微复杂一些,但是平均查找的长度也比较短,各个用于存储节点的内存都是动态申请的,可以节省比较多的存储空间。

    实现拉链法一般会使用数组加上链表,不过一些编程语言会在拉链法的哈希中引入红黑树以优化性能,拉链法会使用链表数组作为哈希底层的数据结构,我们可以将它看成可以扩展的二维数组

哈希表可能会在装载因子过高或者溢出桶过多时进行扩容,哈希表扩容并不是原子过程,在扩容的过程中保证哈希的访问是比较有意思的话题.

2.3.3 map的数据结构

Go 语言使用拉链法来解决哈希碰撞的问题实现了哈希表,它的访问、写入和删除等操作都在编译期间转换成了运行时的函数或者方法。哈希在每一个桶中存储键对应哈希的前 8 位,当对哈希进行操作时,这些 tophash 就成为可以帮助哈希快速遍历桶中元素的缓存。

哈希表的每个桶都只能存储 8 个键值对,一旦当前哈希的某个桶超出 8 个,新的键值对就会存储到哈希的溢出桶中。随着键值对数量的增加,溢出桶的数量和哈希的装载因子也会逐渐升高,超过一定范围就会触发扩容,扩容会将桶的数量翻倍,元素再分配的过程也是在调用写操作时增量进行的,不会造成性能的瞬时巨大抖动。

Go 语言运行时同时使用了多个数据结构组合表示哈希表,其中 runtime.hmap 是最核心的结构体, runtime.hmap 的桶是 runtime.bmap。每一个 runtime.bmap 都能存储 8 个键值对,当哈希表中存储的数据过多,单个桶已经装满时就会使用 extra.nextOverflow 中桶存储溢出的数据。

溢出桶是在 Go 语言还使用 C 语言实现时使用的设计,由于它能够减少扩容的频率所以一直使用至今。

随着哈希表存储的数据逐渐增多,我们会扩容哈希表或者使用额外的桶存储溢出的数据,不会让单个桶中的数据超过 8 个。

溢出桶也只是临时的解决方案,创建过多的溢出桶最终也会导致哈希的扩容。

runtime.mapassign 函数会在以下两种情况发生时触发哈希的扩容:

  1. 装载因子已经超过 6.5;
  2. 哈希使用了太多溢出桶;

不过因为 Go 语言哈希的扩容不是一个原子的过程,所以 runtime.mapassign 还需要判断当前哈希是否已经处于扩容状态,避免二次扩容造成混乱。

2.3.4 map初始化
  1. 字面量

    hashMap := map[string]int{
    	"1": 2,
    	"3": 4,
    	"5": 6,
    }
    

    当哈希表中的元素数量少于或者等于 25 个时,编译器会将字面量初始化的结构体转换成以下的代码,将所有的键值对一次加入到哈希表中,初始化的方式与数组和切片几乎完全相同

    hashMap := make(map[string]int, 3)
    hashMap["1"] = 2
    hashMap["3"] = 4
    hashMap["5"] = 6
    

    一旦哈希表中元素的数量超过了 25 个,编译器会创建两个数组分别存储键和值,这些键值对会通过如下所示的 for 循环加入哈希:

    hashMap := make(map[string]int, 26)
    vstatk := []string{"1", "2", "3", ...  "26"}
    vstatv := []int{1, 2, 3, ... , 26}
    for i := 0; i < len(vstak); i++ {
        hashMap[vstatk[i]] = vstatv[i]
    }
    
  2. 运行时

    hashMap := make(map[string]int, 3)
    

    当创建的哈希被分配到栈上并且其容量小于 BUCKETSIZE = 8 时,Go 语言在编译阶段会使用如下方式快速初始化哈希,这也是编译器对小容量的哈希做的优化:

    var h *hmap
    var hv hmap
    var bv bmap
    h := &hv
    b := &bv
    h.buckets = b
    h.hash0 = fashtrand0()
    

    除了上述特定的优化之外,无论 make 是从哪里来的,只要我们使用 make 创建哈希,Go 语言编译器都会在类型检查期间将它们转换成 runtime.makemap,使用字面量初始化哈希也只是语言提供的辅助工具,最后调用的都是 runtime.makemap

2.3.5 一般使用
_ = hashMap[key]

for k, v := range hashMap {
    // k, v
}

hashMap[key] = value
hashMap[key] = newValue
delete(hashMap, key)

2.4 字符串

变量

声明变量的一般形式是使用 var 关键字:

var identifier type

变量声明

操作符 := 可以高效地创建一个新的变量,是使用变量的首选形式,但是只能被用在函数体内,不可以用于全局变量的声明与赋值。

var a int  =  10  // 指定变量类型,声明后若不赋值,使用默认值。
var b =  10 		// 根据值自行判定变量类型。
c :  =  10  		// 注意 :=左侧的变量不应该是已经声明过的,否则会导致编译错误。

多变量声明

//类型相同多个变量, 非全局变量  
var vname1, vname2, vname3 type
vname1, vname2, vname3 = v1, v2, v3 

var vname1, vname2, vname3 = v1, v2, v3

vname1, vname2, vname3 := v1, v2, v3 

// 这种因式分解关键字的写法一般用于声明全局变量
var  ( 
	vname1 v_type1
	vname2 v_type2 
)

常量

常量的定义格式:

const identifier [type]  = value

多个相同类型的声明可以简写为:

const c_name1, c_name2 = value1, value2

常量还可以用作枚举:

const  (  
	Unknown  =  0  
	Female  =  1  
	Male  =  2  
)

iota,特殊常量,可以认为是一个可以被编译器修改的常量。每当 iota 在新的一行被使用时,它的值都会自动加 1。

运算符

与 C/C++ 相当接近,不写了。速查手册

条件循环语句

条件语句

if  布尔表达式  {  
	/* 在布尔表达式为 true 时执行 */  
}  else  {  
	/* 在布尔表达式为 false 时执行 */  
}

switch var1 {  
	case val1:  ...  
	case val2:  ...  
	default:  ...  
}

var c1, c2, c3 chan int  
var i1, i2 int  

select  {  
	case i1 =  <-c1: 
		fmt.Printf("received ", i1,  " from c1\n")  
	case c2 <- i2: 
		fmt.Printf("sent ", i2,  " to c2\n")  
	
	default: 
		fmt.Printf("no communication\n")  
}

循环语句

package main

import "fmt"

func main() {

   var b int = 15
   var a int

   numbers := [6]int{1, 2, 3, 5} 

   /* for 循环 */
   for a := 0; a < 10; a++ {
      fmt.Printf("a 的值为: %d\n", a)
   }

   for a < b {
      a++
      fmt.Printf("a 的值为: %d\n", a)
      }

   for i,x:= range numbers {
      fmt.Printf("第 %d 位 x 的值 = %d\n", i,x)
   }   
}

附录:数字类型

整型:

序号 类型和描述
1 uint8 无符号 8 位整型 (0 到 255)
2 uint16 无符号 16 位整型 (0 到 65535)
3 uint32 无符号 32 位整型 (0 到 4294967295)
4 uint64 无符号 64 位整型 (0 到 18446744073709551615)
5 int8 有符号 8 位整型 (-128 到 127)
6 int16 有符号 16 位整型 (-32768 到 32767)
7 int32 有符号 32 位整型 (-2147483648 到 2147483647)
8 int64 有符号 64 位整型 (-9223372036854775808 到 9223372036854775807)

浮点型:

序号 类型和描述
1 float32 IEEE-754 32位浮点型数
2 float64 IEEE-754 64位浮点型数
3 complex64 32 位实数和虚数
4 complex128 64 位实数和虚数

其他数字类型

序号 类型和描述
1 byte 类似 uint8
2 rune 类似 int32
3 uint 32 或 64 位
4 int 与 uint 一样大小
5 uintptr 无符号整型,用于存放一个指针

参考资料