Go json 库 json-iterator


高效json库.png

直接替换 json.Marshal 替为 jsoniter.Marshal

type ColorGroup struct {
    ID     int
    Name   string
    Colors []string
}
group := ColorGroup{
    ID:     1,
    Name:   "Reds",
    Colors: []string{"Crimson", "Red", "Ruby", "Maroon"},
}
b, err := jsoniter.Marshal(group)

阅读全文

Go json 反序列化成 interface{} 对 Number 处理


json 的规范中,对于数字类型,并不区分是整型还是浮点型。

go json number.gif

对于如下 json 文本:

{
    "name": "ethancai",
    "fansCount": 9223372036854775807
}

如果反序列化的时候指定明确的结构体和变量类型

package main

import (
    "encoding/json"
    "fmt"
)

type User struct {
    Name      string
    FansCount int64
}

func main() {
    const jsonStream = `{"name":"ethancai", "fansCount": 9223372036854775807}`
    // 类型为User
    var user User
    err := json.Unmarshal([]byte(jsonStream), &user)
    if err != nil {
        fmt.Println("error:", err)
    }
    fmt.Printf("%+v \n", user)
}

Output:

{Name:ethancai FansCount:9223372036854775807}

如果反序列化不指定结构体类型或者变量类型,则 json 中的数字类型,默认被反序列化成 float64 类型:

package main

import (
    "encoding/json"
    "fmt"
    "reflect"
)

func main() {
    const jsonStream = `{"name":"ethancai", "fansCount": 9223372036854775807}`
    // 不指定反序列化的类型
    var user interface{}
    err := json.Unmarshal([]byte(jsonStream), &user)
    if err != nil {
        fmt.Println("error:", err)
    }
    m := user.(map[string]interface{})
    fansCount := m["fansCount"]
    fmt.Printf("%+v \n", reflect.TypeOf(fansCount).Name())
    fmt.Printf("%+v \n", fansCount.(float64))
}

Output:

float64
9.223372036854776e+18

另一个程序

package main

import (
    "encoding/json"
    "fmt"
)

type User struct {
    Name      string
    // 不指定 FansCount 变量的类型
    FansCount interface{}
}

func main() {
    const jsonStream = `{"name":"ethancai", "fansCount": 9223372036854775807}`
    var user User
    err := json.Unmarshal([]byte(jsonStream), &user)
    if err != nil {
        fmt.Println("error:", err)
    }
    fmt.Printf("%+v \n", user)
}

Output:

{Name:ethancai FansCount:9.223372036854776e+18}

从上面的程序可以发现,如果 fansCount 精度比较高,反序列化成 float64 类型的数值时存在丢失精度的问题。

package main

import (
    "encoding/json"
    "fmt"
    "reflect"
    "strings"
)

func main() {
    const jsonStream = `{"name":"ethancai", "fansCount": 9223372036854775807}`
    decoder := json.NewDecoder(strings.NewReader(jsonStream))
    // UseNumber causes the Decoder to unmarshal a number into an interface{} as a Number instead of as a float64.
    decoder.UseNumber()
    var user interface{}
    if err := decoder.Decode(&user); err != nil {
        fmt.Println("error:", err)
        return
    }
    m := user.(map[string]interface{})
    fansCount := m["fansCount"]
    fmt.Printf("%+v \n", reflect.TypeOf(fansCount).PkgPath() + "." + reflect.TypeOf(fansCount).Name())
    v, err := fansCount.(json.Number).Int64()
    if err != nil {
        fmt.Println("error:", err)
        return
    }
    fmt.Printf("%+v \n", v)
}

Output:

encoding/json.Number
9223372036854775807

上面的程序,使用了 func (*Decoder) UseNumber 方法告诉反序列化 json 的数字类型的时候,不要直接转换成 float64,而是转换成 json.Number 类型。
json.Number 内部实现机制:

// A Number represents a JSON number literal.
type Number string

// String returns the literal text of the number.
func (n Number) String() string { return string(n) }

// Float64 returns the number as a float64.
func (n Number) Float64() (float64, error) {
    return strconv.ParseFloat(string(n), 64)
}

// Int64 returns the number as an int64.
func (n Number) Int64() (int64, error) {
    return strconv.ParseInt(string(n), 10, 64)
}

json.Number 本质是字符串,反序列化的时候将 json 的数值先转成 json.Number,其实是一种延迟处理的手段,待后续逻辑需要时候,再把 json.Number 转成 float64 或者 int64。

阅读全文

Go 处理 json


json to struct

import (
    "testing"
    "encoding/json"
)

// 这里对应的 N 和 A 不能为小写,首字母必须为大写,这样才可对外提供访问,具体 json 匹配是通过后面的 tag 标签进行匹配的,与 N 和 A 没有关系
// tag 标签中 json 后面跟着的是字段名称,都是字符串类型,要求必须加上双引号
type Person struct {
    N string     `json:"name"`
    A int        `json:"age"`
}

func TestStruct2Json(t *testing.T) {
    jsonStr := `{
        "name":"liangyongxing",
        "age":12
    }`
    var person Person
    json.Unmarshal([]byte(jsonStr), &person)
    t.Log(person)
}

阅读全文

Go sync.Map


image

sync.map 底层使用 map[interface{}]*entry 来做存储,所以无论 key 还是 value 都是支持多种数据类型。

package main

import (
    "fmt"
    "sync"
)

type MySyncMap struct {
    sync.Map
}

func (m MySyncMap) Print(k interface{}) {
    value, ok := m.Load(k)
    fmt.Println(value, ok)
}

func main() {
    var syncMap MySyncMap
    syncMap.Print("Key1")
    syncMap.Store("Key1", "Value1")
    syncMap.Print("Key1")
    syncMap.Store("Key2", "Value2")
    syncMap.Store("Key3", 2)
    syncMap.Print("Key3")
    syncMap.Store(4, 4)
    syncMap.Print(4)
    syncMap.Delete("Key1")
    syncMap.Print("Key1")
}
<nil> false
Value1 true
2 true
4 true
<nil> false

并发 hashmap 的方案有很多。

  1. 直接在不支持并发的 hashmap 上,使用一个读写锁的保护,这也是 sync map 还没出来前,常用的方法。这种方法的缺点是写会堵塞读。
  2. 分段锁,每一个读写锁保护一段区间。平均情况下这样的性能还挺好的,但是极端情况下,如果某个区间有热点写,那么那个区间的读请求也会受到影响。
  3. 使用使用链表法解决冲突,然后链表使用 CAS 去解决并发下冲突。

An overview of sync.Map 中有提到,在 cpu 核数很多的情况下,因为 cache contention,reflect.New、sync.RWMutex、atomic.AddUint32 都会很慢。

读写分离

使用了两个 map,一个叫 read,一个叫 dirty,两个 map 存储的都是指针,指向 value 数据本身,所以两个 map 是共享 value 数据的,更新 value 对两个 map 同时可见。

dirty 可以进行增删查,要进行加互斥锁。

read 中存在的 key,可以无锁的读,借助 CAS 进行无锁的更新、删除操作,但是不能新增 key,相当于 dirty 的一个 cache,由于 value 共享,所以能通过 read 对已存在的 value 进行更新。

sync.Map 中会记录 miss cache 的次数,当 miss 次数大于等于 dirty 元素个数时,就会把 dirty 变成 read,原来的 dirty 清空。

为了方便 dirty 直接变成 read,那么得保证 read 中存在的数据 dirty 必须有,所以在 dirty 是空的时候,如果要新增一个 key,那么会把 read 中的元素复制到 dirty 中,然后写入新 key。

删除操作使用的是延迟删除,优先看 read 中没有,read 中有,就把 read 中的对应 entry 指针中的 p 置为 nil,作为一个标记。在 read 中标记为 nil 的,只有在 dirty 提升为 read 时才会被实际删除。

// The zero Map is empty and ready for use. A Map must not be copied after first use.
type Map struct {
    mu Mutex

    // read contains the portion of the map's contents that are safe for
    // concurrent access (with or without mu held).
    //
    // The read field itself is always safe to load, but must only be stored with
    // mu held.
    //
    // Entries stored in read may be updated concurrently without mu, but updating
    // a previously-expunged entry requires that the entry be copied to the dirty
    // map and unexpunged with mu held.
    read atomic.Value // readOnly

    // dirty contains the portion of the map's contents that require mu to be
    // held. To ensure that the dirty map can be promoted to the read map quickly,
    // it also includes all of the non-expunged entries in the read map.
    //
    // Expunged entries are not stored in the dirty map. An expunged entry in the
    // clean map must be unexpunged and added to the dirty map before a new value
    // can be stored to it.
    //
    // If the dirty map is nil, the next write to the map will initialize it by
    // making a shallow copy of the clean map, omitting stale entries.
    dirty map[interface{}]*entry

    // misses counts the number of loads since the read map was last updated that
    // needed to lock mu to determine whether the key was present.
    //
    // Once enough misses have occurred to cover the cost of copying the dirty
    // map, the dirty map will be promoted to the read map (in the unamended
    // state) and the next store to the map will make a new dirty copy.
    misses int
}

// read的实际结构体
// readOnly is an immutable struct stored atomically in the Map.read field.
type readOnly struct {
    m       map[interface{}]*entry
    amended bool // true if the dirty map contains some key not in m.
}

// expunged is an arbitrary pointer that marks entries which have been deleted
// from the dirty map.
var expunged = unsafe.Pointer(new(interface{}))

// An entry is a slot in the map corresponding to a particular key.
type entry struct {
    // p points to the interface{} value stored for the entry.
    //
    // If p == nil, the entry has been deleted and m.dirty == nil.
    //
    // If p == expunged, the entry has been deleted, m.dirty != nil, and the entry
    // is missing from m.dirty.
    //
    // Otherwise, the entry is valid and recorded in m.read.m[key] and, if m.dirty
    // != nil, in m.dirty[key].
    //
    // An entry can be deleted by atomic replacement with nil: when m.dirty is
    // next created, it will atomically replace nil with expunged and leave
    // m.dirty[key] unset.
    //
    // An entry's associated value can be updated by atomic replacement, provided
    // p != expunged. If p == expunged, an entry's associated value can be updated
    // only after first setting m.dirty[key] = e so that lookups using the dirty
    // map find the entry.
    p unsafe.Pointer // *interface{}
}

image

mu 是用来保护 dirty 的互斥锁。

missed 是记录没命中read的次数。

注意对于 entry.p,有两个特殊值,一个是 nil,另一个是 expunged。

nil 代表的意思是,在read中被删除了,但是dirty中还在,所以能直接更新值。如果 dirty==nill 的特殊情况,下次写入新值时会复制。

expunged 代表数据在 ditry 中已经被删除了,更新值的时候要先把这个 entry 复制到 dirty。

Load 读取

// Load returns the value stored in the map for a key, or nil if no
// value is present.
// The ok result indicates whether value was found in the map.
func (m *Map) Load(key interface{}) (value interface{}, ok bool) {
    read, _ := m.read.Load().(readOnly)
    e, ok := read.m[key]
    if !ok && read.amended {
        m.mu.Lock()
        // Avoid reporting a spurious miss if m.dirty got promoted while we were
        // blocked on m.mu. (If further loads of the same key will not miss, it's
        // not worth copying the dirty map for this key.)
        read, _ = m.read.Load().(readOnly)
        e, ok = read.m[key]
        if !ok && read.amended {
            e, ok = m.dirty[key]
            // Regardless of whether the entry was present, record a miss: this key
            // will take the slow path until the dirty map is promoted to the read
            // map.
            m.missLocked()
        }
        m.mu.Unlock()
    }
    if !ok {
        return nil, false
    }
    return e.load()
}

func (e *entry) load() (value interface{}, ok bool) {
    p := atomic.LoadPointer(&e.p)
    if p == nil || p == expunged {
        return nil, false
    }
    return *(*interface{})(p), true
}

func (m *Map) missLocked() {
    m.misses++
    if m.misses < len(m.dirty) {
        return
    }
    m.read.Store(readOnly{m: m.dirty})
    m.dirty = nil
    m.misses = 0
}

读取时,先去 read 读取。如果没有,就加锁,然后去 dirty 读取,同时调用 missLocked(),再解锁。在m issLocked 中,会递增 misses 变量,如果 misses>len(dirty),那么把 dirty 提升为 read,清空原来的 dirty。

在代码中,我们可以看到一个 double check,检查 read 没有,上锁,再检查 read 中有没有,是因为有可能在第一次检查之后,上锁之前的间隙,dirty 提升为 read 了,这时如果不 double check,可能会导致一个存在的 key 却返回给调用方说不存在。

Store 写入

// Store sets the value for a key.
func (m *Map) Store(key, value interface{}) {
    read, _ := m.read.Load().(readOnly)
    if e, ok := read.m[key]; ok && e.tryStore(&value) {
        return
    }

    m.mu.Lock()
    read, _ = m.read.Load().(readOnly)
    if e, ok := read.m[key]; ok {
        if e.unexpungeLocked() {
            // The entry was previously expunged, which implies that there is a
            // non-nil dirty map and this entry is not in it.
            m.dirty[key] = e
        }
        e.storeLocked(&value)
    } else if e, ok := m.dirty[key]; ok {
        e.storeLocked(&value)
    } else {
        if !read.amended {
            // We're adding the first new key to the dirty map.
            // Make sure it is allocated and mark the read-only map as incomplete.
            m.dirtyLocked()
            m.read.Store(readOnly{m: read.m, amended: true})
        }
        m.dirty[key] = newEntry(value)
    }
    m.mu.Unlock()
}

// tryStore stores a value if the entry has not been expunged.
//
// If the entry is expunged, tryStore returns false and leaves the entry
// unchanged.
func (e *entry) tryStore(i *interface{}) bool {
    p := atomic.LoadPointer(&e.p)
    if p == expunged {
        return false
    }
    for {
        if atomic.CompareAndSwapPointer(&e.p, p, unsafe.Pointer(i)) {
            return true
        }
        p = atomic.LoadPointer(&e.p)
        if p == expunged {
            return false
        }
    }
}

func (m *Map) dirtyLocked() {
    if m.dirty != nil {
        return
    }

    read, _ := m.read.Load().(readOnly)
    m.dirty = make(map[interface{}]*entry, len(read.m))
    for k, e := range read.m {
        if !e.tryExpungeLocked() {
            m.dirty[k] = e
        }
    }
}

func (e *entry) tryExpungeLocked() (isExpunged bool) {
    p := atomic.LoadPointer(&e.p)
    for p == nil {
        if atomic.CompareAndSwapPointer(&e.p, nil, expunged) {
            return true
        }
        p = atomic.LoadPointer(&e.p)
    }
    return p == expunged
}

// unexpungeLocked ensures that the entry is not marked as expunged.
//
// If the entry was previously expunged, it must be added to the dirty map
// before m.mu is unlocked.
func (e *entry) unexpungeLocked() (wasExpunged bool) {
    return atomic.CompareAndSwapPointer(&e.p, expunged, nil)
}

写入的时候,先看 read 中能否查到 key,在 read 中存在的话,直接通过 read 中的 entry 来更新值。在 read 中不存在,那么就上锁,然后 double check。

double check 情况:

  1. double check 发现 read 中存在,如果是 expunged,那么就先尝试把 expunged 替换成 nil,最后如果 entry.p==expunged 就复制到 dirty 中,再写入值。否则不用替换直接写入值。
  2. dirty 中存在,直接更新。dirty 中不存在,如果此时 dirty 为空,那么需要将 read 复制到 dirty 中,最后再把新值写入到 dirty 中。复制的时候调用的是 dirtyLocked(),在复制到 dirty 的时候,read 中为 nil 的元素,会更新为 expunged,并且不复制到 dirty 中。

在更新 read 中的数据时,使用的是 tryStore,通过 CAS 来解决冲突,在 CAS 出现冲突后,如果发现数据被置为 expung,tryStore 那么就不会写入数据,而是会返回 false,在 Store 流程中,就是接着往下走,在 dirty 中写入。

double check 的时候,在 read 中存在,那么就是说在加锁之前,有并发线程先写入了 key,然后由 Load 触发了 dirty 提升为 read,这时 dirty 可能为空,也可能不为空,但无论 dirty 状态如何,都是可以直接更新 entry.p。如果是 expunged 的话,那么要先替换成 nil,再复制 entry 到 dirty 中。

Delete 删除

// Delete deletes the value for a key.
func (m *Map) Delete(key interface{}) {
    read, _ := m.read.Load().(readOnly)
    e, ok := read.m[key]
    if !ok && read.amended {
        m.mu.Lock()
        read, _ = m.read.Load().(readOnly)
        e, ok = read.m[key]
        if !ok && read.amended {
            delete(m.dirty, key)
        }
        m.mu.Unlock()
    }
    if ok {
        e.delete()
    }
}

func (e *entry) delete() (hadValue bool) {
    for {
        p := atomic.LoadPointer(&e.p)
        if p == nil || p == expunged {
            return false
        }
        if atomic.CompareAndSwapPointer(&e.p, p, nil) {
            return true
        }
    }
}

删除很简单,read 中存在,就把 read 中的 entry.p 置为 nil,如果只在 ditry 中存在,那么就直接从 dirty 中删掉对应的 entry。

阅读全文

Go Map 原理


map 的底层实现是一个散列表,因此实现 map 的过程实际上就是实现散表的过程。

在这个散列表中,主要出现的结构体有两个,一个叫 hmap(a header for a go map),一个叫 bmap(a bucket for a Go map,通常叫其 bucket)。

这两种结构的样子分别如下所示:

hmap:

image

只需要关心的只有一个,就是标红的字段 buckets 数组。map 中用于存储的结构是 bucket 数组。而 bucket(bmap) 的结构是怎样的呢?

bucket:

image

相比于 hmap,bucket 的结构显得简单一些,标红的字段依然是核心,我们使用的 map 中的 key 和 value 就存储在这里。高位哈希值数组记录的是当前 bucket 中 key 相关的索引,稍后会详细叙述。还有一个字段是一个指向扩容后的 bucket 的指针,使得 bucket 会形成一个链表结构。

image

由此看出 hmap 和 bucket 的关系是这样的:

image

而 bucket 又是一个链表。

image

哈希表的特点是会有一个哈希函数,对传来的 key 进行哈希运算,得到唯一的值,一般情况下都是一个数值。map 中也有这么一个哈希函数,也会算出唯一的值。

Go 把求得的值按照用途一分为二:高位和低位。

image

蓝色为高位,红色为低位。

然后低位用于寻找当前 key 属于 hmap 中的哪个 bucket,而高位用于寻找 bucket 中的哪个 key。上文中提到,bucket 中有个属性字段是高位哈希值数组,这里存的就是蓝色的高位值,用来声明当前 bucket 中有哪些 key,便于搜索查找。

需要特别指出的一点是,map 中的 key/value 值都是存到同一个数组中的。

image

并不是 key0/value0/key1/value1 的形式,这样做的好处是在 key 和 value 的长度不同的时候,可以消除 padding 带来的空间浪费。

map整个的结构图

image

map的扩容

当以上的哈希表增长的时候,Go 语言会将 bucket 数组的数量扩充一倍,产生一个新的 bucket 数组,并将旧数组的数据迁移至新数组。

加载因子

判断扩充的条件,就是哈希表中的加载因子(loadFactor)。

加载因子是一个阈值,一般表示为:散列包含的元素数 除以 位置总数。是一种 产生冲突机会空间使用 的平衡与折中。

加载因子越小,说明空间空置率高,空间使用率小,但是加载因子越大,说明空间利用率上去了,但是“产生冲突机会”高了。

每种哈希表的都会有一个加载因子,数值超过加载因子就会为哈希表扩容。

加载因子公式

map长度 / 2^B

阈值是 6.5。其中 B 可以理解为已扩容的次数。

当 map 长度增长到大于加载因子所需的 map 长度时,就会将产生一个新的 bucket 数组,然后把旧的 bucket 数组移到一个属性字段 oldbucket 中。

注意:并不是立刻把旧的数组中的元素转义到新的 bucket 当中,而是只有当访问到具体的某个 bucket 的时候,才会把 bucket 中的数据转移到新的 bucket 中。

当扩容的时候,map 结构体中会保存旧的数据,和新生成的数组。

image

上面部分代表旧的有数据的 bucket,下面部分代表新生成的新的 bucket。蓝色代表存有数据的 bucket,橘黄色代表空的 bucket。

image

注意:这里并不会直接删除旧的 bucket,而是把原来的引用去掉,利用 GC 清除内存。

map 中数据的删除

  1. 如果 key 是一个指针类型的,则直接将其置为空,等待 GC 清除。
  2. 如果是值类型的,则清除相关内存。
  3. 同理,对 value 做相同的操作。
  4. 最后把 key 对应的高位值对应的数组 index 置为空。
阅读全文

Go Map


先声明 map

var m1 map[string]string

再使用 make 函数创建一个非 nil 的 map,nil map 不能赋值。

make 函数会分配和初始化一个哈希 map 数据结构,并返回一个指向该哈希 map 的映射变量。相关数据结构细节由运行时实现,并且不因语言本身所规定。

m1 = make(map[string]string)

最后给已声明的 map 赋值

m1["a"] = "aa"
m1["b"] = "bb"

阅读全文

Go 比较两个 Slice 是否相等


开发中经常会遇到需要比较两个 slice 包含的元素是否完全相等的情况,一般来说有两个思路:

  • reflect 比较的方法
  • 循环遍历比较的方法

reflect 比较的方法

func StringSliceReflectEqual(a, b []string) bool {
    return reflect.DeepEqual(a, b)
}

这个写法很简单,就是直接使用 reflect 包的 reflect.DeepEqual 方法来比较 a 和 b 是否相等。

阅读全文

Go Array & Slice 底层实现


Array

Go 语言的数组不同于 C 语言或者其他语言的数组,C 语言的数组变量是指向数组第一个元素的指针;而 Go 语言的数组是一个值,Go 语言中的数组是值类型,一个数组变量就表示着整个数组,意味着 Go 语言的数组在传递的时候,传递的是原数组的拷贝。你可以理解为 Go 语言的数组是一种有序的 struct。

func testArray(x [2]int) {
    fmt.Printf("func Array : %p , %v\n", &x, x)
}
    
func main() {
    arrayA := [2]int{100, 200}
    var arrayB [2]int
    arrayB = arrayA
    fmt.Printf("arrayA : %p , %v\n", &arrayA, arrayA)
    fmt.Printf("arrayB : %p , %v\n", &arrayB, arrayB)
    testArray(arrayA)
}

打印结果:

arrayA : 0xc4200bebf0 , [100 200]
arrayB : 0xc4200bec00 , [100 200]
func Array : 0xc4200bec30 , [100 200]

阅读全文

Go Array & Slice


数组

几乎所有计算机语言,数组的实现都是相似的:一段连续的内存,Go 语言也一样,Go 语言的数组底层实现就是一段连续的内存空间。每个元素有唯一一个索引(或者叫下标)来访问。

如下图所示,下图是 [5]int{1:10, 2:20} 数组的内部实现逻辑图:

image

由于内存连续,CPU 很容易计算索引(即数组的下标),可以快速迭代数组里的所有元素。

slice

切片是一个很小的对象,是对数组进行了抽象,并提供相关的操作方法。切片有三个属性字段:长度、容量和指向数组的指针。严格地讲,切片有容量(capacity)和长度(length)两个属性。

切片有两种定义方式,一种是先声明一个变量是切片,然后使用内置函数 make 去初始化这个切片。另外一种是通过取数组切片来赋值。

package main
    
import (
    "fmt"
)
    
func main() {
    var x = make([]float64, 5)
    fmt.Println("Capcity:", cap(x), "Length:", len(x))
    var y = make([]float64, 5, 10)
    fmt.Println("Capcity:", cap(y), "Length:", len(y))
    for i := 0; i < len(x); i++ {
        x[i] = float64(i)
    }
    fmt.Println(x)
    for i := 0; i < len(y); i++ {
        y[i] = float64(i)
    }
    fmt.Println(y)
}

输出结果为

Capcity: 5 Length: 5
Capcity: 10 Length: 5
[0 1 2 3 4]
[0 1 2 3 4]

上面我们首先用 make 函数定义切片 x,这个时候 x 的容量是 5,长度也是 5。然后使用 make 函数定义了切片 y,这个时候 y 的容量是 10,长度是 5。然后我们再分别为切片 x 和 y 的元素赋值,最后输出。

所以使用 make 函数定义切片的时候,有两种方式,一种只指定长度,这个时候切片的长度和容量是相同的。另外一种是同时指定切片长度和容量。虽然切片的容量可以大于长度,但是赋值的时候要注意最大的索引仍然是 len(x)-1。否则会报索引超出边界错误。

另外一种是通过数组切片赋值,采用 [low_index:high_index] 的方式获取数值切片,其中切片元素包括 low_index 的元素,但是不包括 high_index 的元素。

package main
    
import (
    "fmt"
)
    
func main() {
    var arr1 = [5]int{1, 2, 3, 4, 5}
    var s1 = arr1[2:3]
    var s2 = arr1[:3]
    var s3 = arr1[2:]
    var s4 = arr1[:]
    fmt.Println(s1)
    fmt.Println(s2)
    fmt.Println(s3)
    fmt.Println(s4)
}

输出结果为

[3]
[1 2 3]
[3 4 5]
[1 2 3 4 5]

在上面的例子中,我们还省略了 low_index 或 high_index。

如果省略了 low_index,那么等价于从索引 0 开始;如果省略了 high_index,则默认 high_index 等于 len(arr1),即切片长度。

这里为了体现切片的长度可以变化,我们看一下下面的例子:

package main
    
import (
    "fmt"
)
    
func main() {
    var arr1 = make([]int, 5, 10)
    for i := 0; i < len(arr1); i++ {
        arr1[i] = i
    }
    fmt.Println(arr1)
    arr1 = append(arr1, 5, 6, 7, 8)
    fmt.Println("Capacity:", cap(arr1), "Length:", len(arr1))
    fmt.Println(arr1)
}

输出结果为

[0 1 2 3 4]
Capacity: 10 Length: 9
[0 1 2 3 4 5 6 7 8]

image

上图中,ptr 指的是指向 array 的 pointer,len 是指切片的长度, cap 指的是切片的容量。

对于s := make([]byte, 5) 和 s := []byte{...} 的方式

image

对于 s = s[2:4] 的方式

image

对于 nil 的切片即 var s []byte 对应的逻辑图是

image

在此有一个说明:nil 切片和空切片是不太一样的,空切片即 s := make([]byte, 0) 或者 s := []byte{} 出来的切片
空切片的逻辑图为:

image

空切片指针不为 nil,而 nil 切片指针为 nil。但是,不管是空切片还是 nil 切片,对其调用内置函数 append()、len 和 cap 的效果都是一样的,感受不到任何区别。

扩容

这里我们初始化 arr1 为容量 10,长度为 5 的切片,然后为前面的 5 个元素赋值。然后输出结果。然后我们再使用 Go 内置方法 append 来为 arr1 追加四个元素,这个时候再看一下 arr1 的容量和长度以及切片元素,我们发现切片的长度确实变了。

另外我们再用 append 方法给 arr1 多追加几个元素,试图超过 arr1 原来定义的容量大小。

package main
    
import (
    "fmt"
)
    
func main() {
    var arr1 = make([]int, 5, 10)
    for i := 0; i < len(arr1); i++ {
        arr1[i] = i
    }
    arr1 = append(arr1, 5, 6, 7, 8, 9, 10)
    fmt.Println("Capacity:", cap(arr1), "Length:", len(arr1))
    fmt.Println(arr1)
}

输出结果为

Capacity: 20 Length: 11
[0 1 2 3 4 5 6 7 8 9 10]

我们发现 arr1 的长度变为 11,因为元素个数现在为 11 个。另外我们发现 arr1 的容量也变了,变为原来的两倍。这是因为 Go 在默认的情况下,如果追加的元素超过了容量大小,Go 会自动地重新为切片分配容量,容量大小为原来的两倍。

上面我们介绍了,可以使用 append 函数给切片增加元素,现在我们再来介绍一个 copy 函数用来从一个切片拷贝元素到另一个切片。

package main
    
import (
    "fmt"
)
    
func main() {
    slice1 := []int{1, 2, 3, 4, 5, 6}
    slice2 := make([]int, 5, 10)
    copy(slice2, slice1)
    fmt.Println(slice1)
    fmt.Println(slice2)
}

输出结果

[1 2 3 4 5 6]
[1 2 3 4 5]

在上面的例子中,我们将 slice1 的元素拷贝到 slice2,因为 slice2 的长度为 5,所以最多拷贝 5 个元素。

应用

package main

import (
    "fmt"
    "reflect"
)

func main() {
    // index
    arrayA := [...]int{0: 1, 2: 1, 3: 4}
    fmt.Println(arrayA)

    // type
    arrayB := [...]int{1, 2, 3}
    arrayC := [...]int{1, 2, 3, 4}
    fmt.Println(reflect.TypeOf(arrayB))
    fmt.Println(reflect.TypeOf(arrayC))
    fmt.Println(reflect.TypeOf(arrayB) == reflect.TypeOf(arrayC))

    array := [4]int{10, 20, 30, 40}
    slice := array[0:2]
    fmt.Println(slice)

    newSlice := append(append(append(slice, 50), 100), 150)
    fmt.Println(newSlice)

    newSlice[1] = newSlice[1] + 1
    fmt.Println(newSlice)

    sliceInt := []int{1, 2, 3, 4, 5, 6}
    newSliceInt := append(sliceInt, 1)
    fmt.Println(newSliceInt)
}
r := [...]int{99:-1}

定义了一个含有 100 个元素的数组 r,最后一个元素输出化为 -1,其他的元素都是用 0 初始化。

阅读全文