操作系统 - 锁

mutex lock

所谓锁,在计算机中,本质是内存中的一块空间,其值被赋予了加锁/未加锁的含义,其底层实现,是基于计算机系统中的原子操作。

mutex lock 核心包含两点。

  • Compare And Set(CAS)
    • 互斥锁是对临界区的保护,能否进入临界区即能否获取到锁,思路都是判断并置位一个标志位,这个标志位本身可以是一个普通的内存变量,关键在于,对于这个变量的"判断并置位"操作需要是原子的。
    • 计算机实现原子操作,对于单核机器,关中断防止调度即可;多核理论上可使用 spinlock 保护,实际一般通过原子汇编语言实现,比如x86的tsl指令(test and set),可以用一条“无法继续分割的”汇编指令实现判断变量值并根据是否为0进行置位,具体这个指令实现原子性一般通过锁总线实现,也就是我执行这条指令时,其它核都不能访问这个地址了。
  • 根据所是否获得,决定执行策略
    • 如果锁持有,则继续运行
    • 如果所没有获取到,常规做法是将当前任务挂起,并附在 mutex 变量对应的链表上,一旦锁被释放,查找锁上挂起的任务并唤醒

操作系统 - 进程、线程以及协程

简述

进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。

线程(Thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。

协程 (coroutine)是可暂停和恢复执行的过程,是一种编程思想。

协程

子程序,或者称为函数,在所有语言中都是层级调用。子程序调用是通过栈实现的,一个线程同一时间就是执行一个子程序。协程本质上也是子程序,协程作为子程序最大的特征是可中断可恢复

关联

Java 中的线程

green threads VS native threads

green threads 是一种由运行环境或虚拟机(VM)调度,而不是由本地底层操作系统调度的线程。绿色线程并不依赖底层的系统功能,模拟实现了多线程的运行,这种线程的管理调配发生在用户空间而不是内核空间,所以它们可以在没有原生线程支持的环境中工作。
在Java 1.1中,绿色线程(至少在 Solaris 上)是JVM 中使用的唯一一种线程模型。 由于绿色线程和原生线程比起来在使用时有一些限制,随后的 Java 版本中放弃了绿色线程,转而使用native threads。[2]

在 Java1.2 之后. Linux中的JVM是基于pthread实现的,即 现在的Java中线程的本质,其实就是操作系统中的线程

参考

c++ debug

addr2line

1
addr2line -f -e path/to/binary <address>

nm

列出对象文件中的符号

1
2
nm libssl.a 
nm -an libssl.a
1
2
# 安装
$ yum install binutils

ldd

1
2
3
4
5
6
7
8
$ ldd main
linux-vdso.so.1 (0x00007ffe5458c000)
libresolv.so.2 => /lib/x86_64-linux-gnu/libresolv.so.2 (0x00007f74d9d1a000)
libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007f74d9c33000)
libstdc++.so.6 => /lib/x86_64-linux-gnu/libstdc++.so.6 (0x00007f74d9a07000)
libgcc_s.so.1 => /lib/x86_64-linux-gnu/libgcc_s.so.1 (0x00007f74d99e7000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f74d97be000)
/lib64/ld-linux-x86-64.so.2 (0x00007f74dc649000)

strings

strings 程序的主要功能是找出文件(包括文本文件、二进制文件等)内容中的可打印字符串。

1
strings /usr/lib/libstdc++.so.6 | grep GLIBCXX

java function

简述

  • Function<A, B> : 一个参数 + 返回值
  • Supplier<A> : 无参数 + 返回值

示例

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
public class FunctionTest {

// Function<A, B> : 一个参数 + 返回值
// Supplier<A> : 无参数 + 返回值
static class FC implements Function<String, String> {
private final Function<String, String> f;
private final Supplier<Long> spl;
private final Function<String[], String> mf;

public FC(Function<String, String> f,
Supplier<Long> spl,
Function<String[], String> mf) {
this.f = f;
this.spl = spl;
this.mf = mf;
}

@Override
public String apply(String s) {
return spl.get() + " " + f.apply(s);
}

public String rmf(String[] ss) {
return mf.apply(ss);
}
}

public static void main(String[] args) {
FC fc = new FC((item) -> "Hello " + item,
System::currentTimeMillis,
(ags) -> String.join(",", ags));
System.out.println(fc.apply(fc.rmf(new String[]{"Tom", "Kitty"})));
}
}

java 锁

锁分类

  • 公平锁 / 非公平锁
  • 可重入锁 / 不可重入锁
  • 独享锁 / 共享锁
  • 互斥锁 / 读写锁
  • 乐观锁 / 悲观锁
  • 分段锁
  • 偏向锁 / 轻量级锁 / 重量级锁
  • 自旋锁

公平锁 & 非公平锁

以多线程申请及获取锁的顺序区分,先申请先得则为公平锁。非公平锁容易造成饥饿现象,但吞吐量优于公平锁(公平锁实现会先判断是否有前驱线程在等待)。

ReentrantLock 可通过构造参数指定为公平锁,默认非公平锁,基于AQS实现线程调度。

synchronized 没有线程调度机制,所以为非公平锁。

可重入锁 & 不可重入锁

可重入锁可重复可递归调用的锁,行为为一个线程可以对同一对象多次加锁。

独享锁 & 共享锁

独享锁一次只能被一个线程加锁,共享锁可被多个线程加锁。

互斥锁 & 读写锁

互斥锁

访问临界资源前加锁,访问后解锁。加锁后,再次加锁会被阻塞,直到当前进程解锁。同一时间,只有一个线程,能够访问被互斥锁保护的资源。

读写锁

读写锁由read模式的共享锁和write模式的互斥锁组成,读写锁有三种状态,读加锁状态、写加锁状态、不加锁状态。

乐观锁 & 悲观锁

悲观锁

总是假设最坏情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞直到它拿到锁(共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程)。传统的关系型数据库里边就用到了很多这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁。JavasynchronizedReentrantLock等独占锁就是悲观锁思想的实现。

乐观锁

总是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号机制和CAS算法实现。乐观锁适用于多读的应用类型,这样可以提高吞吐量,像数据库提供的类似于write_condition机制,其实都是提供的乐观锁。在Javajava.util.concurrent.atomic包下面的原子变量类就是使用了乐观锁的一种实现方式CAS实现的

分段锁

分段锁是一种锁的设计,通过减小锁的粒度,提升多并发程序性能。

偏向锁 & 轻量级锁 & 重量级锁

JVM为了提高锁的获取与释放效率,在对象头中添加字段,使得对象监视器可获取其锁的状态,锁的状态如下。

  • 无锁状态
  • 偏向锁状态
  • 轻量级锁状态
  • 重量级锁状态

偏向锁

偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁。降低获取锁的代价。

轻量级

轻量级锁是指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。

重量级锁

重量级锁是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其他申请的线程进入阻塞,性能降低。

自旋锁

自旋锁(spinlock):是指当一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,直到获取到锁才会退出循环

它是为实现保护共享资源而提出一种锁机制。其实,自旋锁与互斥锁比较类似,它们都是为了解决对某项资源的互斥使用。无论是互斥锁,还是自旋锁,在任何时刻,最多只能有一个保持者,也就说,在任何时刻最多只能有一个执行单元获得锁。但是两者在调度机制上略有不同。对于互斥锁,如果资源已经被占用,资源申请者只能进入睡眠状态。但是自旋锁不会引起调用者睡眠,如果自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁,”自旋”一词就是因此而得名。

实现示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class SpinLock {
private AtomicReference<Thread> cas = new AtomicReference<Thread>();
public void lock() {
Thread current = Thread.currentThread();
// 利用CAS
while (!cas.compareAndSet(null, current)) {
// DO nothing
}
}
public void unlock() {
Thread current = Thread.currentThread();
cas.compareAndSet(current, null);
}
}

lock() 方法利用的CAS,当第一个线程A获取锁的时候,能够成功获取到,不会进入while循环,如果此时线程A没有释放锁,另一个线程B又来获取锁,此时由于不满足CAS,所以就会进入while循环,不断判断是否满足CAS,直到A线程调用unlock方法释放了该锁。

问题

1、如果某个线程持有锁的时间过长,就会导致其它等待获取锁的线程进入循环等待,消耗CPU。使用不当会造成CPU使用率极高。
2、上面Java实现的自旋锁不是公平的,即无法满足等待时间最长的线程优先获取锁。不公平的锁就会存在“线程饥饿”问题。

优点

  1. 自旋锁不会使线程状态发生切换,一直处于用户态,即线程一直都是active的;不会使线程进入阻塞状态,减少了不必要的上下文切换,执行速度快
  2. 非自旋锁在获取不到锁的时候会进入阻塞状态,从而进入内核态,当获取到锁的时候需要从内核态恢复,需要线程上下文切换。(线程被阻塞后便进入内核(Linux)调度状态,这个会导致系统在用户态与内核态之间来回切换,严重影响锁的性能)

总结

  1. 自旋锁:线程获取锁的时候,如果锁被其他线程持有,则当前线程将循环等待,直到获取到锁。
  2. 自旋锁等待期间,线程的状态不会改变,线程一直是用户态并且是活动的(active)。
  3. 自旋锁如果持有锁的时间太长,则会导致其它等待获取锁的线程耗尽CPU。
  4. 自旋锁本身无法保证公平性,同时也无法保证可重入性。
  5. 基于自旋锁,可以实现具备公平性和可重入性质的锁。

关键词

  • CAS
    • Compare And Swap(比较并交换)
    • AQS 中使用 CAS 设置 State
  • AQS
    • AbstractQueuedSynchronizer 的简称
    • 提供了原子式管理同步状态、阻塞和唤醒线程功能以及队列模型的简单框架
  • CLH
    • CLH(Craig, Landin, and Hagersten locks):是一个自旋锁,能确保无饥饿性,提供先来先服务的公平性。
  • ReentrantLock
    • 可重入锁,线程可对一个临界资源重复加锁
  • synchronized
    • 可重入独享锁
  • volatile
  • ReeReentrantLock
    • 可重入独享锁,AQS
  • ReentrantReadWriteLock
    • 可重入共享锁,AQS

synchronized

jdk 1.5 之前,synchronized 是一种独占式的重量级锁,底层使用系统的 mutex lock 实现。jdk 1.6 之后,做了大量优化,加入了CAS,轻量级锁和偏向锁的功能,性能上已经跟ReentrantLock相差无几。

同步代码块

monitorenter指令插入到同步代码块的开始位置,monitorexit指令插入到同步代码块的结束位置,JVM需要保证每一个monitorenter都有一个monitorexit与之相对应。任何对象都有一个monitor与之相关联,当且一个monitor被持有之后,他将处于锁定状态。线程执行到monitorenter指令时,将会尝试获取对象所对应的monitor所有权,即尝试获取对象的锁。

同步方法

synchronized方法则会被翻译成普通的方法调用和返回指令如:invokevirtual、areturn指令,有一个ACC_SYNCHRONIZED标志,JVM就是通过该标志来判断是否需要实现同步的,具体过程为:当线程执行该方法时,会先检查该方法是否标志了ACC_SYNCHRONIZED,如果标志了,线程需要先获取monitor,获取成功后才能调用方法,方法执行完后再释放monitor,在该线程调用方法期间,其他线程无法获取同一个monitor对象。其实本质上和synchronized块相同,只是同步方法是用一种隐式的方式来实现,而不是显式地通过字节码指令。

锁对象 粒度
普通同步方法 当前实例对象
静态同步方法 当前类的class对象
同步方法块 关键词括号内的对象
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
public class TestLock {
private static final ExecutorService es = Executors.newFixedThreadPool(16);

void sleep() {
try {
Thread.sleep(5000);
} catch (Exception e) {
e.printStackTrace();
}
}

void fe() {
System.out.println("fe: " + LocalDateTime.now());
sleep();
}

synchronized void fd() {
System.out.println("fd: " + LocalDateTime.now());
}

synchronized void fc() {
System.out.println("fc: " + LocalDateTime.now());
}

synchronized void fb() {
System.out.println("fb: " + LocalDateTime.now());
fc();
es.submit(this::fd);
sleep();
}

synchronized void fa() {
System.out.println("fa: " + LocalDateTime.now());
sleep();
}

public static void main(String[] args) {
TestLock tl = new TestLock();
es.submit(tl::fa);
es.submit(tl::fb);
es.submit(tl::fe);
}
}

// output
fe: 2021-03-11T19:57:10.698877
fa: 2021-03-11T19:57:10.698912
fb: 2021-03-11T19:57:15.703141
fc: 2021-03-11T19:57:15.703565
fd: 2021-03-11T19:57:20.708362

// description
1. 普通同步方法,锁粒度为当前实例对象
2. synchronized 为可重入锁,在 fb 中调用 fc 需要重复为当前实例对象上锁
3. 重入锁不适用于子线程

AQS

AQS:AbstractQuenedSynchronizer抽象的队列式同步器。是除了java自带的synchronized关键字之外的锁机制。

核心思想,如果被请求的共享资源空闲,那么将当前请求资源的线程设置为有效的工作线程,将共享资源设置为锁定状态;如果共享资源被占用,就需要一定的阻塞等待机制来保证锁分配。这个机制主要用的是CLH队列的变体实现的,将暂时获取不到锁的线程加入到队列中。

AQS是将每一条请求共享资源的线程封装成一个CLH锁队列的一个结点(Node),来实现锁的分配。

CLH:Craig、Landin and Hagersten队列,是单向链表,AQS中的队列是CLH变体的虚拟双向队列(FIFO),AQS是通过将每条请求共享资源的线程封装成一个节点来实现锁的分配。

AQS使用一个volatile的int类型的成员变量来表示同步状态,通过内置的FIFO队列来完成资源获取的排队工作,通过CAS完成对State值的修改。

CAS

CAS是英文单词Compare and Swap(比较并交换),是一种有名的无锁算法。无锁编程,即不使用锁的情况下实现多线程之间的变量同步,也就是在没有线程被阻塞的情况下实现变量的同步,所以也叫非阻塞同步(Non-blocking Synchronization)。CAS算法涉及到三个操作数

  1. 需要读写的内存值 V

  2. 进行比较的值 A

  3. 拟写入的新值 B

更新一个变量的时候,只有当变量的预期值A和内存地址V当中的实际值相同时,才会将内存地址V对应的值修改为B,否则不会执行任何操作。一般情况下是一个自旋操作,即不断的重试。

指令重排

为了提高程序执行的性能,编译器和执行器(处理器)通常会对指令做一些优化(重排序)。

volatile

并发编程的三大特性,原子性、可见性、有序性。synchronized可保证三大特性(保护的代码块通知只能有一个线程执行,单线程没有指令重排的问题),volatile 可以保证可见性(把工作内存中的最新变量强制刷新至主存)和有序性(编译器在生成字节码时,在指令序列中添加“内存屏障”来禁止指令重排序)。

对比

ReentrantLock VS Synchronized

- ReentrantLock Synchronized
锁实现机制 AQS 监视器模式
灵活性 支持响应中断、超时、尝试获取锁 不灵活
释放形式 必须显式调用 unlock 释放锁 自动释放监视器
锁类型 公平锁 & 非公平锁 非公平锁
条件队列 可关联多个条件队列 关联一个条件队列
可重入性 可重入 可重入

关联

  • ConcurrentHashMap
    • Jdk 1.7 VS Jdk 1.8

ConcurrentHashMap

JDK 1.7

Jdk 1.7 中,HashMap 底层为数组+链表实现,ConcurrentHashMap与其不同的是,添加了 Segment 作为数组+链表的上层结构,Concurrent 继承 ReentrantLock,对 Segment 加锁,实现不同 Segment 可以同时读写。在写入获取key所在Segment是需要保证可见性,ConcurrentHashMap使用如下方法保证可见性,取得最新的Segment。

1
Segment<K,V> s = (Segment<K,V>)UNSAFE.getObjectVolatile(segments, u)

写入 Segment 时,需要获取锁,先使用自旋锁,超过重试次数后,通过 lock 获取锁(会被阻塞,切换至内核态等待)。

JDK 1.8

Jdk 1.8 中的 ConcurrentHashMap 刨除了 Segment 的设计,直接 <大数组+[链表|红黑树]> 的方式,如果数组对应的链表超过一定阈值后,转换为红黑树。大数组使用 volatile 关键字修饰。

对于写操作,key对应的数组元素,使用 synchronized 关键字申请锁(当然,如果为null,则不需要加锁),然后进行操作。

对于读操作,数组使用 volatile 保证可见性,数组的每个元素为Node实例(jdk 1.7 为 HashEntry),他的 key 和 hash 值都使用 final 修饰,无需担心可见性。value 和下一个元素的引用由 volatile 修饰,保证可见性。

1
2
3
4
5
6
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
volatile V val;
volatile Node<K,V> next;
}

对于 key 对应的数组元素的可见性,使用 Unsafe 的 getObjectVolatile 方法保证。

1
2
3
static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) {
return (Node<K,V>)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE);
}

mutex lock

所谓锁,在计算机中,本质是内存中的一块空间,其值被赋予了加锁/未加锁的含义,其底层实现,是基于计算机系统中的原子操作。

mutex lock 核心包含两点。

  • Compare And Set
    • 互斥锁是对临界区的保护,能否进入临界区即能否获取到锁,思路都是判断并置位一个标志位,这个标志位本身可以是一个普通的内存变量,关键在于,对于这个变量的"判断并置位"操作需要是原子的。
    • 计算机实现原子操作,对于单核机器,关中断防止调度即可;多核理论上可使用 spinlock 保护,实际一般通过原子汇编语言实现,比如x86的tsl指令(test and set),可以用一条“无法继续分割的”汇编指令实现判断变量值并根据是否为0进行置位,具体这个指令实现原子性一般通过锁总线实现,也就是我执行这条指令时,其它核都不能访问这个地址了。
  • 根据所是否获得,决定执行策略
    • 如果锁持有,则继续运行
    • 如果所没有获取到,常规做法是将当前任务挂起,并附在 mutex 变量对应的链表上,一旦锁被释放,查找锁上挂起的任务并唤醒

参考

java 线程

Executor

1
2
3
4
5
6
7
8
9
10
11
12
// 创建 executor
private static final ExecutorService executor = Executors.newFixedThreadPool(8);

// 异步提交
executor.submit(Callable<V> / Runnable);
executor.execute(Runnable);

// 同步等待
executor.invokeAll(Collection<Callable<V>>);

// shutdown
executor.shutdown();

Tips

  • submit VS execute
    • 参数不同,submit 可接受 Callable 和 Runable 类型变量,execute 只接受 Runable
    • submit 提交任务后可在主线程同步等待,execute 由于没有返回值,则不行
    • submit 可以通过 Future.get() 在主线程捕获异常,execute 则不行

Thrift

示例

序列化

1
2
TSerializer serializer = new TSerializer(new TBinaryProtocol.Factory());
byte[] serialized = serializer.serialize(obj);

反序列化

1
2
TDeserializer deserializer = new TDeserializer(new TBinaryProtocol.Factory());
deserializer.deserialize(obj, bytes);

Go 语言基础

[toc]

组织

文件

1
2
package main
package config

版本

1
2
3
4
5
6
# 格式
v<major>.<minor>.<patch>-<pre-release>.<patch>
# 实例
v1.2.23
v1.4.0-beta.2
v2.3.4-alpha.3

数据类型

类型

数字

类型 说明
uint8 -
uint16 -
uint32 -
uint64 -
int8 -
int16 -
int32 -
int64 -
float32 -
float64 -
complex64 复数
complex128 复数
byte 类似 uint8
rune 类似 int32
uint 硬件架构相关,32位或64位无符号整型
int 硬件架构相关,32位或64位有符号整型
uintptr 无符号整型,存放指针

示例

1
2
3
var i int = 1
var f32 float32 = 1.0
var f64 float64 = 1.0

字符串

定义

1
var name string = "wii"

操作

1
2
3
4
5
6
7
8
// 拼接
var name string = "name" + " : " + "wii"

// 取长度
len("wii")

// 截断

布尔

1
val b bool = true

限定

常量

字面常量

1
var i, s, b = 1, "wii", false

itoa

特殊常量,可以被编译器修改的常量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const (
a = iota //0
b //1
c //2
d = "ha" //独立值,iota += 1
e //"ha" iota += 1
f = 100 //iota +=1
g //100 iota +=1
h = iota //7,恢复计数
i //8
)

const (
i=1<<iota // 1
j=3<<iota // 6
k // 12
l // 24
)

变量

声明

1
2
3
4
5
6
7
8
9
10
var identifier [type] = v
var id1, id2 [type] = v1, v2 // 如果只声明,不初始化值,必须制定类型;制定初始化值,编译器可自行推断
id3 := value // OK := 左侧需要有未被声明过的标识符,有即可
id1 := value // ERROR id1 已被声明
id1, id4 := v1, v2 // OK id4 未被声明

var ( // 因式分解式,一般用于声明全局变量
id5 type
id6 type
)

示例

1
2
3
4
5
var a string = "wii"
var b, c int = 1, 2

var e, f = 123, "hello"
g, h := 123, "hello"

数据结构

数组 / slice

声明

1
2
var variable_name [SIZE] variable_type
var variable_name [SIZE1][SIZE2]...[SIZEN] variable_type // 多维数组

初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
var balance = [5]float32{1000.0, 2.0, 3.4, 7.0, 50.0}
balance := [5]float32{1000.0, 2.0, 3.4, 7.0, 50.0}
il := []int{1, 2}

// ... 代替长度
var balance = [...]float32{1000.0, 2.0, 3.4, 7.0, 50.0}
balance := [...]float32{1000.0, 2.0, 3.4, 7.0, 50.0}

// 通过下表初始化
balance := [5]float32{1:2.0,3:7.0} // 将索引为 1 和 3 的元素初始化

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

// 创建空数组
s := make([]int, 0, 10)
// 获取长度
len(s) // 0
// 获取容量
cap(s) // 10

多维数组示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 先声明后赋值
values := [][]int{} // 创建数组
row1 := []int{1, 2, 3}
row2 := []int{4, 5, 6}
values = append(values, row1) // 使用 appped() 函数向空的二维数组添加两行一维数组
values = append(values, row2)

// 维度不一致数组
animals := [][]string{}
row1 := []string{"fish", "shark", "eel"}
row2 := []string{"bird"}
row3 := []string{"lizard", "salamander"}
animals = append(animals, row1)
animals = append(animals, row2)
animals = append(animals, row3)

列表

list

底层实现是双向链表。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 创建
l := list.New()

# 插入元素
l.PushBack(1)
l.PushFront(2)

# 插入其他列表
l.PushFrontList(ol)
l.PushBackList(ol)

# 移除
l.Remove(1)

# 长度
l.Len()

集合

1
2
3
4
var m map[string]string = map[string]string{"France": "Paris", "Italy": "Rome", "Japan": "Tokyo"}
m := make(map[int]int)

// 遍历参考章节 循环

语法

程序结构

注释

1
2
3
4
5
// 单行注释

/*
多行注释
*/

运算符

条件控制

循环

1
2
3
4
5
6
7
8
// 1. 类似于 for(...; ...; ...)
for init; condition; post { }

// 2. 类似于 while
for condition { }

// 3. 类似于 for (;;)
for { }

range

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
// map
for key, value := range oldMap {
newMap[key] = value
}

strings := []string{"google", "runoob"}
for i, s := range strings {
fmt.Println(i, s)
}

// list - 顺序
for i := l.Front(); i != nil; i = i.Next() {
fmt.Println(i.Value)
}
// list -
for i := l.Back(); i != nil; i = i.Prev() {
fmt.Println(i.Value)
}

// 数组
for i := 0; i < len(arr); i++ {
//arr[i]
}
for index, value := range arrHaiCoder{
}
for _, value := range arrHaiCoder{ // 忽略 index
}

函数

格式

1
2
3
func function_name( [parameter list] ) [return_types] {
// 函数体
}

示例

1
2
3
func swap(x, y string) (string, string) {
return y, x
}

参数

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
// 值传递
func swap(x, y int) int {
var temp int

temp = x /* 保存 x 的值 */
x = y /* 将 y 值赋给 x */
y = temp /* 将 temp 值赋给 y*/

return temp;
}
swap(a, b)

// 引用传递
func swap(x *int, y *int) {
var temp int
temp = *x /* 保持 x 地址上的值 */
*x = *y /* 将 y 值赋给 x */
*y = temp /* 将 temp 值赋给 y */
}
swap(&a, &b)

// 函数参数
getSquareRoot := func(x float64) float64 {
return math.Sqrt(x)
}
fmt.Println(getSquareRoot(9))

函数参数

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

// 声明一个函数类型
type cb func(int) int

func main() {
testCallBack(1, callBack)
testCallBack(2, func(x int) int {
fmt.Printf("我是回调,x:%d\n", x)
return x
})
}

func testCallBack(x int, f cb) {
f(x)
}

func callBack(x int) int {
fmt.Printf("我是回调,x:%d\n", x)
return x
}

匿名函数

1
2
3
4
5
6
7
func getSequence() func() int {
i:=0
return func() int {
i+=1
return i
}
}

方法

Go 同时有函数和方法,一个方法就是一个包含了接受者的函数,接受者可以是命名类型或者结构体类型的一个值或者是一个指针。所有给定类型的方法属于该类型的方法集,格式如下。

1
2
3
func (variable_name variable_data_type) function_name() [return_type]{
/* 方法体 */
}

下面定义一个结构体,及该类型的一个方法。

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

import (
"fmt"
)

/* 定义结构体 */
type Circle struct {
radius float64
}

func main() {
var c1 Circle
c1.radius = 10.00
fmt.Println("圆的面积 = ", c1.getArea())
}

//该 method 属于 Circle 类型对象中的方法
func (c Circle) getArea() float64 {
//c.radius 即为 Circle 类型对象中的属性
return 3.14 * c.radius * c.radius
}

结构体

1
2
3
4
5
6
7
8
9
10
11
// 定义
type struct_variable_type struct {
member definition
member definition
...
member definition
}

// 声明
variable_name := structure_variable_type {value1, value2...valuen}
variable_name := structure_variable_type { key1: value1, key2: value2..., keyn: valuen}

示例

1
2
3
4
5
6
7
type Data struct {
s string
sl []string
ll [][]string
}

data := Data {s: "yes"}

打印

1
2
3
4
5
6
# fmt.Printf 占位符
%T 打印变量类型
%p 打印指针
%v 打印值
%s 打印字符串
%d 打印数值

特性

语法糖

空指针处理

函数式编程

接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
// 定义接口
type FileReader interface {
NextLine() string
HasNext() bool
Close()
}

// 定义实现接口实现结构
type FileReaderImpl struct {
dataPath string
file *os.File
scanner *bufio.Scanner
done bool
buf string
}

// 定义接口实现方法
func (impl *FileReaderImpl) NextLine() string {
t := impl.buf
impl.done = !impl.scanner.Scan()

if err := impl.scanner.Err(); err != nil {
log.Fatal(err)
} else {
v := impl.scanner.Text()
impl.buf = v
}

return t
}

func (impl *FileReaderImpl) HasNext() bool {
return !impl.done
}

func (impl *FileReaderImpl) Close() {
err := impl.file.Close()
if err != nil {
log.Fatal(err)
}
}

// 定义创建接口方法
func NewFileReader(dp string) FileReader {
file, err := os.Open(dp)
if err != nil {
log.Fatal(err)
}
scanner := bufio.NewScanner(file)
sr := &FileReaderImpl{dataPath: dp, file: file, scanner: scanner, done: false}
// read one line, for HasNext
sr.NextLine()
return sr
}

注意

NextLine 方法定义由于需要修改 impl 对象内容,必须使用指针。如果使用指针,创建接口时,必须加 & ,比如 sr := &FileReaderImpl{dataPath: dp, file: file, scanner: scanner, done: false}

指针

slice 与指针

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func PointArg(l interface{}) {
fmt.Printf("%p %p\n", l, &l)
}

func main() {
l := make([]int, 0, 10)
fmt.Printf("%T %p\n", l, l)
PointArg(l)
}

// 输出
[]int 0xc0000b8000
0xc0000b8000 0xc000096220

// 分析
1. l 保存的 make 返回的指针
2. 指针传参,直接打印参数值为源参数内存地址;对指针参数 & 操作,返回参数变量的内存地址

struct 与指针

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
func PointArg(l interface{}) {
fmt.Printf("%p %p\n", l, &l)
}

type Person struct {
name string
}

func main() {
p := Person{name: "wii"}
fmt.Printf("%T %v %p\n", p, p, &p)
PointArg(p)
}

// 输出
main.Person {wii} 0xc000010240
%!p(main.Person={wii}) 0xc000010270

// 分析
1. p 不是指针类型
2. 非指针参数传递,会导致内存拷贝

Tips

Interface{}

作为类型参数

使用 interface{} 作为参数类型,来匹配任意类型,注意,不可用 *interface{}

转换类型

1
2
3
4
// i 为 interface{} 变量, 实际类型为 map[string]interface{}
for k, v := range i.(map[string]interface{}) { // 使 i 识别为 map[string]interface{} 类型,并遍历
...
}

函数变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// 保存函数
func main() {
var fns []func()
fns = append(fns, beeper)
fns = append(fns, pinger)

for _, fn := range fns {
fn()
}
}

func beeper() {
fmt.Println("beep-beep")
}

func pinger() {
fmt.Println("ping-ping")
}

// 另外一个示例
type dispatcher struct {
listeners []func()
}

func (d *dispatcher) addListener(f func()) {
d.listeners = append(d.listeners, f)
}

func (d *dispatcher) notify() {
for _, f := range d.listeners {
f()
}
}

func ping() {
fmt.Println("Ping... ping...")
}

func beep() {
fmt.Println("Beep... beep...")
}

func main() {
d := dispatcher{}
d.addListener(ping)
d.addListener(beep)
d.notify()
}

类型判断

1
2
3
4
5
6
7
8
9
t := map[string]interface{}{}
switch t.(type) {
case map[string]interface{}:
for tk, tv := range t.(map[string]interface{}) {
r[tk] = tv
}
default:
r[cvt.Name] = t
}

单测(Unit Tests)

单测文件

创建名为 {code}_test.go 的文件,即为 {code}.go 的单测文件。

示例

echo.go

1
2
3
4
5
package tm

func Echo(s string) string {
return s
}

echo_tests.go

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

import (
"github.com/stretchr/testify/assert"
"testing"
)

func TestEcho(t *testing.T) {
word := "hello"
echo := Echo(word)
assert.Equal(t, word, echo)
}

参考