gevent backdoor 实践

gevent 提供了一个后门server,可以用来debug正在运行的进程,官方文档是这样描述的

The BackdoorServer provides a REPL inside a running process. As long as the process is monkey-patched, the BackdoorServer can coexist with other elements of the process.

下面这个例子是从stackoverflow上看到的,这里自己实践一下

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
import json
from gevent import pywsgi
from gevent import monkey
from gevent import event
from gevent.backdoor import BackdoorServer
import time

monkey.patch_all()

a = "foo"

def build_response(response):
return json.dumps(response)


def start_process(environ, start_response):
'''
{"content": {"code": 0, "req_id": "20160420154445000"}, "res": "ok", "err_code": 0}
'''
risk_id = 0
response = dict()
response = {
'err_code': 0,
'res': 'ok',
'content': dict()
}
status = '200 OK'
response_headers = [('Content-type', 'text/html'), ('Connection', 'close')]
start_response(status, response_headers)
return build_response(response)


def main():
stop_event = event.Event()
servers = [BackdoorServer(('127.0.0.1', 5001),
banner="Hello from gevent backdoor!",
locals={'foo': "From defined scope!"}), pywsgi.WSGIServer(('127.0.0.1', 5002),
start_process, ), ]
for server in servers:
if not server.started:
server.start()
stop_event.wait()
for server in servers:
if server.started:
server.stop()


if __name__ == '__main__':
main()

将这个python文件run起来,然后执行telnet 127.0.0.1 5001就可以链接到上面的进程中去

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#  telnet 127.0.0.1 5001
>>> import greenlet, inspect
# 获取当前协程
>>> greenlet.getcurrent()
<_Greenlet_stdreplace at 0x10bd40448: _handle_and_close_when_done(<bound method BackdoorServer.handle of <BackdoorSe, <bound method StreamServer.do_close of <BackdoorSe, (<gevent._socket3.socket object, fd=8, family=2, t)>
# 获取当前协程的父协程,看到是hub,这其实是gevent的调度协程
>>> greenlet.getcurrent().parent
<Hub '' at 0x10bcdd6d8 select default pending=0 ref=2 resolver=<gevent.resolver_thread.Resolver at 0x10bd167f0 pool=<ThreadPool at 0x10bd169b0 0/1/10 hub=<Hub at 0x10bcdd6d8 thread_ident=0x140736039191424>>> threadpool=<ThreadPool at 0x10bd169b0 0/1/10 hub=<Hub at 0x10bcdd6d8 thread_ident=0x140736039191424>> thread_ident=0x7fffa99f8380>
# 再网上走一层,这里就到了我们运行main的协程
>>> inspect.getouterframes(greenlet.getcurrent().parent.parent.gr_frame)
[FrameInfo(frame=<frame object at 0x7faecf476e48>, filename='xxxxxxxxxx', lineno=100, function='main', code_context=[' stop_event.wait()\n'], index=0), FrameInfo(frame=<frame object at 0x7faecf41ea78>, filename='xxxxxxxxxxx', lineno=107, function='<module>', code_context=[' main()\n'], index=0)]
>>> inspect.getouterframes(greenlet.getcurrent().parent.parent.gr_frame)[1][0]
<frame object at 0x7faecf41ea78>
# 通过inspect可以获取变量a的值,这样就可以debug当前的进程了
>>> inspect.getargvalues(inspect.getouterframes(greenlet.getcurrent().parent.parent.gr_frame)[1][0]).locals['a']
'foo'

hmm example

原始文档

the plane can fly . the typical plane can see the plane . a typical fly can see . who might see ? the large can might see a can . the can can destroy a large can . who might see ? who might fly ? who can fly ? the can might see . the plane can fly a typical fly . who can fly ? the

分句

1
./../sentsplit.pl example0.train example0.sentences

the plane can fly .
the typical plane can see the plane .
a typical fly can see .
who might see ?
the large can might see a can .
the can can destroy a large can .
who might see ?

标注数据

the/at plane/nn can/md fly/vb ./.
the/at typical/jj plane/nn can/md see/vb the/at plane/nn ./.
a/at typical/jj fly/nn can/md see/vb ./.
who/wps might/md see/vb ?/.
the/at large/jj can/nn might/md see/vb a/at can/nn ./.
the/at can/nn can/md destroy/vb a/at large/jj can/nn ./.
who/wps might/md see/vb ?/.

数据编号

1
./../create_key.pl words.key < example0.sentences > example0.seq

单词

1 the
6 typical
3 can
8 a
9 who
13 destroy
7 see
2 plane
11 ?
10 might
5 .
12 large
4 fly

标注

6 jj
7 wps
2 nn
3 md
1 at
4 vb
5 .

词频统计

1
./../pretrain.pl example0.all lex ngram

词型及其词性标记的组合在训练集中出现的次数

plane nn 34
a at 58
see vb 45
? . 57
typical jj 25
large jj 22
destroy vb 9
can md 58
might md 42
can nn 39
fly nn 20
who wps 57
fly vb 46
. . 43
the at 35

一元词性及二元词性在训练集中的出现次数

md 100
wps 57
at 93
. 100
nn 93
vb 100
jj 47
vb . 50
wps md 57
at jj 47
nn . 50
nn md 43
vb at 50
at nn 46
md vb 100
jj nn 47

模型训练

1
./../hmmtrain.pl words.key pos.key ngram lex example.hmm
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
M= 13
N= 7
A:
0.001002 0.505874 0.001150 0.001003 0.001004 0.001000 0.494967
0.001000 0.001000 0.001422 0.001004 0.001003 0.001003 0.999567
0.500420 0.001002 0.001001 0.001001 0.001000 0.500576 0.001000
0.001001 0.001000 0.999848 0.001002 0.001001 0.001003 0.001146
0.001000 0.001003 0.001000 0.999995 0.001000 0.001000 0.001003
0.424812 0.001003 0.001001 0.001000 0.576183 0.001000 0.001002
0.001000 0.001000 0.001003 0.462992 0.001000 0.538002 0.001002
B:
0.376957 0.001000 0.001000 0.001000 0.001002 0.001001 0.001000 0.624018 0.001001 0.001000 0.001021 0.001001 0.001000
0.001006 0.001004 0.001003 0.001002 0.001000 0.532372 0.001000 0.001005 0.001000 0.001001 0.001000 0.468607 0.001000
0.001000 0.001001 0.001001 0.460642 0.001001 0.001000 0.450462 0.001000 0.001000 0.001000 0.001002 0.001000 0.090891
0.001000 0.001000 0.580419 0.001000 0.001001 0.001000 0.001000 0.001000 0.001000 0.420578 0.001001 0.001000 0.001000
0.001004 0.001003 0.001002 0.001003 0.001000 0.001000 0.001000 0.001002 0.999987 0.001000 0.001000 0.001000 0.001000
0.001001 0.001000 0.001001 0.001001 0.430575 0.001000 0.001001 0.001002 0.001000 0.001000 0.570418 0.001000 0.001002
0.001000 0.366299 0.420021 0.215676 0.001000 0.001001 0.001001 0.001000 0.001001 0.001000 0.001000 0.001001 0.001000
pi:
0.999995 0.001005 0.001000 0.001000 0.001000 0.001000 0.001000

标注
测试数据

the can can destroy the typical fly .

编号

T= 8
1 3 3 13 1 6 4 5

预测

1
./../testvit example.hmm example0.test

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
------------------------------------
Viterbi using direct probabilities
Viterbi MLE log prob = -1.223539E+01
Optimal state sequence:
T= 8
1 7 4 3 1 2 7 6
------------------------------------
Viterbi using log probabilities
Viterbi MLE log prob = -1.223539E+01
Optimal state sequence:
T= 8
1 7 4 3 1 2 7 6
------------------------------------
The two log probabilites and optimal state sequences
should identical (within numerical precision).

the/at can/wps can/vb destroy/md the/at typical/nn fly/wps ./jj

对于无标注:

数据编号

1
./../create_key.pl words.key < example0.sentences > example0.seq

单词

1 the
6 typical
3 can
8 a
9 who
13 destroy
7 see
2 plane
11 ?
10 might
5 .
12 large
4 fly

编号后的文档

1
2
T= 590
1 2 3 4 5 1 6 2 3 7 1 2 5 8 6 4 3 7 5 9 10 7 11 1 12 3 10 7 8 3 5 1 3 3 13 8 12 3 5 9 10 7 11 9 10 4 11 9 3 4 11 1 3 10 7 5 1 2 3 4 8 6 4 5 9 3 4 11 1 12 4 3 4 5 9 3 7 11 9 3 7 8 3 11 1 2 3 7 1 6 3 5 9 3 7 11 8 2 3 7 5 9 3 7 8 12 2 11 9 10 13 8 6 3 11 9 3 7 11 9 10 7 11 9 10 4 11 9 10 7 8 4 11 1 2 3 4 1 2 5 1 6 2 10 4 8 2 5 9 10 4 8 12 2 11 9 3 4 8 12 4 11 9 10 4 11 9 3 7 8 4 11 9 3 4 11 1 2 10 4 8 2 5 9 10 7 1 3 11 8 12 3 10 4 5 1 2 3 7 8 12 4 5 9 3 13 8 12 4 11 9 3 7 8 2 11 8 12 2 3 4 5 9 10 7 11 9 3 4 8 3 11 8 12 3 10 7 5 8 6 3 3 7 1 3 5 9 3 13 8 6 3 11 9 3 4 11 8 6 3 3 4 1 12 2 5 8 4 3 4 8 2 5 9 3 4 8 2 11 9 10 13 1 3 11 1 6 2 3 4 8 12 2 5 1 6 4 3 7 1 12 3 5 9 10 4 8 2 11 9 10 4 11 9 3 7 8 12 4 11 1 6 4 3 13 8 12 2 5 9 3 4 8 3 11 8 6 3 3 7 5 8 6 4 10 4 5 9 3 7 1 2 11 9 3 4 1 12 2 11 1 4 10 4 8 6 3 5 9 3 7 8 2 11 9 10 7 8 4 11 8 3 10 4 5 9 3 7 11 9 10 7 11 9 10 7 11 8 2 3 4 5 9 10 4 8 3 11 8 12 3 10 7 5 9 10 7 11 8 12 3 3 13 8 3 5 8 6 3 10 7 1 3 5 9 10 7 11 1 6 3 3 4 5 9 10 7 8 6 4 11 1 6 4 3 4 5 9 3 4 11 8 4 3 7 8 6 3 5 8 2 10 4 5 9 10 7 11 8 6 3 10 4 8 2 5 9 3 7 1 3 11 8 12 3 3 7 5 9 3 4 8 6 3 11 9 10 4 11 9 10 4 11 9 3 4 8 6 3 11 9 10 4 8 12 2 11 9 3 4 11 9 10 4 1 6 3 11 1 3 3 13 1 6 4 5 9 3 7 11 8 2 3 13 1 3 5 9 10 4 11 9 3 7 11 1 12 2 3 7 5 8 4 3 7 5 8 2 10 4 5 8 3 10 7 5 9 3 4 11

训练

1
./../esthmm -N 7 -M 13 example0.seq > example0.hmm

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
M= 13
N= 7
A:
0.001001 0.001041 0.001002 0.001001 0.001002 0.001001 0.999951
0.001001 0.001003 0.576307 0.001004 0.001001 0.001002 0.424681
0.001000 0.001000 0.001000 0.999991 0.001000 0.001008 0.001000
0.093382 0.002156 0.001001 0.003694 0.903712 0.001055 0.001001
0.001004 0.554405 0.001002 0.001018 0.001003 0.001001 0.446567
0.001256 0.354273 0.001008 0.307920 0.007258 0.333280 0.001005
0.001000 0.001000 0.001004 0.001011 0.001225 0.999749 0.001010
B:
0.001000 0.001000 0.001002 0.021460 0.001457 0.001000 0.019199 0.001000 0.001000 0.001001 0.001343 0.001000 0.960537
0.001002 0.001002 0.001004 0.001024 0.430486 0.001000 0.001025 0.001003 0.001000 0.001001 0.570408 0.001000 0.001046
0.001006 0.001006 0.001008 0.001009 0.001000 0.001000 0.001000 0.001003 0.999967 0.001000 0.001000 0.001000 0.001000
0.001000 0.001005 0.580741 0.001008 0.001001 0.001002 0.001000 0.001001 0.001000 0.420237 0.001001 0.001004 0.001000
0.001000 0.001012 0.001017 0.508569 0.001153 0.001000 0.491349 0.001000 0.001000 0.001002 0.001117 0.001000 0.001782
0.001001 0.244775 0.280068 0.140141 0.001001 0.180252 0.001011 0.001001 0.001000 0.001008 0.001001 0.158740 0.001000
0.376952 0.001000 0.001001 0.001000 0.001001 0.001006 0.001000 0.624012 0.001003 0.001001 0.001017 0.001006 0.001000
pi:
0.001000 0.001000 0.001000 0.001000 0.001000 0.001001 0.999999

测试

1
he can can destroy the typical fly .

1
2
T= 8
1 3 3 13 1 6 4 5
1
./../testvit example0.hmm example0.test
1
2
3
4
5
6
7
8
9
10
11
12
13
14
Viterbi using direct probabilities
Viterbi MLE log prob = -1.401504E+01
Optimal state sequence:
T= 8
7 6 4 1 7 6 6 2
------------------------------------
Viterbi using log probabilities
Viterbi MLE log prob = -1.401504E+01
Optimal state sequence:
T= 8
7 6 4 1 7 6 6 2
------------------------------------
The two log probabilites and optimal state sequences
should identical (within numerical precision).

深入理解kafka学习笔记

kafka起初由LinkedIn公司采用Scala语言开发的一个多分区、多副本基于zookeeper协调的分布式消息系统,它以高吞吐、可持久化、可水平扩展、支持流数据处理等多种特性而被广泛应用。
本文通过生产者、消费者、主题分区、日志存储等几个方面最kafka的基本用法和原理进行阐述,旨在以后方面回顾。文中大部分内容提取自《深入理解Kakfa 核心设计与实践原理》。

1. 初识kafka

1.1 基本概念

kakfa体系结构

  • 生产者
  • 消费者
  • Broker,可以简单看做是一个独立的kafka服务节点或者服务实例。

  • 主题,kafka中的消息以主题为单位进行归类,生产者负责将消息发送到特定的主题,消费者订阅主题并进行消费

  • 分区,一个分区只属于单个主题,每个主题可以包含多个分区,分区可以分布在不同的broker上,同一主题下不同分区包含的消息是不同的

kakfa消息追加写入

分区在存储层面可以看做是一个可追加的日志本间。每条消息被发送到broker之前,会根据分区规则选择存储到哪个具体的分区。分区规则如果设置的合理,那么消息会被均匀的分配到不同的分区中。

kafka为分区引入了多副本(Replica)机制,通过增加副本的数量可以提升容灾能力。副本之间是一主多从的关系,leader副本负责处理读写请求,follower副本负责和leader副本消息同步。副本处于不同的broker中,
当leader副本出现故障时,从follower副本中重新选举新的leader副本对外提供服务。kafka通过多副本机制实现了故障的自动转移,当kafka集群中某个broker失效时仍能保证服务可用。

kakfa多副本架构

分区中所有的副本统称AR(Assigned Replicas), 所有与leader副本保持一定程度同步的副本(包括leader副本在内)组成ISR(In-Sync Replicas)。消息会先发送到leader副本,然后follower副本从leader副本中拉取消息进行同步。

kakfa HW LEO

  • LEO: 高水位,副本最大位移
  • HW: 低水位,ISR列表中最小的LEO,是消费者可消费的最大位移

2. 生产者

最基本的生产者代码

1
2
3
4
5
6
7
8
9
10
11
Properties props = new Properties();
props.put("bootstrap.servers", "localhost:9092");
props.put("acks", "all");
props.put("key.serializer", "org.apache.kafka.common.serialization.StringSerializer");
props.put("value.serializer", "org.apache.kafka.common.serialization.StringSerializer");

Producer<String, String> producer = new KafkaProducer<>(props);
for (int i = 0; i < 100; i++)
producer.send(new ProducerRecord<String, String>("my-topic", Integer.toString(i), Integer.toString(i)));

producer.close();
  • bootstrap.servers: 设置集群broker地址清单,具体格式:host1:port1, host2:port2 可以设置一个或者多个,不需要设置所有的broker地址,生产者可以从给定的broker里面查到其他的broker信息,不过建议最少设置两个以上,其中一个宕机时,生产者还可以连到集群上
  • key.serializer、value.serializer用来序列化ProducerRecord
  • asks: 这个参数用来指定分区中必须要有多少副本收到这条消息,之后生产者才会认为这条消息是成功写入的, asks的设置会影响吞吐量
    • asks == 1. 即只要leader副本成功写入消息,那么生产端就会收到服务端的成功响应
    • asks == 0. 生产者不需要等待任何服务服务端的响应
    • asks == -1 or asks == all. 需要等待ISR列表中的所有副本都成功写入,服务器端才能响应成功

发送数据三种模式,发后即忘,同步和异步

同步:

1
2
3
4
5
6
try{
Future<RecordMetadata> future = producer.send(record);
RecordMetadata metadata = future.get()
} catch(...) {
...
}

异步

1
2
3
4
5
6
producer.send(record, new Callback(){
@Override
public void onCompletion(RecordMetadata metadata, Exception exception){
...
}
});

kakfa 生产端的整体架构

消息缓存到batch,分批次发送,减少网络传输的资源消耗
send时,生产者并不会立即发送记录到broker,会先对记录做缓存。从图中可以看出,send会先发消息放到一个batch中,当一个batch满了之后,再申请一个新的batch填充,
kafka消费端维护了一个batch池,以便batch可以循环利用。此外消费者客户端会有一个send线程,实时去队列中获取已经满了的batch,发送到broker

3. 消费者

kafka中的消费是基于拉取的,消费者会重复的调用poll方法拉取数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

Properties props = new Properties();
props.put("bootstrap.servers", "localhost:9092");
props.put("group.id", "test");
props.put("enable.auto.commit", "true");
props.put("auto.commit.interval.ms", "1000");
props.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
props.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props);
consumer.subscribe(Arrays.asList("foo", "bar"));
while (true) {
ConsumerRecords<String, String> records = consumer.poll(100);
for (ConsumerRecord<String, String> record : records)
System.out.printf("offset = %d, key = %s, value = %s%n", record.offset(), record.key(), record.value());
}
  • group.id: 消费者隶属的消费组名称, 消费组可以包含多个消费者,每个分片只能被一个消费组中的一个消费者消费

kakfa consumer group

  • enable.auto.commit: 消费者消费完数据,需要告诉服务端消费的offset,以便下次poll数据时,服务端知道从哪里开始下发数据, 这个字段告诉消费者自动提交offset,
    消费者后台线程会定期自动提交offset,周期可以通过auto.commit.interval.ms设置,默认5s

kakfa consumer offset

但是自动提交会产生漏数据或者重复消费的问题。加入消费者一次poll到10条数据,在消费到第五条的时候,后台线程提交了offset,提交后消费者线程挂掉,
此时服务端会认为消费者已经将这10条数据消费,消费者重启,再poll数据时,会直接拉取10条后的数据。
不过因为kafka数据存储实在磁盘上的,会周期性的持久化,如果知道漏数据的offset,可以通过offset重新拉取数据
如果消费者在消费完数据后,提交位移前挂掉,会发生重复消费的问题。如果消费者业务是幂等的,重复消费不会产生太多问题,否则不太乐观
kafka同时提供了手动提交位移的api

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//一次poll消费完统一提交位移
Properties props = new Properties();
props.put("bootstrap.servers", "localhost:9092");
props.put("group.id", "test");
props.put("enable.auto.commit", "false");
props.put("key.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
props.put("value.deserializer", "org.apache.kafka.common.serialization.StringDeserializer");
KafkaConsumer<String, String> consumer = new KafkaConsumer<>(props);
consumer.subscribe(Arrays.asList("foo", "bar"));
while (true) {
ConsumerRecords<String, String> records = consumer.poll(100);
insertIntoDb(records);
consumer.commitSync();
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//每消费完一个partition记录就提交
try {
while(running) {
ConsumerRecords<String, String> records = consumer.poll(Long.MAX_VALUE);
for (TopicPartition partition : records.partitions()) {
List<ConsumerRecord<String, String>> partitionRecords = records.records(partition);
for (ConsumerRecord<String, String> record : partitionRecords) {
System.out.println(record.offset() + ": " + record.value());
}
long lastOffset = partitionRecords.get(partitionRecords.size() - 1).offset();
consumer.commitSync(Collections.singletonMap(partition, new OffsetAndMetadata(lastOffset + 1)));
}
}
} finally {
consumer.close();
}

4. 主题分区

5. 日志存储

kafka以log日志的形式将数据存储到磁盘上,每个partition可以看做是一个整体的log文件,但为了不使单个log文件太大,kafka引入了日志分段(LogSegment),将log日志切分成多个LogSegment,
相当于被平分为多个较小的文件

kakfa log partition logsegment

kakfa 日志结构

index 中存储了索引以及物理偏移量。 log 存储了消息的内容。索引文件的元数据执行对应数据文件中message 的物理偏移地址。举个简单的案例来说,以
[4053,80899]为例,在 log 文件中,对应的是第 4053 条记录,物理偏移量(position)为 80899. position 是ByteBuffer 的指针位置

kakfa 日志结构

查找算法

  • 根据offset的值,查找segment段中的index 索引文件。由于索引文件命名是以上一个文件的最后一个offset 进行命名的,所以,使用二分查找算法能够根据offset 快速定位到指定的索引文件。
  • 找到索引文件后,根据 offset 进行定位,找到索引文件中的符合范围的索引。(跳表)
  • 得到 position 以后,再到对应的 log 文件中,从 position出开始查找 offset 对应的消息,将每条消息的 offset 与目标 offset 进行比较,直到找到消息

顺序写,页缓存和零拷贝

  • 顺序写: 磁盘的顺序写速度要远大于随机写,kafka在设计时采用了文件追加的方式写入消息,即只能在日志文件的尾部追加新的消息并且也不允许修改已经写入的消息,这种方式属于典型的顺序写盘的操作
  • 页缓存:
    • kafka采用mmap直接操作页缓存pagecache的方式,最终数据的读写基本都是操作内存,脏页数据由操作系统统一刷到磁盘,
    • kafka提供了参数可以强制刷盘。页缓存存在的风险是,如果机器断电,页缓存中脏页数据没有刷到磁盘,会导致丢数据。但这个问题仅在机器断电时发生,多副本机制可以保障数据的可靠性。
    • Linux会使用磁盘的一部分作为swap分区,这样可以进行进程的调度,把当前非活跃的进程调入swap分区,以此把内存空出来让给活跃的进程。但是对kafka来说,如果大量的页缓存被置换到swap区,会极大的降低kafka的性能,但是如果不设置swap分区,当内存不够用时,会导致OOM的发生。建议swap设置的小一些。
    • 使用页缓存使同时可以避免在JVM内部缓存数据,降低内存消耗
  • 零拷贝: 在从磁盘读数据发送给消费者时,传统的做法被将数据从磁盘拷贝的内核空间,再由内核拷贝到用户空间,然后由用户控件拷贝到内核态的socket buffer中,最后内核态的buffer中拷贝到网卡,这中间需要四次复制过程,零拷贝技术将拷贝次数降低到两次,
    • 将文件拷贝到kernel buffer中;
    • 向socket buffer中追加当前要发生的数据在kernel buffer中的位置和偏移量;
    • 根据socket buffer中的位置和偏移量直接将kernel buffer的数据copy到网卡设备(protocol engine)中;

kakfa 传统拷贝
kakfa 零拷贝

并发 -《Go In Action》-Ch6

每个goroutine是一个独立的工作单元,这个单元会被调度到可用的逻辑处理器上执行。Go运行时通过调度器管理goroutine,为其分配执行时间。
调度器在操作系统之上,将操作系统的线程和语言运行时的逻辑处理器绑定,并在逻辑处理器上运行goroutine。
Go语言通过在goroutine之间传递数据来通信,而不是对数据加锁来实现同步访问。

6.1 并行和并发

Go调度器如何管理goroutine

并发是让不同的代码片段同时在不同的物理处理器上执行,并发是指同时管理很多事情。
每当创建一个goroutine并准备运行,goroutine被分配到调度器的全局队列中,调度器会给goroutine分配一个逻辑处理器,将goroutine放到逻辑处理器对应的本地队列中。

并发和并行的区别

6.2 goroutine

下面这个程序展示了逻辑处理器是如何调度goroutine的,runtime.GOMAXPROCS(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
71
72
package main

import (
"fmt"
"runtime"
"sync"
)

// wg is used to wait for the program to finish.
var wg sync.WaitGroup

// main is the entry point for all Go programs.
func main() {
// Allocate 1 logical processors for the scheduler to use.
runtime.GOMAXPROCS(1)

// Add a count of two, one for each goroutine.
wg.Add(2)

// Create two goroutines.
fmt.Println("Create Goroutines")
go printPrime("A")
go printPrime("B")

// Wait for the goroutines to finish.
fmt.Println("Waiting To Finish")
wg.Wait()

fmt.Println("Terminating Program")
}

// printPrime displays prime numbers for the first 5000 numbers.
func printPrime(prefix string) {
// Schedule the call to Done to tell main we are done.
defer wg.Done()

next:
for outer := 2; outer < 5000; outer++ {
for inner := 2; inner < outer; inner++ {
if outer%inner == 0 {
continue next
}
}
fmt.Printf("%s:%d\n", prefix, outer)
}
fmt.Println("Completed", prefix)
}

// output
// Create Goroutines
// Waiting To Finish
// B:2
// B:3
// ...
// B:4583
// B:4591
// A:3 ** 切换 goroutine
// A:5
// ...
// A:4561
// A:4567
// B:4603 ** 切换 goroutine
// B:4621
// ...
// Completed B
// A:4457 ** 切换 goroutine
// A:4463
// ...
// A:4993
// A:4999
// Completed A
// Terminating Program

可以看到goroutine A和B是交替运行的,因为只有一个逻辑处理器。调度过程可以用下图表示:

逻辑处理器调度goroutine

6.3 竞争状态

多个goroutine同时对一个共享资源进行读和写,容易进入相互竞争的状态

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"
"runtime"
"sync"
)

var (
// counter is a variable incremented by all goroutines.
counter int

// wg is used to wait for the program to finish.
wg sync.WaitGroup
)

// main is the entry point for all Go programs.
func main() {
// Add a count of two, one for each goroutine.
wg.Add(2)

// Create two goroutines.
go incCounter(1)
go incCounter(2)

// Wait for the goroutines to finish.
wg.Wait()
fmt.Println("Final Counter:", counter)
}

// incCounter increments the package level counter variable.
func incCounter(id int) {
// Schedule the call to Done to tell main we are done.
defer wg.Done()

for count := 0; count < 2; count++ {
// Capture the value of Counter.
value := counter

// Yield the thread and be placed back in queue.
runtime.Gosched()

// Increment our local value of Counter.
value++

// Store the value back into Counter.
counter = value
}
}

最后counter的值有可能是2,可以用下面这个图描述下过程

竞争状态下程序行为的图像表达

可以用go build -race检测代码里的竞争状态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
go build -race // 用竞争检测器标志来编译程序
./example // 运行程序
==================
WARNING: DATA RACE
Write by goroutine 5:
main.incCounter()
/example/main.go:49 +0x96
Previous read by goroutine 6:
main.incCounter()
/example/main.go:40 +0x66
Goroutine 5 (running) created at:
main.main()
/example/main.go:25 +0x5c
Goroutine 6 (running) created at:
main.main()
/example/main.go:26 +0x73
==================
Final Counter: 2
Found 1 data race(s)

6.4 锁住共享资源

可以使用原子函数和互斥锁解决共享资源的问题

6.4.1 原子函数

原子函数能够以很底层的加锁机制来同步访问整形变量和指针,atomic包提供了一些原子操作,如AddInt64,这个函数会同步整型类型的的加法
LoadInt64和StoreInt64,这两个函数提供了一种安全的读写一个整型值的方式。

1
2
3
4
var count int64
atomic.AddInt64(&counter, 1)
atomic.LoadInt64(&cunter)
atomic.StoreInt64(&count, 1)

6.4.2 互斥锁

互斥锁用于在代码上创建一个临界区,保证同一时间只有一个goroutine 可以执行这个临界区代码

1
2
3
4
mutex sync.Mutex
mutex.Lock()
...
mutex.Unlock()

6.5 通道

可以使用make来创建通道

1
2
3
4
5
6
7
8
9
// 无缓冲的整型通道
unbuffered := make(chan int)
// 有缓冲的字符串通道
buffered := make(chan string, 10)

// 向通道发送值
buffered <- "Gopher"
// 从通道里接收值
value := <-buffered

无缓冲的通道(unbuffered channel)是指在接收前没有能力保存任何值的通道。这种类型的通
道要求发送goroutine 和接收goroutine 同时准备好,才能完成发送和接收操作。如果两个goroutine
没有同时准备好,通道会导致先执行发送或接收操作的goroutine 阻塞等待。这种对通道进行发送
和接收的交互行为本身就是同步的。其中任意一个操作都无法离开另一个操作单独存在。

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
// This sample program demonstrates how to use an unbuffered
// channel to simulate a relay race between four goroutines.
package main

import (
"fmt"
"sync"
"time"
)

// wg is used to wait for the program to finish.
var wg sync.WaitGroup

// main is the entry point for all Go programs.
func main() {
// Create an unbuffered channel.
baton := make(chan int)

// Add a count of one for the last runner.
wg.Add(1)

// First runner to his mark.
go Runner(baton)

// Start the race.
baton <- 1

// Wait for the race to finish.
wg.Wait()
}

// Runner simulates a person running in the relay race.
func Runner(baton chan int) {
var newRunner int

// Wait to receive the baton.
runner := <-baton

// Start running around the track.
fmt.Printf("Runner %d Running With Baton\n", runner)

// New runner to the line.
if runner != 4 {
newRunner = runner + 1
fmt.Printf("Runner %d To The Line\n", newRunner)
go Runner(baton)
}

// Running around the track.
time.Sleep(100 * time.Millisecond)

// Is the race over.
if runner == 4 {
fmt.Printf("Runner %d Finished, Race Over\n", runner)
wg.Done()
return
}

// Exchange the baton for the next runner.
fmt.Printf("Runner %d Exchange With Runner %d\n",
runner,
newRunner)

baton <- newRunner
}

有缓冲的通道(buffered channel)是一种在被接收前能存储一个或者多个值的通道。这种类
型的通道并不强制要求goroutine 之间必须同时完成发送和接收。通道会阻塞发送和接收动作的
条件也会不同。只有在通道中没有要接收的值时,接收动作才会阻塞。只有在通道没有可用缓冲
区容纳被发送的值时,发送动作才会阻塞。这导致有缓冲的通道和无缓冲的通道之间的一个很大
的不同:无缓冲的通道保证进行发送和接收的goroutine 会在同一时间进行数据交换;有缓冲的
通道没有这种保证。

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
// This sample program demonstrates how to use a buffered
// channel to work on multiple tasks with a predefined number
// of goroutines.
package main

import (
"fmt"
"math/rand"
"sync"
"time"
)

const (
numberGoroutines = 4 // Number of goroutines to use.
taskLoad = 10 // Amount of work to process.
)

// wg is used to wait for the program to finish.
var wg sync.WaitGroup

// init is called to initialize the package by the
// Go runtime prior to any other code being executed.
func init() {
// Seed the random number generator.
rand.Seed(time.Now().Unix())
}

// main is the entry point for all Go programs.
func main() {
// Create a buffered channel to manage the task load.
tasks := make(chan string, taskLoad)

// Launch goroutines to handle the work.
wg.Add(numberGoroutines)
for gr := 1; gr <= numberGoroutines; gr++ {
go worker(tasks, gr)
}

// Add a bunch of work to get done.
for post := 1; post <= taskLoad; post++ {
tasks <- fmt.Sprintf("Task : %d", post)
}

// Close the channel so the goroutines will quit
// when all the work is done.
close(tasks)

// Wait for all the work to get done.
wg.Wait()
}

// worker is launched as a goroutine to process work from
// the buffered channel.
func worker(tasks chan string, worker int) {
// Report that we just returned.
defer wg.Done()

for {
// Wait for work to be assigned.
task, ok := <-tasks
if !ok {
// This means the channel is empty and closed.
fmt.Printf("Worker: %d : Shutting Down\n", worker)
return
}

// Display we are starting the work.
fmt.Printf("Worker: %d : Started %s\n", worker, task)

// Randomly wait to simulate work time.
sleep := rand.Int63n(100)
time.Sleep(time.Duration(sleep) * time.Millisecond)

// Display we finished the work.
fmt.Printf("Worker: %d : Completed %s\n", worker, task)
}
}

上面代码需要注意的是close(tasks),关闭通道后,goroutine依旧可以从通道接收数据,但是不能再向通道里发送数据。

6.6 小结

并发是指goroutine运行的时候是相互独立的
使用关键字go创建goroutine来运行函数
goroutine在逻辑处理器上执行,逻辑处理器具有独立的系统线程和运行队列
竞争状态是指两个或者多个goroutine试图访问同一个资源
原子函数和互斥锁提供了一种防止出现竞争状态的办法
通道提供了一种在两个goroutine之间共享数据的简单方法
无缓冲的通道保证同时交换数据,而有缓冲的通道不做这种保证

Go语言的类型系统 -《Go In Action》-Ch5

Go语言是一种静态类型的编程语言,编译器需要在编译时知道每个值的类型

5.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
// 第一种声明方式
type user struct {
name string
email string
}

// 使用结构类型声明变量,并初始化为其零值
// 声明user 类型的变量
var bill user

// 使用结构字面量来声明一个结构类型的变量
// 声明user 类型的变量,并初始化所有字段
lisa := user{
name: "Lisa",
email: "lisa@email.com",
ext: 123,
privileged: true,
}

// 不使用字段名,创建结构类型的值
// 声明user 类型的变量
lisa := user{"Lisa", "lisa@email.com", 123, true}

// 使用其他结构类型声明字段
// admin 需要一个user 类型作为管理者,并附加权限
type admin struct {
person user
level string
}

// 使用结构字面量来创建字段的值
// 声明admin 类型的变量
fred := admin{
person: user{
name: "Lisa",
email: "lisa@email.com",
ext: 123,
privileged: true,
},
level: "super",
}

// 第二种声明方式
// 基于int64 声明一个新类型
// int64 和 Duration 是两种不同的类型,int64是Duration的基础类型,试图对ini64和Duration相互赋值将产生编译错误
package main
type Duration int64

func main() {
var dur Duration
dur = int64(1000)
}

// prog.go:7: cannot use int64(1000) (type int64) as type Duration
// in assignment

5.2 方法

方法能给用户定义的类型添加新的行为,方法实际上也是函数,只是在声明时,在关键字func和方法名之间的增加了一个参数,
这个参数被称为接收者,将函数和接收者的类型绑在一起。

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
// 这个示例程序展示如何声明
// 并使用方法
package main

import (
"fmt"
)

// user 在程序里定义一个用户类型
type user struct {
name string
email string
}

// notify 使用值接收者实现了一个方法
func (u user) notify() {
fmt.Printf("Sending User Email To %s<%s>\n",
u.name,
u.email)
}

// changeEmail 使用指针接收者实现了一个方法
func (u *user) changeEmail(email string) {
u.email = email
}

// main 是应用程序的入口
func main() {
// user 类型的值可以用来调用
// 使用值接收者声明的方法
bill := user{"Bill", "bill@email.com"}
bill.notify()

// 指向user 类型值的指针也可以用来调用
// 使用值接收者声明的方法
lisa := &user{"Lisa", "lisa@email.com"}
lisa.notify()

// user 类型的值可以用来调用
// 使用指针接收者声明的方法
bill.changeEmail("bill@newdomain.com")
bill.notify()

notify接受者是user值的一个副本,notify也可以使用指针调用,Go会在背后执行一个转换操作

1
2
3
// Go在代码背后的执行动作
lisa := &user{"Lisa", "lisa@email.com"}
*(lisa).notify()

可以不到不管是使用值调用,还是使用指针调用,notify函数的接收者都是一个user的副本,对副本的修改并不会影响原来的值
changeEmail恰恰相反,他的接受者是指针,这种情况函数对值进行的修改,会影响到原来的变量值,绑定指针类型的函数,也可以接受值的调用
Go会在背后做如下优化

1
(&bill).changeEmail("bill@newdomain.com")

5.3 类型的本质

一个类型在以参数在函数间传递或者作为接受者绑定方法时,需要根据类型的特点以及使用的方法,去决定是传指针还是传值

5.3.1 内置类型

原始的,内置类型是语言提供的一组类型,诸如数值类型、字符串类型和布尔类型,对于这种类型的传递一般是传值,因为对这些值进行增加或者删除的时候,会创建一个新的值

5.3.2 引用类型

非原始的,引用类型诸如切片、映射、通道、接口和函数类型,每个引用类型包含一组独特的字段,用于管理底层数据。不需要共享一个引用类型的值,可以通过赋值来传递一个引用类型的值的副本,本质上这就是在共享底层数据结构

5.3.3 结构类型

结构类型有可能是原始的,也有可能是非原始的,需要遵守上面内置类型和引用类型的规范。是使用值接受者还是使用指针接受者,不应该由该方法是否修改了接受到的值来决定,应该基于该类型的本质。

5.4 接口

多态是指代码可以根据类型的具体实现采取不同行为的能力,如果一个类型实现了某个接口,所有使用这个接口的地方,都可以支持这种类型的值。

5.4.1 标准库

下面这个程序实现了类似于curl的基本功能,io.Copy的第一个参数是复制到的目标,这个参数是必须实现了io.Writer接口的值,os.Stdout实现了io.Writer。
io.Copy的第二个参数接收一个io.Reader接口类型的值,表示数据流入的源,http.Response.Body实现了io.Reader接口

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

import (
"fmt"
"io"
"net/http"
"os"
)

// init is called before main.
func init() {
if len(os.Args) != 2 {
fmt.Println("Usage: ./example2 <url>")
os.Exit(-1)
}
}

// main is the entry point for the application.
func main() {
// Get a response from the web server.
r, err := http.Get(os.Args[1])
if err != nil {
fmt.Println(err)
return
}

// Copies from the Body to Stdout.
io.Copy(os.Stdout, r.Body)
if err := r.Body.Close(); err != nil {
fmt.Println(err)
}
}

5.4.2 实现

接口是用来定义行为的类型。行为通过方法由用户定义的类型实现。用户定义的类型实现了某个接口类型声明的一组方法,那么这个用户定义的类型的值就可以赋给这个接口类型的值。这个赋值会把用户定义的类型的值存入接口类型的值。

对接口值方法的调用会执行接口值里存储的用户定义的类型的值的方法。将自定义类型赋值给接口分两种情况,自定义类型的值赋值给接口值和自定义类型指针赋值给接口值。下面两幅图展示了分别赋值给接口值后接口值的内存布局

值赋值后接口值
指针赋值后接口值

接口值是两个字长度的数据结构,第一个字包含一个指向内部表的指针。内部表叫做iTable,包含了所存储的值的类型信息。iTable包含了已存储的值的类型信息和与这个值相关联的的一组方法。
第二个字是一个指向所存储值的指针。

5.4.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
36
37
38
39
40
41
42
43
package main

import (
"fmt"
)

// notifier is an interface that defined notification
// type behavior.
type notifier interface {
notify()
}

// user defines a user in the program.
type user struct {
name string
email string
}

// notify implements a method with a pointer receiver.
func (u *user) notify() {
fmt.Printf("Sending user email to %s<%s>\n",
u.name,
u.email)
}

// main is the entry point for the application.
func main() {
// Create a value of type User and send a notification.
u := user{"Bill", "bill@email.com"}

sendNotification(u)

// ./listing36.go:32: cannot use u (type user) as type
// notifier in argument to sendNotification:
// user does not implement notifier
// (notify method has pointer receiver)
}

// sendNotification accepts values that implement the notifier
// interface and sends notifications.
func sendNotification(n notifier) {
n.notify()
}

上面的程序会编译失败,错误的原因是user类型的值并没有实现notify接口。这里涉及到了方法集的概念,方法集定义了一组关联到给定类型的值或指针的方法。
定义方法时使用的接收者的类型决定了这个方法时关联到值还是关联到指针,还是两个都关联。
Go语言规范里定义了方法集的规则。

1
2
3
4
5
6
7
8
9
10

Values Methods Receivers
-----------------------------------------------
T (t T)
*T (t T) and (t *T)

Methods Receivers Values
-----------------------------------------------
(t T) T and *T
(t *T) *T

也就是说T类型的方法集只包含了值接收者声明的方法。而*T的方法集即包含了值接收者声明的方法,也包含指针接受者声明的方法。
那么上面错误代码的解决方式就有两种,一种是sendNotification(&n),因为&n即包含了值接收者方法,也包含了指针接收者方法。另一种是将notify修改为值接收方法。

5.4.4 多态

在了解了方法集的基础上,这里给了一个展示接口的多态行为的例子

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

import (
"fmt"
)

// notifier is an interface that defines notification
// type behavior.
type notifier interface {
notify()
}

// user defines a user in the program.
type user struct {
name string
email string
}

// notify implements the notifier interface with a pointer receiver.
func (u *user) notify() {
fmt.Printf("Sending user email to %s<%s>\n",
u.name,
u.email)
}

// admin defines a admin in the program.
type admin struct {
name string
email string
}

// notify implements the notifier interface with a pointer receiver.
func (a *admin) notify() {
fmt.Printf("Sending admin email to %s<%s>\n",
a.name,
a.email)
}

// main is the entry point for the application.
func main() {
// Create a user value and pass it to sendNotification.
bill := user{"Bill", "bill@email.com"}
sendNotification(&bill)

// Create an admin value and pass it to sendNotification.
lisa := admin{"Lisa", "lisa@email.com"}
sendNotification(&lisa)
}

// sendNotification accepts values that implement the notifier
// interface and sends notifications.
func sendNotification(n notifier) {
n.notify()
}

可以看到user和admin都实现了notify接口,对同一个行为做出了不同的表示。

5.6 嵌入类型

将已有的类型直接声明在新的结构类型里被称为嵌入类型。通过嵌入类型,与内部类型相关的标识会提升到外部类型上。这些被提升的标识符和直接声明在外部类型里一样。也是外部类型的一部分。

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

import (
"fmt"
)

// user defines a user in the program.
type user struct {
name string
email string
}

// notify implements a method that can be called via
// a value of type user.
func (u *user) notify() {
fmt.Printf("Sending user email to %s<%s>\n",
u.name,
u.email)
}

// admin represents an admin user with privileges.
type admin struct {
user // Embedded Type
level string
}

// main is the entry point for the application.
func main() {
// Create an admin user.
ad := admin{
user: user{
name: "john smith",
email: "john@yahoo.com",
},
level: "super",
}

// We can access the inner type's method directly.
ad.user.notify()

// The inner type's method is promoted.
ad.notify()
}

可以看到user被嵌入到admin中,user的notify方法也被提升到了admin类型上,可以直接调用。如果admin自己也实现了notify接口,这时候user的notify方法不会被提升。

5.6 公开和未公开的标识符

当一个标识符的名字以小写字母开头时,这个标识符就是未公开的,即包外的代码不可见。大写字母开头表示是公开的,对包外的代码可见。

5.7 小结

使用关键字struct或者通过指定已存在的类型,可以声明用户定义的类型
方法提供了一种给用户定义的类型增加行为的方式
设计类型时需要确认类型的本质是原始的还是非原始的
接口是声明了一组行为并支持多态的类型
嵌入类型提供了扩展类型的能力,而无需使用继承
标识符要么是从包里公开的,要么是在包里未公开的

数组、切片和映射-《Go In Action》-Ch4

4.1 数组的内部实现和基础功能

4.1.1 内部实现

长度固定、内存连续分配、CPU数据缓存更久、容易计算索引,迭代速度快

4.2.1 声明和初始化

声明需要类型和长度,长度一旦确定就不能改变

1
var array [5] int

声明变量时,会使用对应类型的零值对变量进行初始化
可以使用字面变量声明数组

1
array := [5]int{10, 20, 30, 40, 50}

也可以使用…替代数组长度,Go会根据初始化时数组元素的数量来确定数组的长度

1
array := [...]int{10, 20, 30, 40, 50}

还可以给指定位置赋值确定值

1
array := [5]int{1: 10, 2: 20}

4.1.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
36
37
38
39
array := [5]int{10, 20, 30, 40, 50}
// 修改索引为2 的元素的值
array[2] = 35

// 声明包含5 个元素的指向整数的数组
// 用整型指针初始化索引为0 和1 的数组元素
array := [5]*int{0: new(int), 1: new(int)}
// 为索引为0 和1 的元素赋值
*array[0] = 10
*array[1] = 20
// 声明第一个包含5 个元素的字符串数组
var array1 [5]string
// 声明第二个包含5 个元素的字符串数组
// 用颜色初始化数组
array2 := [5]string{"Red", "Blue", "Green", "Yellow", "Pink"}
// 把array2 的值复制到array1
array1 = array2

// 声明第一个包含4 个元素的字符串数组
var array1 [4]string
// 声明第二个包含5 个元素的字符串数组
// 使用颜色初始化数组
array2 := [5]string{"Red", "Blue", "Green", "Yellow", "Pink"}
// 将array2 复制给array1
array1 = array2
// Compiler Error:
// cannot use array2 (type [5]string) as type [4]string in assignment

// 声明第一个包含3 个元素的指向字符串的指针数组
var array1 [3]*string
// 声明第二个包含3 个元素的指向字符串的指针数组
// 使用字符串指针初始化这个数组
array2 := [3]*string{new(string), new(string), new(string)}
// 使用颜色为每个元素赋值
*array2[0] = "Red"
*array2[1] = "Blue"
*array2[2] = "Green"
// 将array2 复制给array1
array1 = array2

4.1.4 多维数组

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
// 声明一个二维整型数组,两个维度分别存储4 个元素和2 个元素
var array [4][2]int
// 使用数组字面量来声明并初始化一个二维整型数组
array := [4][2]int{{10, 11}, {20, 21}, {30, 31}, {40, 41}}
// 声明并初始化外层数组中索引为1 个和3 的元素
array := [4][2]int{1: {20, 21}, 3: {40, 41}}
// 声明并初始化外层数组和内层数组的单个元素
array := [4][2]int{1: {0: 20}, 3: {1: 41}}

// 声明一个2×2 的二维整型数组
var array [2][2]int
// 设置每个元素的整型值
array[0][0] = 10
array[0][1] = 20
array[1][0] = 30
array[1][1] = 40

// 声明两个不同的二维整型数组
var array1 [2][2]int
var array2 [2][2]int
// 为每个元素赋值
array2[0][0] = 10
array2[0][1] = 20
array2[1][0] = 30
array2[1][1] = 40
// 将array2 的值复制给array1
array1 = array2

// 将 array1 的索引为1 的维度复制到一个同类型的新数组里
var array3 [2]int = array1[1]
// 将外层数组的索引为1、内层数组的索引为0 的整型值复制到新的整型变量里
var value int = array1[1][0]

4.1.5 在函数间传递数组

内存和性能上,传递数组是个很大的开销,因为总是值传递,需要拷贝,可以使用指针在函数间传递大数组,但是传递指针,函数会有改变指针指向的值的权限

1
2
3
4
5
6
7
var array [1e6]int
// 将数组的地址传递给函数foo
foo(&array)
// 函数foo 接受一个指向100 万个整型值的数组的指针
func foo(array *[1e6]int) {
...
}

4.2 切片的内部实现和基础功能

切片类似于动态数组,可以按需自动增长和缩小,通过内置append函数,可以高效增长切片,切片在内存中连续分配,可以索引、迭代

4.2.1 内部实现

三个要素:指向底层数组的指针、切片访问元素的个数(即长度)和切片允许增长到的元素个数(即容量)

4.2.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
// 创建一个字符串切片
// 其长度和容量都是5 个元素
slice := make([]string, 5)
// 创建一个整型切片
// 分别指定长度和容量时,创建的切片,底层数组的长度是指定的容量,但是初始化后并不能访问所有的数组元素 // 这里不能访问最后两个元素
slice := make([]int, 3, 5)

// 容量小于长度的切片会在编译时报错
// 创建一个整型切片
// 使其长度大于容量
slice := make([]int, 5, 3)
// Compiler Error:
// len larger than cap in make([]int)

// 通过切片字面量来声明切片
// 创建字符串切片
// 其长度和容量都是5 个元素
slice := []string{"Red", "Blue", "Green", "Yellow", "Pink"}
// 创建一个整型切片
// 其长度和容量都是3 个元素
slice := []int{10, 20, 30}

// 使用索引声明切片
// 创建字符串切片
// 使用空字符串初始化第100 个元素
slice := []string{99: ""}

// 创建nil 整型切片
// 数组指针为nil,长度和容量都是0
var slice []int

// 声明空切片
// 数组包含0个元素,长度和容量都是0
// 使用make 创建空的整型切片
slice := make([]int, 0)
// 使用切片字面量创建空的整型切片
slice := []int{}

4.2.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
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
96
97
98
99
// 创建一个整型切片
// 其容量和长度都是5 个元素
slice := []int{10, 20, 30, 40, 50}
// 改变索引为1 的元素的值
slice[1] = 25

// 创建一个整型切片
// 其长度和容量都是5 个元素
slice := []int{10, 20, 30, 40, 50}
// 创建一个新切片
// 其长度为2 个元素,容量为4 个元素
newSlice := slice[1:3]

// 修改切片内容可能导致的结果
// 创建一个整型切片
// 其长度和容量都是5 个元素
slice := []int{10, 20, 30, 40, 50}
// 创建一个新切片
// 其长度是2 个元素,容量是4 个元素
newSlice := slice[1:3]
// 修改newSlice 索引为1 的元素
// 同时也修改了原来的slice 的索引为2 的元素
newSlice[1] = 35

// 表示索引越界的语言运行时错误
// 创建一个整型切片
// 其长度和容量都是5 个元素
slice := []int{10, 20, 30, 40, 50}
// 创建一个新切片
// 其长度为2 个元素,容量为4 个元素
newSlice := slice[1:3]
// 修改newSlice 索引为3 的元素
// 这个元素对于newSlice 来说并不存在
newSlice[3] = 45
// Runtime Exception:
// panic: runtime error: index out of range

// 切片增长
// 创建一个整型切片
// 其长度和容量都是5 个元素
slice := []int{10, 20, 30, 40, 50}
// 创建一个新切片
// 其长度为2 个元素,容量为4 个元素
newSlice := slice[1:3]
// 使用原有的容量来分配一个新元素
// 将新元素赋值为60
// 注意此时 slice变为:{10, 20, 30, 60, 50}
// append时如果容量有剩余,会在现有数组上增加元素,如果容量没有剩余,会创建一个新的数组,并想现有值复制到新的数组上
// 当切片容量小于1000时,每次扩展成倍增加,一旦元素超过1000,容量银子会设为1.25,也就是每次增加25%
newSlice = append(newSlice, 60)

// 创建切片时的3个索引
// 创建字符串切片
// 其长度和容量都是5 个元素
source := []string{"Apple", "Orange", "Plum", "Banana", "Grape"}
// 将第三个元素切片,并限制容量
// 其长度为1 个元素,容量为2 个元素
slice := source[2:3:4]
// 这比可用的容量大
slice := source[2:3:6]
// Runtime Error:
// panic: runtime error: slice bounds out of range

// 3个索引一旦长度和容量设置的不一样,新的切片和原始切片公用相同的底层数组,对新切片的append会影响到原始切片,很容发生莫名其妙的问题,
// 此时可将新切片的容量设置为和长度一样,再执行append的时候,就会创建新的底层数组,从而和原始切片脱离关系,可以放心修改
// 创建字符串切片
// 其长度和容量都是5 个元素
source := []string{"Apple", "Orange", "Plum", "Banana", "Grape"}
// 对第三个元素做切片,并限制容量
// 其长度和容量都是1 个元素
slice := source[2:3:3]
// 向slice 追加新字符串
slice = append(slice, "Kiwi")

// 将一个切片追加到另一个切片
// ...运算符,可以将一个切片的所有元素追加到另一个切片里
s1 := []int{1, 2}
s2 := []int{3, 4}
// 将两个切片追加在一起,并显示结果
fmt.Printf("%v\n", append(s1, s2...))
Output:
[1 2 3 4]

// 迭代切片for range
// 关键字range 会返回两个值。第一个值是当前迭代到的索引位置,第二个值是该位置对应元素值的一份副本
// 需要强调的是,range 创建了每个元素的副本,而不是直接返回对该元素的引用
// 创建一个整型切片
// 其长度和容量都是4 个元素
slice := []int{10, 20, 30, 40}
// 迭代每个元素,并显示值和地址
for index, value := range slice {
fmt.Printf("Value: %d Value-Addr: %X ElemAddr: %X\n",
value, &value, &slice[index])
}
// Output:
// Value: 10 Value-Addr: 10500168 ElemAddr: 1052E100
// Value: 20 Value-Addr: 10500168 ElemAddr: 1052E104
// Value: 30 Value-Addr: 10500168 ElemAddr: 1052E108
// Value: 40 Value-Addr: 10500168 ElemAddr: 1052E10C

4.2.4 多维切片

1
2
3
4
// 创建一个整型切片的切片
slice := [][]int{{10}, {100, 200}}
// 为第一个切片追加值为20 的元素
slice[0] = append(slice[0], 20)

4.2.5 在函数间传递切片

1
2
3
4
5
6
7
8
9
10
// 成本很低,在 64 位架构的机器上,一个切片需要24 字节的内存:指针字段需要8 字节,长度和容量
字段分别需要8 字节。
slice := make([]int, 1e6)
// 将slice 传递到函数foo
slice = foo(slice)
// 函数foo 接收一个整型切片,并返回这个切片
func foo(slice []int) []int {
...
return slice
}

4.3 映射的内部实现和基础功能

4.3.1 内部实现

桶 + 两个数组
key转换成散列值,散列低位表示桶的序号,每个桶内有两个数组构成,第一个数组存储散列键的高发位置,第二个数组是一个字节数组,用于存储键值对,该字节数组先依次存储了这个桶里的所有键,
之后依次存储了这个桶里的所有值。

映射的内部结构的简单表示

4.3.2 创建和初始化

映射的键可以是任何可以使用(==)比较的值,切片、函数以及包含切片的结构类型不能作为映射的键,因为他们包含引用语义

1
2
3
4
5
6
7
8
9
10
// 创建一个映射,键的类型是string,值的类型是int
dict := make(map[string]int)
// 创建一个映射,键和值的类型都是string
// 使用两个键值对初始化映射
dict := map[string]string{"Red": "#da1337", "Orange": "#e95a22"}

// 创建一个映射,使用字符串切片作为映射的键
dict := map[[]string]int{}
// Compiler Exception:
// invalid map key type []string

4.3.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
36
37
38
39
40
41
42
43
44
45
46
47
48
// 创建一个空映射,用来存储颜色以及颜色对应的十六进制代码
colors := map[string]string{}
// 将Red 的代码加入到映射
colors["Red"] = "#da1337"

// 通过声明映射创建一个nil 映射, 可以通过声明一个未初始化的映射来创建一个值为nil 的映射(称为nil 映射)。nil 映射
// 不能用于存储键值对,否则,会产生一个语言运行时错误
var colors map[string]string
// 将Red 的代码加入到映射
colors["Red"] = "#da1337"
// Runtime Error:
// panic: runtime error: assignment to entry in nil map

// 从映射获取值并判断键是否存在
// 获取键Blue 对应的值
value, exists := colors["Blue"]
// 这个键存在吗?
if exists {
fmt.Println(value)
}

// 从映射获取值,并通过该值是否为零值来判断键是否存在
// 获取键Blue 对应的值
value := colors["Blue"]
// 这个键存在吗?
if value != "" {
fmt.Println(value)
}

// 遍历for range
// 创建一个映射,存储颜色以及颜色对应的十六进制代码
colors := map[string]string{
"AliceBlue": "#f0f8ff",
"Coral": "#ff7F50",
"DarkGray": "#a9a9a9",
"ForestGreen": "#228b22",
}
// 显示映射里的所有颜色
for key, value := range colors {
fmt.Printf("Key: %s Value: %s\n", key, value)
}

// 删除键为Coral 的键值对
delete(colors, "Coral")
// 显示映射里的所有颜色
for key, value := range colors {
fmt.Printf("Key: %s Value: %s\n", key, value)
}

4.3.4 在函数间传递映射

在函数间传递映射并不会制造出该映射的副本,实际上,当传递映射给一个函数,并对这个映射做了修改时,所有对这个映射的引用都会察觉到这个修改,和切片类似

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
func main() {
// 创建一个映射,存储颜色以及颜色对应的十六进制代码
colors := map[string]string{
"AliceBlue": "#f0f8ff",
"Coral": "#ff7F50",
"DarkGray": "#a9a9a9",
"ForestGreen": "#228b22",
}
// 显示映射里的所有颜色
for key, value := range colors {
fmt.Printf("Key: %s Value: %s\n", key, value)
}
// 调用函数来移除指定的键
removeColor(colors, "Coral")
// 显示映射里的所有颜色
for key, value := range colors {
fmt.Printf("Key: %s Value: %s\n", key, value)
}
}
// removeColor 将指定映射里的键删除
func removeColor(colors map[string]string, key string) {
delete(colors, key)
}

// output
// Key: AliceBlue Value: #F0F8FF
// Key: Coral Value: #FF7F50
// Key: DarkGray Value: #A9A9A9
// Key: ForestGreen Value: #228B22
// Key: AliceBlue Value: #F0F8FF
// Key: DarkGray Value: #A9A9A9
// Key: ForestGreen Value: #228B22

4.4 小结

数组是构造切片和映射的基石
Go语言里切片经常用来处理数据的集合,映射用来处理具有键值对结构的数据
内置函数make可以创建切片和映射,并指定原始的长度和容量,也可以直接使用切片和映射字面量,或者使用字面量作为变量的初始值
切片有容量限制,不过可以使用内置的append函数扩展容量
映射的增长没有容量或者任何限制
内置函数len可以用来获取切片或者映射的长度
内置函数cap只能用于切片
通过组合,可以创建多维数组和多维切片。也可以使用切片或者其他映射作为映射的值,但是切片不能用作映射的键
将切片或者映射传递给函数的成本很小,并且不会复制底层的数据结构

打包和工具链-《Go In Action》-Ch3

3.1 包

对于所有的.go文件,都应该在首行声明自己所属的包,每个包在一个单独的目录中,不能把多个包放在一个目录中,也不能把相同的包拆分到不同的目,同一个包下的所有
.go文件必须声明同一个包名

包命名惯例,简洁,顾名思义

main包是特殊的包,编译程序会将带有main包声明的包编译为二进制可执行文件,main包中一定要有main函数,否则不可以创建可执行文件

3.2 导入

导入包查找顺序 GOROOT -》 GOPATH,一旦编译器找到一个满足的包就会停止查找,也可以远程导入包,通过go get 将GitHub的包下载到本地

1
2
3
4
5
import (
"fmt"
"strings"
"github.com/spf13/viper"
)

如果包重名,可以使用命名导入,将导入的包命名为新名字

1
2
3
4
5
6
7
8
9
10
11
package main

import (
"fmt"
myfmt "mylib/fmt"
)

func main() {
fmt.Println("Standard Library")
myfmt.Println("mylib/fmt")
}

如果导入了不在代码使用的包,会导致编译失败,可以使用下划线来重命名不适用的包

3.3 函数init

每个包可以包含任意多个init函数,在main之前被调用,init函数用在设置包、初始化变量或者其他要在程序运行前优先完成的引导工作,可以使用下划线来重命名不适用的包
以数据库驱动为例,sql包在编译时并不知道要注册哪些驱动,如果我们要使用数据库连接,就需要用init函数将驱动注册到mysql上

1
2
3
4
5
6
7
8
9
package postgres

import (
"database/sql"
)

func init() {
sql.Register("postgres", new(PostgresDriver))
}

在使用这个新的数据库驱动时,需要使用空白标识符导入包

1
2
3
4
5
6
7
8
9
10
package main

import (
"database/sql"
_ "github.com/goinaction/code/chapter3/dbdriver/postgres"
)

func main() {
sql.Open("postgres", "mydb")
}

3.4 使用Go的工具

go build 执行编译
参数为空时 默认编译当前目录
参数可以为文件名
参数可以为/… 会编译目录下的所有包

go clean 执行清理,会删除可执行文件
go run 先构建再执行

3.5 进一步介绍Go开发工具

go vet 检测代码常见错误 printf类型匹配错误参数 方法签名错误 错误的结构标签 没有指定字段名的结构字面量
go fmt 格式化代码
go doc 打印文档
godoc 浏览器打开文档 godoc -http:6000

3.7 依赖管理

没有实际管理过一个大工程,这里看的稀里糊涂的,暂时不表了,等搞清楚再补

3.8 小结

Go语言中包是组织代码的基本单位
环境变量GOPATH决定了GO源代码在磁盘上被保存、编译和安装的位置
可以为每个工程设置不同的GOPATH,以保持源代码和依赖的隔离
go工具是在命令行上工作的最好工具
开发人员可以使用go get获取别人的包并将其安装到自己的GOPATH指定的目录
想要为别人创建包很见到,只要把源代码放到共有代码库,把那个遵守一些简单的规则就可以了
GO语言在设计时将分享代码作为语言的核心特性和驱动力
推荐使用依赖管理工具来管理依赖
有很多社区开发的依赖管理工具,godep、vendor、gb

快速开始一个GO程序-《Go In Action》-Ch2

一上来接来个大程序,新手能接得住么
这个程序从不同的数据源拉取数据,将数据内容与一组搜索项做对比,然后将匹配的内容显示在终端窗口。这个程序会读取文本文件,
进行网络调用,解码XML 和JSON 成为结构化类型数据,并且利用Go 语言的并发
机制保证这些操作的速度source code

2.1 程序架构

1
2
3
4
5
6
7
8
9
10
11
- sample
- data
data.json -- 包含一组数据源
- matchers
rss.go -- 搜索 rss 源的匹配器
- search
default.go -- 搜索数据用的默认匹配器
feed.go -- 用于读取 json 数据文件
match.go -- 用于支持不同匹配器的接口
search.go -- 执行搜索的主控制逻辑
main.go -- 程序的入口

2.2 main 包

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

import (
"log"
"os"

_ "github.com/goinaction/code/chapter2/sample/matchers"
"github.com/goinaction/code/chapter2/sample/search"
)

// init is called prior to main.
func init() {
// Change the device for logging to stdout.
log.SetOutput(os.Stdout)
}

// main is the entry point for the program.
func main() {
// Perform the search for the specified term.
search.Run("president")
}

有以下几点需要注意:

  • main()是程序的入口,没有main函数,构建程序不会生成可执行文件
  • 一个包定义一组编译通过的代码,包的名字类似命名空间,可以用来直接访问包内生命的标识符, 可以报不同包中定义的同名标识符区别开
  • 下划线开头的包,是为了进行包的初始化操作,GO不允许声明导入包却不使用,下划线让编译器接受这种到日,并且调用对应包内所有文件代码里定义的init函数,init函数的执行在main函数之前

2.3 search 包

serach 包包含了程序使用的框架和业务逻辑

2.3.1 serach.go

serach文件先获取数据源,然后对每个数据源获取的数据进行匹配,每一个匹配启用一个goroutine。使用sync.WaitGroup控制任务是否完成。
sync.WaitGroup是一个计数信号量,主要有三个方法Add、Done和Wait,每增加一个任务就Add一次,每完成一个任务就Done一次,调用Wait的时候程序会阻塞,直到所有任务完成。

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

//从标准库导入代码时,只需要给出要导入包的包名,
//编译器查找包时,总是会到GOROOT和GOPATH环境变量引用的位置去查找
import (
"log"
"sync"
)

// A map of registered matchers for searching.
// 小写字母标识,标识包内变量,不导出 or 不公开
var matchers = make(map[string]Matcher)

// Run performs the search logic.
func Run(searchTerm string) {
// Retrieve the list of feeds to search through.
feeds, err := RetrieveFeeds()
if err != nil {
log.Fatal(err)
}

// Create an unbuffered channel to receive match results to display.
results := make(chan *Result)

// Setup a wait group so we can process all the feeds.
var waitGroup sync.WaitGroup

// Set the number of goroutines we need to wait for while
// they process the individual feeds.
waitGroup.Add(len(feeds))

// Launch a goroutine for each feed to find the results.
for _, feed := range feeds {
// Retrieve a matcher for the search.
matcher, exists := matchers[feed.Type]
if !exists {
matcher = matchers["default"]
}

// Launch the goroutine to perform the search.
go func(matcher Matcher, feed *Feed) {
Match(matcher, feed, searchTerm, results)
waitGroup.Done()
}(matcher, feed)
}

// Launch a goroutine to monitor when all the work is done.
go func() {
// Wait for everything to be processed.
waitGroup.Wait()

// Close the channel to signal to the Display
// function that we can exit the program.
close(results)
}()

// Start displaying results as they are available and
// return after the final result is displayed.
Display(results)
}

// Register is called to register a matcher for use by the program.
func Register(feedType string, matcher Matcher) {
if _, exists := matchers[feedType]; exists {
log.Fatalln(feedType, "Matcher already registered")
}

log.Println("Register", feedType, "matcher")
matchers[feedType] = matcher
}

对于上面代码,有以下问题需要明确下:

  • feeds, err := RetrieveFeeds() 这种一个函数返回两个值,第一个参数返回值,第二个返回错误信息,是GO中常用的模式
  • 声明运算符(:=),这个运算符在声明变量的同时,给变量赋值
  • feeds 是一个切片,可以理解为Go里面的动态数组,是一种引用类型
  • results是一个无缓冲的channel,和map、slice一样,都是引用类型,channel内置同步机制,从而保证通信安全
  • Go中,如果main函数返回,整个程序也就终止了,终止时,会关闭所有之前启动而且还在运行的goroutine
  • for range对feeds切片做迭代,和python里面的 for in一样的道理,每次迭代会返回两个值(index,value),value是一个副本,下划线_是一个占位符
  • 使用go关键字启动一个goroutine,并对这个goroutine做并发调度。上面程序中go启动了一个匿名函数作为goroutine
  • 在Go语言中,所有的变量都是以值的方式传递。所以想要修改真正的值,可以传递指针
  • Go语言支持闭包,匿名函数中访问searchTerm、results就是通过闭包的形势访问的。注意matcher、feed这两个变量并没有使用闭包的形式访问

2.3.2 feed.go

feed会从本地的data/data.json中读取Json数据,并将数据反序列化为feed切片,defer会安排随后的函数调用在函数返回时才执行, 使用defer可以缩短打开文件和关闭文件的代码间隔

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

import (
"encoding/json"
"os"
)

const dataFile = "data/data.json"

// Feed contains information we need to process a feed.
type Feed struct {
Name string `json:"site"`
URI string `json:"link"`
Type string `json:"type"`
}

// RetrieveFeeds reads and unmarshals the feed data file.
func RetrieveFeeds() ([]*Feed, error) {
// Open the file.
file, err := os.Open(dataFile)
if err != nil {
return nil, err
}

// Schedule the file to be closed once
// the function returns.
defer file.Close()

// Decode the file into a slice of pointers
// to Feed values.
var feeds []*Feed
err = json.NewDecoder(file).Decode(&feeds)

// We don't need to check for errors, the caller can do this.
return feeds, err
}

2.3.3 match.go/default.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package search

// defaultMatcher implements the default matcher.
type defaultMatcher struct{}

// init registers the default matcher with the program.
func init() {
var matcher defaultMatcher
Register("default", matcher)
}

// Search implements the behavior for the default matcher.
func (m defaultMatcher) Search(feed *Feed, searchTerm string) ([]*Result, error) {
return nil, nil
}

func (m defaultMatcher) Search 意味着search和defaultMatcher的值绑定在了一起,我们可以使用defaultMatcher 类型的值或者指向这个类型值的指针来调用Search 方
法。无论我们是使用接收者类型的值来调用这个方,还是使用接收者类型值的指针来调用这个
方法,编译器都会正确地引用或者解引用对应的值,作为接收者传递给Search 方法

1
2
3
4
5
6
7
8
9
10
11
12
// 方法声明为使用defaultMatcher 类型的值作为接收者
func (m defaultMatcher) Search(feed *Feed, searchTerm string)
// 声明一个指向defaultMatcher 类型值的指针
dm := new(defaultMatch)
// 编译器会解开dm 指针的引用,使用对应的值调用方法
dm.Search(feed, "test")
// 方法声明为使用指向defaultMatcher 类型值的指针作为接收者
func (m *defaultMatcher) Search(feed *Feed, searchTerm string)
// 声明一个defaultMatcher 类型的值
var dm defaultMatch
// 编译器会自动生成指针引用dm 值,使用指针调用方法
dm.Search(feed, "test")

与直接通过值或者指针调用方法不同,如果通过接口类型的值调用方法,规则有很大不同,
如代码清单2-38 所示。使用指针作为接收者声明的方法,只能在接口类型的值是一个指针的时
候被调用。使用值作为接收者声明的方法,在接口类型的值为值或者指针时,都可以被调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 方法声明为使用指向defaultMatcher 类型值的指针作为接收者
func (m *defaultMatcher) Search(feed *Feed, searchTerm string)
// 通过interface 类型的值来调用方法
var dm defaultMatcher
var matcher Matcher = dm // 将值赋值给接口类型
matcher.Search(feed, "test") // 使用值来调用接口方法
> go build
cannot use dm (type defaultMatcher) as type Matcher in assignment
// 方法声明为使用defaultMatcher 类型的值作为接收者
func (m defaultMatcher) Search(feed *Feed, searchTerm string)
// 通过interface 类型的值来调用方法
var dm defaultMatcher
var matcher Matcher = &dm // 将指针赋值给接口类型
matcher.Search(feed, "test") // 使用指针来调用接口方法
> go build
Build Successful

match创建不同类型的匹配器,Matcher其实是一个接口,对于每种匹配器又有不同的具体实现。
下面的代码中,Matcher接口定义了一个Search方法,每个实现了Search方法的类型都实现了Matcher接口

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

import (
"log"
)

// Result contains the result of a search.
type Result struct {
Field string
Content string
}

// Matcher defines the behavior required by types that want
// to implement a new search type.
type Matcher interface {
Search(feed *Feed, searchTerm string) ([]*Result, error)
}

// Match is launched as a goroutine for each individual feed to run
// searches concurrently.
func Match(matcher Matcher, feed *Feed, searchTerm string, results chan<- *Result) {
// Perform the search against the specified matcher.
searchResults, err := matcher.Search(feed, searchTerm)
if err != nil {
log.Println(err)
return
}

// Write the results to the channel.
for _, result := range searchResults {
results <- result
}
}

// Display writes results to the console window as they
// are received by the individual goroutines.
func Display(results chan *Result) {
// The channel blocks until a result is written to the channel.
// Once the channel is closed the for loop terminates.
for result := range results {
log.Printf("%s:\n%s\n\n", result.Field, result.Content)
}
}

Display方法会迭代results这个channel,有数据时会打印,没数据时会阻塞,当main.go中的close(result)后,for range循环结束

注意到default.go有init函数,这个函数会在main中通过下划线导入包的时候执行,init的功能是初始化匹配器

2.4 RSS 匹配器

rss.go篇幅过长,这里不贴代码了,其中有几个关注的点说下:
在init中注册了一个rssMatcher,这个match和之前的defaultMatcher一样,绑定了Search方法,即实现了Matcher接口

1
2
3
4
func init() {
var matcher rssMatcher
search.Register("rss", matcher)
}

rss.go主要有两个方法retrieve和Search,retrieve负责抓取网略资源,search负责匹配,具体匹配方法这里不表了

2.5 小结

  • 每个代码文件都属于一个包,而包名应该与代码文件所在的文件夹同名。
  • Go 语言提供了多种声明和初始化变量的方式。如果变量的值没有显式初始化,编译器会将变量初始化为零值。
  • 使用指针可以在函数间或者goroutine 间共享数据。
  • 通过启动goroutine 和使用通道完成并发和同步。
  • Go 语言提供了内置函数来支持Go 语言内部的数据结构。
  • 标准库包含很多包,能做很多很有用的事情。
  • 使用Go 接口可以编写通用的代码和框架。

关于go语言的介绍-《Go In Action》-Ch1

1.1 Go 解决现代编程的难题

1.1.1 开发速度

更加智能的编译器,简化依赖算法,编译速度更快,只会关注直接被引用的库
编译器提供类型检查

1.1.2 并发

提供并发支持,goroutine比线程更轻量级的并发,内置channel,在不同goroutine之间通信

1.goroutine

goroutine是可以和其他goroutine并行执行的函数,使用的内存更少。运行时会自动的配置一组逻辑处理器执行goroutine,每个逻辑处理器绑定到一个操作系统线程上,程序实行效率更高

1
2
3
4
func log(msg string) {
....
}
go log("blablabla")

2.channel

channel是可以让goroutine之间进行安全通信的工具,避免共享内存访问的问题,保证同一时刻只会有一个goroutine修改数据, 但是channel并不提供跨goroutine的数据访问保护机制,
如果传输的是副本,那么每个goroutine都持有一个副本,各自对副本修改是安全的。但是如果传输的诗指针时,还是需要额外的同步动作

1.1.3 Go语言的类型系统

无继承,使用组合设计模式,具有接口机制对行为进行建模

1. 类型简单

内置简单类型,支持自定义类型,使用组合来支持扩展

2.Go接口对一组行为建模

一个类型实现了一个接口的所有方法,那么这个类型的实例就可以存储在这个接口类型的实例中,不需要额外声明
Go语言的整个网络库都是用了io.Reader接口,这样可以将程序的功能和不同的网络实现分离,任何实现了open方法的类型,都实现了io.Reader接口

1
2
3
4
//io.Reader
type Reader interface{
Read(p []byte) (n int, err error)
}

1.1.3 内存管理

Golang提供GC

1.2 Hello Go

1
2
3
4
5
package main
import "fmt"
func main() {
fmt.Println("Hello World")
}

Go Playground是提供在线编辑运行Go的网站

Python闭包和go闭包

最近学习golang,发现golang里面也有闭包的概念,这里和python比较一下,并有些新的体会,记下来

1.Python 闭包

闭包:引用了自由变量的函数,从下面这个例子看出,decorator的返回值_wrap引用了两个自由变量f和cache
通过closure可以一探究竟,输出结果可以看出closure包含两个元素,一个是function,另外一个是cache,

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
def decorator(f):
cache = []
def _wrap(*args, **kwargs):
cache.append(1)
print(cache)
f(*args, **kwargs)

return _wrap

# @decorator
def foo():
print("foo")

if __name__ == "__main__":

a = decorator(foo)
a()
print(a.__closure__)
print(a.__closure__[1].cell_contents)
print(a.__closure__[0].cell_contents)

# output

#foo
#(<cell at 0x1094cd198: list object at 0x10acea248>, <cell at 0x1094cd4c8: function object at 0x107879268>)
#<function foo at 0x107879268>
#[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
def decorator(f):
cache = []

def _wrap(*args, **kwargs):
cache.append(1)
print(cache)
f(*args, **kwargs)

return _wrap


# @decorator
def foo():
print("foo")


if __name__ == "__main__":
a = decorator(foo)
a()
a()
print(a.__closure__)
print(a.__closure__[1].cell_contents)
print(a.__closure__[0].cell_contents)

b = decorator(foo)
b()
b()
print(b.__closure__)
print(b.__closure__[1].cell_contents)
print(b.__closure__[0].cell_contents)

# output
# [1]
# foo
# [1, 1]
# foo
# (<cell at 0x10eb34198: list object at 0x11a657248>, <cell at 0x10eb344c8: function object at 0x10cee0268>)
# <function foo at 0x10cee0268>
# [1, 1]
# [1]
# foo
# [1, 1]
# foo
# (<cell at 0x10eb34a98: list object at 0x112abb188>, <cell at 0x10eb34af8: function object at 0x10cee0268>)
# <function foo at 0x10cee0268>
# [1, 1]

可以看到闭包的自由变量的作用域对于每个函数是独立的,即a和b对于f和cache的引用是独立的

2.Go闭包

go的闭包和python基本一样,看个例子

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

import "fmt"

func decorator(f func()) func(){
var cache []string
cache = append(cache, "1")
_wrap := func() {
cache = append(cache, "foo")
fmt.Printf("%s\n", cache)
f()
}
return _wrap
}

func foo() {
fmt.Println("foo")
}

func main() {
a := decorator(foo)
a()
a()
b := decorator(foo)
b()
b()
}
// output
// [1 foo]
// foo
// [1 foo foo]
// foo
// [1 foo]
// foo
// [1 foo foo]
//foo

可以看到对于a和b对于自用变量的引用也是独立的,互不影响

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×