0%

需求:实现主从复制,且可以向从服务器写数据。

一、主服务器配置

1、启用binlog并设置服务器ID

编辑主服务器的配置文件 /etc/my.cnf(宝塔默认),添加如下内容:

1
2
3
4
5
[mysqld]
#不带路径则与数据库所在路径一致
log-bin=/www/server/data/mysql-bin
#服务器编号,在主从架构中必须唯一
server-id=1

如果省略server-id(或将其显式设置为默认值0),则主服务器拒绝来自从服务器的任何连接。

阅读全文 »

一、并发带来的问题

在多个线程同时开启事务后对数据库进行访问时,不可避免就会出现多个线程之间交叉访问而导致数据的不一致,通常存在以下问题:

1.1 脏读(读取未提交数据)

如下图,事务2读取了事务1未提交的数据。

举个栗子:
假设一个秒杀火车票的场景,目前上海到新化的火车票库存只有1张票了,张三(事务1)和李四(事务2)同时发起抢票,张三抢到了1张票,事务1正在扣除库存还没有提交入库,事务2此时查询发现还有余票(读到库存仍然为1),李四也可能买到了1张票,导致库存与销量不一致。

阅读全文 »

一、什么是Base64编码?

所谓Base64,就是说选出64个字符—-小写字母a-z、大写字母A-Z、数字0-9、符号”+”、”/“(再加上作为垫字的”=”,实际上是65个字符)—-作为一个基本字符集。然后,其他所有符号都转换成这个字符集中的字符。

具体来说,转换方式可以分为四步:
第一步,将每三个字节作为一组,一共是24个二进制位。
第二步,将这24个二进制位分为四组,每个组有6个二进制位。
第三步,在每组前面加两个0,扩展成32个二进制位,即四个字节。
第四步,根据下表,得到扩展后的每个字节的对应符号,这就是Base64的编码值。

阅读全文 »

一、启用go mod

1
2
go env -w GO111MODULE=on #开启 MODULE
go env -w GOPROXY=https://goproxy.cn,https://goproxy.io,direct #默认值为https://proxy.golang.org,direct

GO111MODULE可以设置为:off、on、auto(默认值),从GO111MODULE变量名可以看出,是Go1.11版本之后才出来有依赖包管理办法。

  • off时,则不使用go mod,查找依赖包的顺序是:当前项目根目录/vendor,其次是$GOPATH/src(这是Golang1.11版本之前的用法)。
  • on时,则开启go mod,查找依赖包是以当前项目根目录的go.mod文件为基准,会忽略 $GOPATHvendor 文件夹,只根据go.mod下载依赖。
  • auto或未设置,则go命令根据当前目录启用或禁用模块支持。仅当当前目录位于$GOPATH/src之外且其本身包含go.mod文件或位于包含go.mod文件的目录下时,才启用模块支持。

GOPROXY为依赖包代理地址,由于像golang.org/x这种依赖包需要翻墙才能下载,所以建议设置成国内镜像地址:
https://goproxy.cn 为国内七牛云维护的GO的镜像地址。
https://goproxy.io 为国内另一个镜像地址。

二、初始化go.mod

进入项目根目录(假如目录名为project1),初始化一个moudle,模块名为你的项目名,必须为英文名称,允许字母数字下划线和/,但是不能以/开头。

1
go mod init [模块名]

如果我们的项目根目录在$GOPATH/src/中,模块名可以不填写,将自动生成,一般是与项目根目录名称同名,如project1github.com/project1;如果项目根目录不在$GOPATH/src/中,则模块名必须填写,模块名同样可以命名如project1github.com/project1,也就是说模块名不一定与路径对应起来,但如果我们使用了路径,如github.com/project1,后续也可以把项目搬到$GOPATH/src/github.com目录下去。
执行完后,会在当前项目根目录下创建一个go.mod文件,内容如:

1
2
3
module project1

go 1.13

此时还没有任何依赖包信息。

三、整理依赖包

1
go mod tidy

命令执行后,go mod会去项目文件中发现依赖包,将依赖包名单添加到go.mod文件中,自动删除那些有错误或者没有使用的依赖包。

四、将依赖包拷贝到项目vendor

1
go mod vendor

在项目完工时,我们也可以把依赖包复制到项目根目录/vendor/,以便存档,其实也可以不做这个操作,有go.mod文档就够了。
项目根目录下有了vendor后,就可以这样编译:

1
2
go build -mod vendor -o ./project1  //project1为编译后的包名,或如下面,加上文件名
go build -mod vendor -o ./project1 main.go

五、其他常用命令

下载依赖包到本地缓存

1
go mod download

若之前拉过一次代码,会把该次go.mod里面的版本信息缓存到$GOPATH/pkg/mod/cache里面,
下载时如果cache有该版本信息,就用cache里面的版本信息去下载,否则重新下载。
注意:如果你已经拉取了一个tag版本下的包,若这个tag包含的信息被修改了,需要清除cache才能重新拉取这个这个更新后tag的信息。

清理本地依赖包

1
go clean -modcache

以上命令是go mod download的反操作,执行后会删除放置在$GOPATH/pkg/mod/cache下的依赖包缓存,有时依赖包出现错误,可以先清理后再重新下载到本地缓存。

添加单个依赖包

1
go mod edit -require=golang.org/x/text

移除单个依赖包
如果我们不再使用某个包了,可以单独移除,如:

1
go mod edit -droprequire=golang.org/x/text

验证依赖是否正确

1
go mod verify

解释为什么需要依赖

1
go mod why

校验所有依赖包的正确性

1
go mod verify

六、go.mod文档说明

go.mod文件可以通过require,replace,exclude语句来说明依赖包的管理规则:
require语句用于指定必不可少的依赖包
replace语句用于指定需要替换依赖包的地址,比如golang.org/x/text包替换成github.com/golang/text
exclude语句用于指定可以忽略依赖项模块
例如:

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
replace (
cloud.google.com/go => github.com/googleapis/google-cloud-go v0.34.0
github.com/go-tomb/tomb => gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7
go.opencensus.io => github.com/census-instrumentation/opencensus-go v0.19.0
go.uber.org/atomic => github.com/uber-go/atomic v1.3.2
go.uber.org/multierr => github.com/uber-go/multierr v1.1.0
go.uber.org/zap => github.com/uber-go/zap v1.9.1

golang.org/x/crypto => github.com/golang/crypto v0.0.0-20181001203147-e3636079e1a4
golang.org/x/lint => github.com/golang/lint v0.0.0-20181026193005-c67002cb31c3
golang.org/x/net => github.com/golang/net v0.0.0-20180826012351-8a410e7b638d
golang.org/x/oauth2 => github.com/golang/oauth2 v0.0.0-20180821212333-d2e6202438be
golang.org/x/sync => github.com/golang/sync v0.0.0-20181108010431-42b317875d0f
golang.org/x/sys => github.com/golang/sys v0.0.0-20181116152217-5ac8a444bdc5
golang.org/x/text => github.com/golang/text v0.3.0
golang.org/x/time => github.com/golang/time v0.0.0-20180412165947-fbb02b2291d2
golang.org/x/tools => github.com/golang/tools v0.0.0-20181219222714-6e267b5cc78e
google.golang.org/api => github.com/googleapis/google-api-go-client v0.0.0-20181220000619-583d854617af
google.golang.org/appengine => github.com/golang/appengine v1.3.0
google.golang.org/genproto => github.com/google/go-genproto v0.0.0-20181219182458-5a97ab628bfb
google.golang.org/grpc => github.com/grpc/grpc-go v1.17.0
gopkg.in/alecthomas/kingpin.v2 => github.com/alecthomas/kingpin v2.2.6+incompatible
gopkg.in/mgo.v2 => github.com/go-mgo/mgo v0.0.0-20180705113604-9856a29383ce
gopkg.in/vmihailenco/msgpack.v2 => github.com/vmihailenco/msgpack v2.9.1+incompatible
gopkg.in/yaml.v2 => github.com/go-yaml/yaml v0.0.0-20181115110504-51d6538a90f8
labix.org/v2/mgo => github.com/go-mgo/mgo v0.0.0-20160801194620-b6121c6199b7
launchpad.net/gocheck => github.com/go-check/check v0.0.0-20180628173108-788fd7840127
)

主要包括:golang.org google.golang.org gopkg.in go.uber.org cloud.google.com 在下载包时会有timeout 导致编译失败,以上是对应的github库的替换。

七、go get的使用

使用go mod之后,go get拉取依赖的方式就发生了变化:
1、老的go get取包过程类似:git clone + go install , 开启Go Module功能后go get就只有git clone或者 download过程了。
2、新老实现还有一个不同是,两者存包的位置不同。前者,存放在$GOPATH/src目录下;后者,存放在$GOPATH/pkg/mod目录下。
3、老的go get取完主包后,会对其repo下的submodule进行循环拉取。新的go get不再支持submodule子模块拉取。

查看指定包可以下载的版本

1
go list -m -versions github.com/gogf/gf

下载项目依赖

1
go get ./...

拉取最新的版本(优先择取 tag)

1
go get golang.org/x/text@latest

拉取 master 分支的最新 commit

1
go get golang.org/x/text@master

拉取 tag 为 v0.3.2 的 commit

1
go get golang.org/x/text@v0.3.2

拉取 hash 为 342b231 的 commit,最终会被转换为 v0.3.2:

1
go get golang.org/x/text@342b2e

指定版本拉取,拉取v3版本

1
go get github.com/smartwalle/alipay/v3

更新

1
go get -u

八、go mod与原GOPATH的区别

1、环境变量GOPATH不再用于解析imports包路径,即原有的$GOPATH/src/下的包,通过import是找不到了
2、Go Module功能开启后,下载的包将存放与$GOPATH/pkg/mod路径
3、$GOPATH/bin路径的功能依旧保持

总结

在Go1.11版本之前,依赖包管理一直是GOPATH的包管理方式,不太灵活方便,1.11版本发布后,带来了全新的go mod管理方式,用官方的话说,这是 GOPATH 的替代方案,集成了对版本控制和软件包分发的支持。可以看出借鉴了nodejs等包管理方式,项目的创建不再需要放在固定的$GOPATH/src/路径中,配合着国内的依赖包镜像源,体验很顺滑,推荐!

一、安装Java

官网下载jdk1.8.0_212.tar.gz,解压到如下路径即可。
本次安装路径为:/usr/local/java/jdk1.8.0_212/
安装过程略。

二、安装MyCat

官网:http://www.mycat.io/
GitHub地址:https://github.com/MyCATApache/Mycat-Server
下载地址:http://dl.mycat.io/1.6.7.1/Mycat-server-1.6.7.1-release-20190627191042-linux.tar.gz
本次安装采用最新版本Mycat-server-1.6.7.4-release-20200105164103-linux.tar.gz,解压到如下路径即可。
本次安装路径为:/usr/local/mycat

1
2
3
4
cd /usr/local/src/
#wget -c http://dl.mycat.io/1.6.7.1/Mycat-server-1.6.7.1-release-20190627191042-linux.tar.gz
tar -xzvf Mycat-server-1.6.7.4-release-20200105164103-linux.tar.gz
mv mycat ../

MyCat 1.6.5-release和1.6.7.4-release都测试过了,在jdk1.8环境下都是没有问题的。

三、配置环境变量

1
2
3
4
5
6
7
vim /etc/profile
#在文件末尾加上如下代码:
export JAVA_HOME=/usr/local/java/jdk1.8.0_212/
export JRE_HOME=$JAVA_HOME/jre
export CLASSPATH=$JAVA_HOME/lib:$JRE_HOME/lib:.
export MYCAT_HOME=/usr/local/mycat
export PATH=$JAVA_HOME/bin:$JRE_HOME/bin:$MYCAT_HOME/bin:$PATH

四、创建MyCat用户

1
2
3
adduser mycat 
chown -R mycat:mycat /usr/local/mycat
chmod -R 777 /usr/local/mycat/bin

五、配置MyCat

本文以笔者之前开发的一个广告平台项目ArtarvaSSP为例。

1、schema.xml配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?xml version="1.0"?>
<!DOCTYPE mycat:schema SYSTEM "schema.dtd">
<mycat:schema xmlns:mycat="http://io.mycat/">
<schema name="db_ssp" checkSQLschema="true" sqlMaxLimit="100" dataNode="dn1">
<table name="ssp_ads_fail_log" primaryKey="id" autoIncrement="true" subTables="ssp_ads_fail_log_2020_$1-52" rule="sharding-by-date" dataNode="dn2" />
<table name="mycat_sequence" primaryKey="name" type="global" dataNode="dn1" />
</schema>
<dataNode name="dn1" dataHost="dh1" database="db_ssp" />
<dataNode name="dn2" dataHost="dh2" database="db_ssp_log" />

<dataHost name="dh1" maxCon="1000" minCon="10" balance="0" writeType="0" dbType="mysql" dbDriver="native" switchType="1" slaveThreshold="100">
<heartbeat>select user()</heartbeat>
<writeHost host="hostM108_1" url="localhost:3306" user="db_ssp" password="SrNp6JKKmN26xwXG"/>
</dataHost>

<dataHost name="dh2" maxCon="1000" minCon="10" balance="0" writeType="0" dbType="mysql" dbDriver="native" switchType="1" slaveThreshold="100">
<heartbeat>select user()</heartbeat>
<writeHost host="hostM108_2" url="localhost:3306" user="db_ssp_log" password="ARZ76eCrkni4ZmTs"/>
</dataHost>

</mycat:schema>

这里主要是将ssp_ads_fail_log表分离到一个独立的库db_ssp_log中(放在dn2中),所以在schema中,单独声明了<table name="ssp_ads_fail_log" primaryKey="id" autoIncrement="true" subTables="ssp_ads_fail_log_2020_$1-52" rule="sharding-by-date" dataNode="dn2" />,分片规则为按日期划分(见rule.xml文件),52周共计52张表($1-52)。由于db_ssp_log库设置了独立的用户名密码,于是新建dn2。
mycat_sequence为全局自增表,主要用来接管数据库的自增字段,见文末附件。

2、server.xml配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mycat:server SYSTEM "server.dtd">
<mycat:server xmlns:mycat="http://io.mycat/">
<system>
<property name="nonePasswordLogin">0</property> <!-- 0为需要密码登陆、1为不需要密码登陆 ,默认为0,设置为1则需要指定默认账户-->
<property name="useHandshakeV10">1</property>
<property name="useSqlStat">0</property>
<property name="useGlobleTableCheck">0</property>
<property name="sequnceHandlerType">1</property> <!-- 指定使用 Mycat 全局序列的类型。0为本地文件方式,1为数据库方式,2为时间戳序列方式,3为分布式ZK ID 生成器,4 为zk 递增 id 生成。 -->
<property name="subqueryRelationshipCheck">false</property>
<property name="processorBufferPoolType">0</property>
<property name="serverPort">8066</property><!-- 设置服务端口为8066 -->
<property name="managerPort">9066</property><!-- 设置管理端口为9066 -->
<property name="handleDistributedTransactions">0</property>
<property name="useOffHeapForMerge">1</property>
<property name="memoryPageSize">64k</property>
<property name="spillsFileBufferSize">1k</property>
<property name="useStreamOutput">0</property>
<property name="systemReserveMemorySize">384m</property>
<property name="useZKSwitch">false</property>

</system>

<user name="db_ssp" defaultAccount="true">
<property name="password">SrNp6JKKmN26xwXG</property>
<property name="schemas">db_ssp</property>
</user>
</mycat:server>

这里主要配置了连接MyCat的用户名密码,允许访问的数据库,多个时可以用逗号隔开。为了让之前的项目从MySQL连接无缝切换到MyCat,这里的用户名密码与MySQL保持一致,到时只需要在项目里修改一下连接端口为8066就可以了。MyCat的服务端口为8066,如设置<property name="serverPort">8066</property>

3、rule.xml配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mycat:rule SYSTEM "rule.dtd">
<mycat:rule xmlns:mycat="http://io.mycat/">
<tableRule name="sharding-by-date">
<rule>
<columns>access_date</columns>
<algorithm>sharding-by-date</algorithm>
</rule>
</tableRule>
<function name="sharding-by-date" class="io.mycat.route.function.PartitionByDate">
<property name="dateFormat">yyyyMMdd</property>
<property name="sBeginDate">20200101</property>
<property name="sEndDate">20201229</property>
<property name="sPartionDay">7</property>
</function>

</mycat:rule>

这里主要是按日期(7日)来切分数据表,最近7日的数据放在一个数据表,2020年全年52周零2天。
注意:开始日期与结束日期的天数必须能整除7,否则MyCat启动不了。

4、log4j2.xml日志配置:
conf/log4j2.xml文件,配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<?xml version="1.0" encoding="UTF-8"?>
<Configuration status="WARN">
<Appenders>
<Console name="Console" target="SYSTEM_OUT">
<PatternLayout pattern="%d [%-5p][%t] %m %throwable{full} (%C:%F:%L) %n"/>
</Console>

<RollingFile name="RollingFile" fileName="${sys:MYCAT_HOME}/logs/mycat.log"
filePattern="${sys:MYCAT_HOME}/logs/$${date:yyyy-MM}/mycat-%d{MM-dd}-%i.log.gz">
<PatternLayout>
<Pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %5p [%t] (%l) - %m%n</Pattern>
</PatternLayout>
<Policies>
<OnStartupTriggeringPolicy/>
<SizeBasedTriggeringPolicy size="250 MB"/>
<TimeBasedTriggeringPolicy/>
</Policies>
</RollingFile>
</Appenders>
<Loggers>
<!--<AsyncLogger name="io.mycat" level="info" includeLocation="true" additivity="false">-->
<!--<AppenderRef ref="Console"/>-->
<!--<AppenderRef ref="RollingFile"/>-->
<!--</AsyncLogger>-->
<asyncRoot level="debug" includeLocation="true"> <!--日志级别设置,取值有: info、ware、 debug-->

<!--<AppenderRef ref="Console" />-->
<AppenderRef ref="RollingFile"/>

</asyncRoot>
</Loggers>
</Configuration>

建议在上线前期将日志级别修改成debug,可以看到详细的SQL执行日志(如调度到了哪台主机),观察一段时间,方便排查问题。Mycat运行稳定后将级别再调整为 info/ware(生产环境建议设置)。

六、启动MyCat

1
2
3
4
5
6
su - mycat #切换到mycat用户
source /etc/profile
mycat install #将mycat安装成自启动服务
mycat start
mycat status
Mycat-server is running (18098). #看到此消息说明启动成功了

如果启动失败,可以查看logs目录下的日志文件。

常用命令:
mycat { console | start | stop | restart | status | dump }

附录

mycat_sequence结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
CREATE TABLE `mycat_sequence` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(50) NOT NULL DEFAULT '' COMMENT '自增变量名称',
`current_value` int(11) unsigned NOT NULL COMMENT '当前值',
`increment` int(11) unsigned NOT NULL DEFAULT '100' COMMENT '增量步长',
`remark` varchar(100) NOT NULL DEFAULT '' COMMENT '备注(选填)',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE KEY `mycat_sequence_name` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

INSERT INTO `mycat_sequence`(`name`, `current_value`, `increment`, `remark`) VALUES ('GLOBAL', 100000, 100, '公共自增');
INSERT INTO `mycat_sequence`(`name`, `current_value`, `increment`, `remark`) VALUES ('ADS_FAIL_LOG', 4261454, 1, '广告日志自增');

在插入记录前,通过Go查询表获取一个自增数字,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
id, err := getMycatSequence("ADS_FAIL_LOG") //获取自增值

func getMycatSequence(name string) (int64, error) {
var currentValue,increment,newValue int64
db := models.ConnDB()
defer db.Close()

tx, err := db.Begin()
defer func() {
switch {
case err != nil:
tx.Rollback()
default:
err = tx.Commit()
}
}()

err = db.QueryRow("SELECT `current_value`,`increment` FROM `mycat_sequence` WHERE `name` = ? ORDER BY `name` asc LIMIT 1", name).Scan(&currentValue, &increment)
if err == sql.ErrNoRows {
panic("没有查询到mycat_sequence表中 "+ name +" 相关自增配置~\n")
return newValue, errors.New("")
} else if err != nil {
return newValue, err
}

newValue = currentValue + increment
stmt, err := db.Prepare("UPDATE `mycat_sequence` SET `current_value` = ? where `name`= ? ")
defer stmt.Close()
if err != nil {
beego.Error(err)
}
_, err = stmt.Exec(newValue, name)
if err != nil {
beego.Error(err)
}

return newValue, nil
}

遇到的问题

1、通过Navicat连接到Mycat时,提示too many connections,将连接数修改成1000,一会又出现前面的提示,通过show processlist;,发现有大量的线程处于Waiting for table level lock,执行的操作是往日志表ssp_ads_fail_log表(MyISAM存储引擎)中insert数据,由于日志信息是实时的,每次只插入一条。
在查询一些资料后,将问题定位到存储引擎的问题上来,主要是因为MyISAM是表级锁,频繁插入,锁的开销很大,MyISAM更适合批量插入场景,于是将存储引擎又改回InnoDB。

查看磁盘I/O繁忙程度的命令:iostat -x 2 10,查看最后几列数据。
命令可参考:https://www.cnblogs.com/cynchanpin/p/6936977.html

2、将MyISAM修改成InnoDB时,又遇到了新的问题,磁盘满了(数据库日志、项目日志、MyCat debug日志都挺多的,再加上备份数据库的包也挺多的)
于是开始清理磁盘,先找出前10个大文件du -m / | sort -n -r | head -n 10,清理多余的文件,然后将表修改成InnoDB:

1
alter table ssp_ads_fail_log engine="InnoDB";

再查看一下表的存储引擎是否已经修改成功:

1
2
show table status from db_ssp_log\G;
show engines; #查看数据库支持哪些存储引擎

参考:

https://segmentfault.com/a/1190000014767902 雪花算法

https://tech.meituan.com/2017/04/21/mt-leaf.html Leaf——美团点评分布式ID生成系统

https://www.jianshu.com/p/80069f6153d6 扩展阅读

什么是队列(queue)?

队列是一种存储、组织数据的数据结构,最大的特点就是先进先出(FIFO)。

什么是消息队列?

服务之间最常见的通信方式是直接调用彼此来通信,消息从一端发出后立即就可以达到另一端,称为即时消息通讯(同步通信);
消息从某一端发出后,首先进入一个容器进行临时存储,当达到某种条件后,再由这个容器发送给另一端,称为延迟消息通讯 (异步通信)。
而容器的一个具体实现就是MQ(Message Queue)。
可以通过小红和小明读书的故事来理解一下消息队列。

什么是RabbitMQ?

RabbitMQ是一个实现了AMQP(Advanced Message Queuing Protocol)高级消息队列协议的消息队列服务,用Erlang语言编写,Erlang语言是为电话交换机开发的语言,天生自带高并发光环,和高可用特性。

阅读全文 »

为什么需要指针?

简单地来说,就是保证软件功能实现的同时,尽量的节约内存资源,提高程序运行效率。

当给一个函数/方法传参的时候,我们要了解传进去的是值还是引用地址。当参数为基本类型时(如string, bool, int 及 float),传进去的大都是值,也就是另外复制了一份参数到当前的函数调用栈;当参数为高级类型时(如struct,array/slice,map,chan,func),传进去的基本都是引用地址,这个主要是因为虚拟机的内存管理导致的。

内存管理中的内存区域一般包括 heap(堆)和 stack(栈), stack 主要用来存储当前调用栈用到的简单类型数据:string,bool,int,float 等,这些类型的内存占用小,容易回收,基本上它们的值和指针占用的空间差不多,因此可以直接复制,GC也比较容易做针对性的优化。 复杂的高级类型占用的内存往往相对较大,存储在 heap 中,GC 回收频率相对较低,代价也较大,因此传引用/指针可以避免进行成本较高的复制操作,并且节省内存,提高程序运行效率。
关于堆和栈不太理解的童鞋,可以阅读:堆和栈的区别

因此,在下列情况可以考虑使用指针:
1、需要改变参数的值;
2、避免复制操作;
3、节省内存。

一、定义指针

var ptr1 *int64
刚定义的指针没有指向任何地址,为nil,所以也叫空指针。
如图:

我们假设图片上表示的是一个内存块,存放有变量名(户口)和值(住几口人),以及它的内存地址(门牌号),指针可以理解成一个特殊的变量,它也需要占用内存,它的特殊之处在于,它的值必须且只能存放内存地址,假设它在内存中的地址为:0xabcd

二、使用指针

1
2
3
var a int64 = 100
ptr1 = &a
fmt.Println(*ptr1)

如图:

我们先定义一个变量a,系统为变量a分配了一个内存地址0x1234
通过 & 符可以将变量 a 的内存地址0x1234取出来,并交给 ptr1保存,于是ptr1的值由空(nil)变成了0x1234,通过*ptr1就可以展示刚才存入的内存地址了。

三、指向指针的指针

归根结底还是指针类型,但定义时多了个*,例如:var ptr2 **int64

1
2
3
4
var ptr2 **int64
ptr2 = &ptr1 //ptr2为指针向指针的指针
fmt.Println(&a, ptr1, *ptr2) //三者完全等价
fmt.Println(a, *ptr1, **ptr2) //三者完全等价

如图:
接着前面的定义,我们定义了一个指向ptr1的指针ptr2,此时ptr2的值为ptr1的内存地址,通过&a, ptr1, *ptr2都可以拿到内存地址0x1234,通过a, *ptr1, **ptr2都可以拿到值100

四、将指针作为函数参数

例如:我们用函数来实现交换a和b的值:

1
2
3
4
5
6
7
8
9
10
11
12
13
var a,b int64 = 100,200
fmt.Printf("交换前,a=%d,b=%d\n", a, b)
swap(&a, &b)
fmt.Printf("交换后,a=%d,b=%d\n", a, b)

//通过传递引用,交换两个变量的值
func swap(ptr1 *int64, ptr2 *int64) {
var tmp int64
tmp = *ptr1
*ptr1 = *ptr2
*ptr2 = tmp
//*ptr1,*ptr2 = *ptr2,*ptr1 //交换语句这样写更加简洁,也是 go 语言的特性,c++ 和 c# 是不能这么干的
}

如图:

swap函数的形参中,我们定义了两个指针ptr1ptr2分别用来接收a和b的地址,通过*ptr1*ptr2分别拿到值进行交换。在Go语言中,交换值还有更简洁的操作:*ptr1,*ptr2 = *ptr2,*ptr1,直接交换。

五、指针数组

说白了还是数组,只不过这个数组仅用来存放内存地址,也就有了一个书面术语——指针数组。
来看代码:

1
2
3
4
5
6
7
8
9
10
var arr = [3]int64 {1,2,3}
var ptrarr [3]*int64
for k,_ := range arr {
ptrarr[k] = &arr[k]
}
fmt.Println("指针指向的内存地址有:", ptrarr)
fmt.Println("遍历指针数组的值:")
for k,_ := range ptrarr {
fmt.Println(*ptrarr[k]);
}

上面代码定义了一个指针数组ptrarr,用于存放数组arr中的每一个元素的内存地址,然后通过ptrarr可以直接查看到值都是内存地址,通过*ptrarr[k]就可以获取到arr每一个元素的值。

有了指针,为什么还要有指针数组,因为数组(指针数组)在内存中的地址一般情况下都是连续分配的,比零散存放的变量(指针)查找存取更有效率,速度更快。

六、结构体指针

6.1 说白了还是指针,只不过这个指针是指向了一个结构体,
例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
type Person struct {
name string
sex rune //rune等同于int32
height uint8
}

var ouyang Person
ouyang.name = "欧阳"
ouyang.sex = '男'
ouyang.height = 175
//也可以这样赋值:ouyang := Person("欧阳", '男', 175)

var ptr1 *Person //定义结构体指针,格式:var 指针名称 *结构体名
ptr1 = &ouyang
fmt.Println((*ptr1).name, ptr1.name)

从以上代码可以得出,定义结构体指针分四步:
1、定义结构体类型
2、定义指针
3、实例化结构体
4、指针指向结构体实例

与指向变量或数组的指针不一样的地方在于,多了一种读取方式:Go提供了一种隐式解引用特性,可以直接用指针名.结构字段的形式访问值,如上面代码:(*ptr1).nameptr1.name都可以获取到值。

6.2 也可以将结构体指针作为函数参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
func main()  {
type Person struct {
name string
sex rune //rune等同于int32
height uint8
}
var ouyang Person
ouyang.name = "欧阳"
ouyang.sex = '男'
ouyang.height = 175
printPerson(&ouyang)
}

func printPerson(ptr1 *Person) {
fmt.Println(ptr1.name, ptr1.sex, ptr1.height)
}

总结

在学校学习C语言的时候,指针没学好,所以有些阴影,本文试着用简单的代码来理解GO的指针,希望给自己加深印象。不同于 C 语言,Golang 的指针是单独的类型,而不是 C 语言中的 int 类型,而且也不能对指针做整数运算。传递指针给函数不但可以节省内存(因为没有复制变量的值),而且赋予了函数直接修改外部变量的能力。