ChatGPT最佳实践和使用场景收集

ChatGPT最佳实践

怎么与ChaGPT对话能让它更好地理解你的意图,然后给出精彩的回答?以下工具可能可以帮到你:

ChatGPT使用场景收集

ChatGPT+Stable Diffusion画出文字描述的场景

刚才试了一下用 #stablediffusion 画版画,用到了一个很少人用的 LoRA,先让 ChatGPT 帮我想场景描述,然后直接复制到 SD 里开始画。可以转行当个版画大师了,看看这几幅画能卖出去多少钱。

from:https://twitter.com/decohack/status/1629078910947430400 @viggo

使用ChatGPT写Python爬虫

如何用 ChatGPT 帮你写 Python 爬虫?实际样例循序渐进手把手教程https://www.youtube.com/watch?v=jh-yX6KwIlY

使用ChatGPT做主题挖掘

如何用 ChatGPT 帮你做主题挖掘?自动 Python 编程 LDA 可视化 https://www.youtube.com/watch?v=jh-yX6KwIlY

ChatGPT角色扮演

https://github.com/f/awesome-chatgpt-prompts

https://www.youtube.com/watch?v=oF74vvgq4Kc

小爱同学+ChatGPT

https://github.com/yihong0618/xiaogpt

基于ChatGPT API的文本翻译、文本润色、语法纠错Bob插件

Bob是一款macOS平台翻译和OCR软件。

基于ChatGPT API的文本翻译、文本润色、语法纠错Bob插件:

https://github.com/yetone/bob-plugin-openai-translator

此插件已支持使用ChatGPT API对句子进行润色和语法修改,只需要把目标语言选成跟源语言一样即可,全面替代Grammarly!而且理论上任何语言都可以润色,不只是英语。

ChatPDF

地址:https://www.chatpdf.com

在网站上传 PDF 文件后,就可以对上传的PDF文件内容向ChatGPT提任何关于此本书的问题,包括快速生成PDF的内容摘要,结果支持输出为中文。

类似的应用还有:https://github.com/mckaywrigley/paul-graham-gpt

这个应用是将Paul Graham的一些论文整理成CSV文件,然后通过ChatGPT的 OpenAI Embeddings 接口提交给ChatGPT,然后可以向ChatGPT询问关于Paul Graham论文的问题。

这两个应用代表了ChatGPT最有用但刚刚开始的应用场景:让ChatGPT针对垂直行业知识学习,成为你真正的私人学习助理、工作助理。。。

例如: 如果给ChatGPT输入的是某个专业的教材(例如来自 https://openstax.org),配合ChatGPT的翻译能力、Wisper接口能力,那就得到了一个精通某个专业、多门语言的私人教师。

语言翻译及润色

原来用DeepL来做翻译,用DeepL Write(或Grammarly)来做语法润色和修改,已经觉得很好用了。

使用ChatGPT后,明显感觉用ChatGPT来做翻译和润色比DeepL和DeepL Write更加好用。但由于在交互界面上使用很不方便。

ChatGPT开放API几天,接连出现了几个此类应用。

OpenAI Translator

基于 ChatGPT API 的文本翻译、文本润色、语法纠错 Bob 插件

使用ChatGPT翻译制作双语电子书

将外文电子书翻译成双语对照版本,并在任何设备上阅读

http://editgpt.app是翻译类浏览器插件,改善了以往 grammarly 擅长的语法校对、改善书面表达功能。以我的弱鸡英文写作能力和中上英文阅读能力,我自己测试下来效果挺好的,可能跟美国人写的还有点不同(指习惯,不是说写作能力),但已经够强大了。

可以说,开放API后,ChatGPT已经可以取代目前大部分的翻译了。对比一下中信、机械工业等出版社每一年出版的国外图书的中文版的翻译质量,ChatGPT已经完全超出几条街。

生成内容摘要

glarity算是最近几天发现的一个宝贝,可以生成Google搜索摘要、Youtube视频摘要。

地址:https://glarity.app

主要功能:

为Google 搜索结果生成摘要。

基于 ChatGPT 和 Youtube 字幕生成视频摘要。类似的chrome扩展还有 Glasp,但Glarity更还用。 还有一个 Language Reactor ,能够实时生成Youtube等视频站的字幕,虽然没有ChatGPT加持,但强烈推荐。

好用的AI工具软件收集

让你失业的不一定是AI,但一定是比你更早熟练掌握AI的人。最近体验AI真是大开眼界,文字,图片,设计,剪辑,配音无一不有,甚至还有色图生成工具,不禁为福利姬感到深深的担忧

整理了一批AI实用工具,图文,新媒体,编程等均有涉及,一句话:千万别错过,有工作的来练习技能,自由的来体验科技进步。

写作类

http://copy.ai

https://copymatic.ai

图片类

https://openai.com/dall-e-2/

https://deepai.org

https://lexica.art

https://stablediffusionweb.com

Stable Diffusion是2022年发布的深度学习文生图模型。它主要用于根据文本的描述产生详细图像,尽管它也可以应用于其他任务,如内补绘制、外补绘制,以及在提示词指导下产生图生图的翻译。

Scribble Diffusion:https://scribblediffusion.com/ GitHub地址:https://github.com/replicate/scribble-diffusion

Scribble Diffusion,一款开源涂鸦AI绘画工具,能够将草图变成精致的图像,基于Replicate 提供的API来实现的,使用ControlNet来固定图形布局,使用比较简单,直接在草粗画图,然后配上描述,点击go/去即可生成一幅精致的图片了,喜欢的可以在线体验试试。

图标/UI设计

https://logo.com

https://lordicon.com

http://uiverse.io

全世界第一个使用自然语言/文字进行 UI 设计的设计工具Galileo。UI 设计师们最担心的一刻提前到来了。以前的Midjourney们都擅长美术创作,画到UI时只能输出效果图,没法得到设计稿。看起来Galileo能逐步生成真实UI设计稿。排队等试用地址:http://useGalileo.ai

视频/剪辑

http://runwayml.com

http://veed.io

配音

http://murf.ai

https://typecast.ai

编程

http://tabnine.com

https://beta.openai.com/examples

小工具

智能取名:https://namelix.com

福利姬杀手(仅做研究用):https://pornpen.ai

AI工具软件聚合网站

https://gpt3demo.com/map

https://futuretools.io/?tags-n5zn=finance…

https://futurepedia.io

https://allthingsai.com

https://www.ainavpro.com/

https://www.vondy.com/explore/shubham

https://orelmizrahii.github.io/Web-AI-Archive/thelist.html

https://theresanaiforthat.com/

https://saasaitools.com/

https://www.futurepedia.io/

https://ai-toolbox.codefuture.top/

https://17yongai.com/

JSON和Go

本文翻译自《JSON and Go》。

Andrew Gerrand

2011/01/25

介绍

JSON(JavaScript对象表示法)是一种简单的数据交换格式。在语法上,它类似于JavaScript的对象和列表。它最常用于web后端和浏览器中运行的JavaScript程序之间的通信,但也用于许多其他地方。它的主页json.org提供了一个非常清晰和简洁的标准定义。

使用json包,从Go程序读取和写入json数据很简单。

编码

为了编码JSON数据,我们使用Marshal函数。

func Marshal(v interface{}) ([]byte, error)

给定Go结构体Message

type Message struct {
    Name string
    Body string
    Time int64
}

Message的一个实例:

m := Message{"Alice", "Hello", 1294706395881547000}

我们可以使用json.Marshal函数得到m的JSON编码版本:

b, err := json.Marshal(m)

如果一切顺利,err将为nilb将是包含此JSON数据的[]byte

b == []byte(`{"Name":"Alice","Body":"Hello","Time":1294706395881547000}`)

只有可以表示为有效JSON的数据结构才可以被编码:

  • JSON对象只支持字符串作为键;要编码Go的map类型,它必须是map[string]T格式(其中T是json包支持的任何Go类型)。
  • 无法对通道、复数和函数类型进行编码。
  • 不支持循环数据结构;他们会使Marshal陷入无限循环。
  • 指针将被编码为它们指向的值(如果指针为空,则编码为“null”)。

json包只访问结构体类型的导出字段(以大写字母开头的字段)。因此,只有结构体的导出字段才会出现在JSON的输出中。

解码

要解码JSON数据,我们使用Unmarshal函数。

func Unmarshal(data []byte, v interface{}) error

我们必须首先创建一个存储解码数据的变量:

var m Message

并调用json.Unmarshal,将一个[]byte的JSON数据和一个指向m的指针传递给它:

err := json.Unmarshal(b, &m)

如果b包含适合m的有效JSON,则在调用之后err将为nil,并且来自b的数据将存储在结构体m中,就像通过如下赋值一样:

m = Message{
    Name: "Alice",
    Body: "Hello",
    Time: 1294706395881547000,
}

Unmarshal如何识别存储解码数据的字段?对于给定的JSON键“Foo”,Unmarshal将查看目标结构体的字段来查找(按以下优先级顺序):

  • 带有“Foo”标签(tag)的导出(公有)字段(有关结构体标签的更多信息,请参阅Go语言规范),
  • 名为“Foo”的导出字段,或
  • 其他“Foo”单词的不区分大小写的匹配项,例如名为“FOO”或“FoO”的导出字段。

当JSON数据的结构与Go类型不完全匹配时会发生什么?

b := []byte(`{"Name":"Bob","Food":"Pickle"}`)
var m Message
err := json.Unmarshal(b, &m)

Unmarshal将只解码它可以在目标类型中找到的字段。在这种情况下,只会填充mName字段,而忽略Food字段。当你希望从大型JSON数据中仅选择几个特定字段时,此行为特别有用。这也意味着目标结构体中任何未导出(私有)的字段都不会受到Unmarshal的影响。

但是,如果你事先不知道JSON数据的结构怎么办?

带接口的通用JSON

interface{}(空接口)类型描述了一个具有零个方法(没有一个方法)的接口。每个Go类型都至少实现了零个方法,因此都实现了空接口。

空接口可以用作通用的容器类型:

var i interface{}
i = "a string"
i = 2011
i = 2.777

类型断言访问底层的具体类型:

r := i.(float64)
fmt.Println("the circle's area", math.Pi*r*r)

或者,如果底层类型未知,则可以使用switch语句来确定类型:

switch v := i.(type) {
case int:
    fmt.Println("twice i is", v*2)
case float64:
    fmt.Println("the reciprocal of i is", 1/v)
case string:
    h := len(v) / 2
    fmt.Println("i swapped by halves is", v[h:]+v[:h])
default:
    // i的类型不是以上类型中的一种
}

json包使用map[string]interface{}[]interface{}值来存储任意JSON对象或数组;它会愉快地将任何有效的JSON blob解码为一个普通的interface{}值。interface{}值默认使用的底层Go类型是:

  • bool用于JSON布尔值,
  • float64用于JSON数字值,
  • string用于JSON字符串值,以及
  • nil用于JSON的null(空值)。

解码任意数据

考虑这个存储在变量b中的JSON数据:

b := []byte(`{"Name":"Wednesday","Age":6,"Parents":["Gomez","Morticia"]}`)

在不知道此数据的内部结构的情况下,我们可以使用Unmarshal将其解码为interface{}值:

var f interface{}
err := json.Unmarshal(b, &f)

此时,f中的Go值将是一个map,其键为字符串,其值存储为空接口interface{}值:

f = map[string]interface{}{
    "Name": "Wednesday",
    "Age":  6,
    "Parents": []interface{}{
        "Gomez",
        "Morticia",
    },
}

要访问此数据,我们可以使用类型断言来访问f的底层map[string]interface{}

m := f.(map[string]interface{})

然后我们可以使用range语句遍历这个map,并使用switch语句来判断其值的具体类型:

for k, v := range m {
    switch vv := v.(type) {
    case string:
        fmt.Println(k, "is string", vv)
    case float64:
        fmt.Println(k, "is float64", vv)
    case []interface{}:
        fmt.Println(k, "is an array:")
        for i, u := range vv {
            fmt.Println(i, u)
        }
    default:
        fmt.Println(k, "is of a type I don't know how to handle")
    }
}

通过这种方式,你可以使用未知内部结构的JSON数据,同时仍然享受类型安全的好处。

引用类型

让我们定义一个Go类型来包含上一个示例中的数据:

type FamilyMember struct {
    Name    string
    Age     int
    Parents []string
}

var m FamilyMember
err := json.Unmarshal(b, &m)

将该数据解码为FamilyMember值按预期工作,但如果我们仔细观察,我们会发现发生了一件了不起的事情。通过var语句,我们分配了一个FamilyMember结构体,然后将指向该值的指针提供给Unmarshal函数,但此时Parents字段是一个nil切片值。为了填充Parents字段,Unmarshal函数在幕后分配了一个新切片。这是Unmarshal解码它支持的引用类型(指针、切片和映射)的典型方式。

考虑解码到这个数据结构中:

type Foo struct {
    Bar *Bar
}

如果JSON中有一个Bar字段,Unmarshal函数将会分配一个新的Bar实例并填充它。否则,Bar将被保留为nil指针。

由此产生了一个有用的模式:如果你有一个接收几种不同消息类型的应用程序,你可以定义“接收者”结构,例如

type IncomingMessage struct {
    Cmd *Command
    Msg *Message
}

发送方可以填充JSON对象的Cmd字段和/或Msg字段,具体取决于他们想要传达的消息类型。Unmarshal函数在将JSON解码为IncomingMessage结构时,将仅分配JSON数据中存在的那个数据结构。具体要处理哪种消息,程序员只需测试CmdMsg是否不为nil。

数据流的编码器和解码器

json包提供了DecoderEncoder类型来支持读写JSON数据流的操作。NewDecoderNewEncoder函数包装了io.Readerio.Writer接口类型。

func NewDecoder(r io.Reader) *Decoder
func NewEncoder(w io.Writer) *Encoder

下面是一个示例程序,它从标准输入流中读取一系列JSON对象,从每个对象中删除除了Name字段以外的所有字段,然后将对象写入标准输出流:

package main

import (
    "encoding/json"
    "log"
    "os"
)

func main() {
    dec := json.NewDecoder(os.Stdin)
    enc := json.NewEncoder(os.Stdout)
    for {
        var v map[string]interface{}
        if err := dec.Decode(&v); err != nil {
            log.Println(err)
            return
        }
        for k := range v {
            if k != "Name" {
                delete(v, k)
            }
        }
        if err := enc.Encode(&v); err != nil {
            log.Println(err)
        }
    }
}

由于io.Readerio.Writer的广泛使用,这些DecoderEncoder类型可以用于广泛的场景,例如读取和写入HTTP连接、WebSocket或文件等。

参考

有关详细信息,请参阅json包的文档。有关json的示例用法,请参阅jsonrpc包的源文件。

Go映射(map)实战

本文翻译自《Go maps in action》。

Andrew Gerrand

2013/02/06

介绍

哈希表是计算机科学中最有用的数据结构之一。许多哈希表的实现具有不同的属性,但通常它们都提供快速查找、添加和删除这些功能。Go提供了实现哈希表的内置的map类型。

定义和初始化

Go的map类型如下所示:

map[KeyType]ValueType

其中KeyType可以是任何可比较的类型(稍后将详细介绍),ValueType可以是任何类型,包括另一个map!

此变量m是字符串键到int值的映射:

var m map[string]int

map类型是引用类型,类似指针或切片,因此上面的m值为nil;它不指向已初始化的map。读取时,nil map的行为类似于空map,但尝试写入nil map会导致运行时panic;不要那样做。初始化一个map,请使用内置的make函数:

m = make(map[string]int)

make函数分配并初始化map数据结构,并返回指向它的map值。该数据结构在运行时的实现细节,不由语言本身决定。在本文中,我们将关注map的使用,而不是它们的实现。

使用map

Go提供了一种熟悉的语法来处理map。以下语句将键“route”设置为值66

m["route"] = 66

以下语句检索存储在键“route”下的值并将其赋值给新变量i

i := m["route"]

如果请求的键不存在,我们将获得值的类型的零值。在本例的情况下,值类型是int,因此零值是0

j := m["root"]
// j == 0

内置的len函数返回map中的元素的个数:

n := len(m)

内置的delete函数从map中删除一个元素:

delete(m, "route")

delete函数不返回任何内容,如果指定的键不存在,就什么也不做。

双值赋值运算可以测试键是否存在:

i, ok := m["route"]

在此语句中,第一个值i被赋予存储在键“route”下的值。如果该键不存在,则i是值类型的零值0。第二个值ok是一个布尔值,如果键存在于map中则为真,否则为假。

要在不检索值的情况下测试键是否存在,可以使用下划线来省略第一个返回值:

_, ok := m["route"]

要遍历map的内容,请使用range关键字:

for key, value := range m {
    fmt.Println("Key:", key, "Value:", value)
}

要使用一些数据初始化一个map,请使用map字面量:

commits := map[string]int{
    "rsc": 3711,
    "r":   2138,
    "gri": 1908,
    "adg": 912,
}

可以使用以下语法来初始化一个空map,这在功能上与使用make函数相同:

m = map[string]int{}

利用零值

当键不存在时,检索map返回零值是很有用的一个特性。

例如,map里的布尔值可以用作类似集合的数据结构(回想一下,布尔类型的零值为false)。此示例遍历链接列表的Nodes并打印它们的值。它使用Node指针的map来检测列表中的循环。

    type Node struct {
        Next  *Node
        Value interface{}
    }
    var first *Node

    visited := make(map[*Node]bool)
    for n := first; n != nil; n = n.Next {
        if visited[n] {
            fmt.Println("cycle detected")
            break
        }
        visited[n] = true
        fmt.Println(n.Value)
    }

如果n已被访问,表达式visited[n]为true,如果n不存在则为false。无需使用二值形式来测试map中是否存在n;默认返回零值已经够用了。

另一个有用的零值实例是map的切片值。append到一个nil切片会分配一个新的切片,所以把一个值append到一个map的切片值无需检查键是否存在。在以下示例中,切片people填充了Person值。每个Person都有一个Name字段和一个Likes切片字段。该示例创建了一个map,将每个爱好(作为likes的键)与喜欢它的那些人(作为likes的值)相关联。

    type Person struct {
        Name  string
        Likes []string
    }
    var people []*Person

    likes := make(map[string][]*Person)
    for _, p := range people {
        for _, l := range p.Likes {
            likes[l] = append(likes[l], p)
        }
    }

打印出所有喜欢奶酪的人:

for _, p := range likes["cheese"] {
        fmt.Println(p.Name, "likes cheese.")
    }

打印出喜欢培根的人数:

fmt.Println(len(likes["bacon"]), "people like bacon.")

请注意,由于rangelen都将nil切片视为零长度切片,因此即使没有人喜欢奶酪或培根(尽管不太可能),最后两个示例仍然能正常工作。

键的类型

如前所述,map的键可以是任何可比较的类型。Go语言规范对此进行了精确定义,但简而言之,可比较类型是布尔类型、数字类型、字符串类型、指针类型、通道类型和接口类型,以及仅包含这些类型的结构体或数组。值得注意的是,没有切片、map和函数,这些类型不能使用==进行比较,因此不能用作map的键。

显然,字符串、整数和其他基本类型应该可以用作map的键,但可能出乎意料的是结构体作为map的键。结构体可以从多个维度作为键。例如,以下map可用于按国家/地区统计网页点击率:

hits := make(map[string]map[string]int)

这个map的键是字符串类型,值是另一个map(字符串到整数的映射)类型。外部map的每个键是网页的路径。内部map的每个键都是两个字母的国家/地区代码。此表达式检索澳大利亚人加载某个网页的次数:

n := hits["/doc/"]["au"]

不幸的是,这种方法在添加数据时并不灵活,对于任何给定的外部键,你必须检查内部map是否存在,并在需要时创建它:

func add(m map[string]map[string]int, path, country string) {
    mm, ok := m[path]
    if !ok {
        mm = make(map[string]int)
        m[path] = mm
    }
    mm[country]++
}
add(hits, "/doc/", "au")

我们可以使用带有结构体键的单个map的设计来消除所有的复杂性:

type Key struct {
    Path, Country string
}
hits := make(map[Key]int)

当越南人访问主页时,增加(并可能创建)适当的计数器,使用一行代码就能实现:

hits[Key{"/", "vn"}]++

同样,看看有多少瑞士人看过/ref/spec网页:

n := hits[Key{"/ref/spec", "ch"}]

并发

map对于并发使用是不安全的:Go没有定义当你同时读取和写入它们时会发生什么。如果你需要从并发执行的goroutine读取和写入map,则访问必须通过某种同步机制进行调解。保护map的一种常见方法是使用sync.RWMutex

此语句声明一个counter变量,它是一个包含map和内嵌sync.RWMutex的匿名结构体。

var counter = struct{
    sync.RWMutex
    m map[string]int
}{m: make(map[string]int)}

要从counter读取,请获取读锁:

counter.RLock()
n := counter.m["some_key"]
counter.RUnlock()
fmt.Println("some_key:", n)

要写入counter,请获取写锁:

counter.Lock()
counter.m["some_key"]++
counter.Unlock()

迭代顺序

使用range循环遍历map时,Go语言没有指定迭代顺序,并且不保证从一次迭代到下一次迭代是相同顺序的。如果你需要稳定的迭代顺序,则必须维护一个单独的数据结构来指定该顺序。以下例子使用单独排序的键切片,来按键在切片里的顺序打印输出map[int]string

import "sort"

var m map[int]string
var keys []int
for k := range m {
    keys = append(keys, k)
}
sort.Ints(keys)
for _, k := range keys {
    fmt.Println("Key:", k, "Value:", m[k])
}

复杂性守恒定律 (The Law of Conservation of Complexity or Tesler’s Law)

复杂性守恒定律 (The Law of Conservation of Complexity or Tesler’s Law):系统中存在着一定程度的复杂性,并且不能减少。

系统中的某些复杂性是无意的。这是由于结构不良,错误或者糟糕的建模造成的。这种无意的复杂性可以减少或者消除。然而,由于待解决问题有固有的复杂性,这些复杂性是内在的。这些复杂性可以转移,但不能消除。

该定律有趣的一点是,即使简化整个系统,内在的复杂性也不会降低。它会转移给用户,并且用户必须以更复杂的方式行事。

现实世界的复杂度无法使用代码来消除,如果你想少写代码,就要多写配置;如果你想少写配置,就要多写注解……

业务逻辑无法使用代码来化简或消除,业务逻辑不写在代码里,那就一定会转移到配置文件里或者其他什么地方。

业务逻辑不是程序员能控制的,程序员只负责代码实现,公司的领导层或许可以通过流程再造来改变业务逻辑。

因为现实世界的复杂性无法在代码层面消除,过度地抽象、解耦、套用设计模式反而会增加代码的复杂度。

关于配置文件

配置文件尽量保持简单直白,不要有分支或循环逻辑。分支或循环逻辑应该放到控制器里,因为控制器就是写业务逻辑代码的,改需求改的是控制器里代码,不变化的代码封装在模型里。配置文件里的配置信息应该是控制器的辅助,而不是相反。

由于外部环境的改变而经常跟着改变的变量值应该写在配置文件里,例如数据库配置信息测试环境一套,生产环境另一套,系统环境变量,要启用的进程数、线程数,公司名称、学校名称等,而不要把业务逻辑中的流程控制语句写在配置文件里。

总之,不要过度设计,不要过早优化,等版本稳定下来了再用设计模式重构代码也不迟。当然如果你的项目不缺钱也不缺时间,那么过早优化完全没有问题。

参考

https://github.com/nusr/hacker-laws-zh

https://en.wikipedia.org/wiki/Law_of_conservation_of_complexity

https://ferd.ca/complexity-has-to-live-somewhere.html

https://www.zhihu.com/question/429538225

Go切片(slice):用法和内部结构

本文翻译自《https://go.dev/blog/slices-intro》。

Andrew Gerrand

2011年1月5日

介绍

Go的切片(slice)类型提供了一种方便有效的方法来处理有类型的数据的序列。切片类似于其他语言中的数组,但具有一些不同寻常的属性。本文将介绍切片是什么以及它们的使用方法。

数组

切片类型是建立在Go的数组类型之上的抽象,因此要理解切片我们必须首先理解数组。

数组类型通过指定长度和元素类型来定义。例如,类型[4]int表示一个包含四个整数的数组。数组的大小是固定的;它的长度是其类型的一部分(例如[4]int[5]int是不同的、不兼容的类型)。数组可以用通常的方式索引,所以表达式s[n]访问第n个元素,从0开始。

var a [4]int
a[0] = 1
i := a[0]
// i == 1

数组不需要显式初始化;数组的零值是其所有元素本身为零值的现成(ready-to-use)数组:

// a[2] == 0,是int类型的零值

[4]int的内存表示形式仅为顺序排列的四个整数值:

Go语言的数组是值类型的。数组变量表示整个数组;它不是指向第一个数组元素的指针(C语言中的情况)。这意味着当你分配或传递数组值时,你将拷贝其所有内容(为了避免拷贝,你可以用一个指针指向数组,这是指向数组的指针,而不是数组本身)。可以将数组视为一种结构体,它具有索引字段而非命名字段,它是一种固定大小的复合值。

数组字面量可以这样指定:

b := [2]string{"Penn", "Teller"}

或者,你也可以让编译器为你计数数组元素的个数:

b := [...]string{"Penn", "Teller"}

这两种情况b的类型都是[2]string

切片

数组有它的空间,但有点不灵活,所以你不会在Go代码中经常看到数组。不过,切片无处不在。它们以数组为基础,提供强大的功能和便利。

切片的类型是[]T,其中T是切片里的元素的类型。与数组类型不同,切片类型没有指定长度。

切片字面量的声明就像数组字面量一样,只是你省略了元素计数:

letters := []string{"a", "b", "c", "d"}

可以使用内置函数make创建切片,该函数的签名如下:

func make([]T, len, cap) []T

其中T代表要创建的切片的元素的类型。make函数接收一个类型T、一个长度len和一个可选的容量cap。调用时,make分配一个数组并返回一个引用该数组的切片。

var s []byte
s = make([]byte, 5, 5)
// s == []byte{0, 0, 0, 0, 0}

当省略cap参数时,它默认等于指定的长度len。这是同一代码的更简洁的版本:

s := make([]byte, 5)

可以使用内置的lencap函数检查切片的长度和容量。

len(s) == 5
cap(s) == 5

接下来的两节讨论长度和容量之间的关系。

切片的零值为nil。对于nil切片,lencap函数都将返回0。

切片也可以通过“切片”现有切片或数组来形成。切片是通过指定一个半开放区间来完成的,其中两个索引由冒号分隔。例如,表达式b[1:4]创建一个包含b的下标从1到3的元素的切片(所得切片的索引将是0到2)。

b := []byte{'g', 'o', 'l', 'a', 'n', 'g'}
// b[1:4] == []byte{'o', 'l', 'a'}, sharing the same storage as b

切片表达式的开始和结束索引是可选的;它们分别默认为0和切片的长度:

// b[:2] == []byte{'g', 'o'}
// b[2:] == []byte{'l', 'a', 'n', 'g'}
// b[:] == b

这也是在给定一个数组的情况下,创建一个指向它的切片的语法:

x := [3]string{"Лайка", "Белка", "Стрелка"}
s := x[:] // 一个指向数组x的切片

切片内部

一个切片是一个数组片段的描述符。它由指向数组的指针、片段的长度及其容量(片段的最大长度)组成。

之前由make([]byte, 5)创建的变量s的结构如下:

长度是切片引用的数组元素的个数。容量是底层数组中的元素的个数(从切片指针指向的元素开始数)。在接下来的几个示例中,长度和容量之间的区别将变得更加清晰。

当我们对s进行切片时,观察切片数据结构的变化及其与底层数组之间的关系:

s = s[2:4]

切片不会复制底层数组的数据。它将创建一个指向原始数组的新切片值。这使得切片操作与处理数组索引一样高效。因此,修改新切片值的元素(而不是新切片值本身)会修改原始切片的元素:

d := []byte{'r', 'o', 'a', 'd'}
e := d[2:]
// e == []byte{'a', 'd'}
e[1] = 'm'
// e == []byte{'a', 'm'}
// d == []byte{'r', 'o', 'a', 'm'}

之前我们将s切成比其容量短的长度。 我们可以通过再次切片来增加s的容量:

s = s[:cap(s)]

切片不能超出其容量。尝试这样做会导致运行时panic,就像索引超出切片或数组边界时一样。同样,不能将切片重新切片到0以下来访问数组中较早的元素。

增长切片的元素(copyappend函数)

为了增加切片的容量,必须创建一个新的、更大的切片,并将原始切片的内容复制到其中。这项技术是其他语言的动态数组在幕后的实现方式。下一个示例通过创建一个新的切片t,将s的内容复制到t,然后将t赋值给s,从而使s的容量加倍:

t := make([]byte, len(s), (cap(s)+1)*2) // +1是为了防止cap(s) == 0的情况
for i := range s {
        t[i] = s[i]
}
s = t

内置的copy函数使这种常见的循环操作变得更容易。顾名思义,copy将数据从源切片复制到目标切片。它返回复制的元素个数。

func copy(dst, src []T) int

copy函数支持在不同长度的切片之间进行复制(长度较短的那个切片复制或被复制完毕就不再继续)。此外,copy可以处理共享同一底层数组的源切片和目标切片,正确地处理元素部分重叠的切片。

使用copy函数,我们可以简化上面的代码片段:

t := make([]byte, len(s), (cap(s)+1)*2)
copy(t, s)
s = t

一个常见的操作是将数据追加到切片的末尾。此函数将字节元素附加到字节切片,必要时会增大切片的容量,返回更新后的切片:

func AppendByte(slice []byte, data ...byte) []byte {
    m := len(slice)
    n := m + len(data)
    if n > cap(slice) { // 如果有必要,重新分配一个底层数组
        // 考虑到未来的数据增长,在此处加倍底层数组的容量。
        newSlice := make([]byte, (n+1)*2)
        copy(newSlice, slice)
        slice = newSlice
    }
    slice = slice[0:n]
    copy(slice[m:n], data)
    return slice
}

可以这样使用AppendByte

p := []byte{2, 3, 5}
p = AppendByte(p, 7, 11, 13)
// p == []byte{2, 3, 5, 7, 11, 13}

AppendByte这样的函数很有用,因为它们提供了对切片增长方式的完全控制。 根据程序的特性,可能需要分配更小或更大的块,或者对重新分配的大小设置上限。

但是大多数程序不需要完全控制,因此Go提供了一个适合大多数用途的内置append函数,签名如下:

func append(s []T, x ...T) []T

append函数将元素x附加到切片s的末尾,并在需要更大容量时扩大切片。

a := make([]int, 1)
// a == []int{0}
a = append(a, 1, 2, 3)
// a == []int{0, 1, 2, 3}

要将一个切片附加到另一个切片,请使用...将第二个参数展开为列表。

a := []string{"John", "Paul"}
b := []string{"George", "Ringo", "Pete"}
a = append(a, b...) // 等价于"append(a, b[0], b[1], b[2])"
// a == []string{"John", "Paul", "George", "Ringo", "Pete"}

由于切片的零值(nil)就像一个零长度切片,你可以声明一个切片变量,然后在循环中附加到它:

// Filter函数返回一个新切片,它只包含s切片中那些使fn函数返回true的元素
func Filter(s []int, fn func(int) bool) []int {
    var p []int // == nil
    for _, v := range s {
        if fn(v) {
            p = append(p, v)
        }
    }
    return p
}

一个可能的“陷阱”

如前所述,重新切片不会复制底层数组。整个数组将保存在内存中,直到不再被引用为止。有时这会导致程序只需要一小部分数据,但将所有数据保存在内存中。

例如,FindDigits函数将一个文件加载到内存中,并在其中搜索第一组连续数字,将它们作为一个切片返回。

var digitRegexp = regexp.MustCompile("[0-9]+")

func FindDigits(filename string) []byte {
    b, _ := ioutil.ReadFile(filename)
    return digitRegexp.Find(b)
}

此代码的行为确实满足要求,但返回的[]byte指向包含整个文件的数组。由于切片引用了原始数组,只要切片一直存在,垃圾收集器就无法释放数组;为了文件的几个有用字节,就将文件的全部内容保存在内存中。

要解决此问题,可以在返回之前将感兴趣的数据复制到新切片:

func CopyDigits(filename string) []byte {
    b, _ := ioutil.ReadFile(filename)
    b = digitRegexp.Find(b)
    c := make([]byte, len(b))
    copy(c, b)
    return c
}

可以使用append函数简化上述函数的代码。这留给读者作为练习。

进一步阅读

Effective Go包含对切片数组的深入处理,Go语言规范定义了切片及其相关的辅助函数。