互联网时代的来临,改变甚至颠覆了很多东西。从前,一台主机就能搞定一切;而在互联网时代,后台由大量分布式系统构成,任何单个后台服务器节点的故障都不会影响整个系统的正常运行。以七牛云、阿里云和腾讯云为代表的云厂商的出现和崛起,标志着云时代的到来。在云时代,掌握分布式编程已经成为软件工程师的基本技能,而基于Go语言构建的Docker、Kubernetes等系统正是将云时代推向顶峰的关键力量。

今天,Go语言已历经十年,最初的追随者也已经逐渐成长为Go语言资深用户。随着资深用户的不断积累,Go语言相关教程随之增加,在内容层面主要涵盖Go语言基础编程、Web编程、并发编程和内部源码剖析等诸多领域。

七月,新书《GO语言高级编程》推荐给小伙伴们!!


    • 一本能满足Gopher好奇心的Go语言进阶读物
    • 汇集了作者多年来学习和使用Go语言的经验
    • 更倾向于描述实现细节,极大地满足开发者的探索欲望

本书作者是国内第一批Go语言实践者和Go语言代码贡献者,创建了Go语言中国讨论组,并组织了早期Go语言相关中文文档的翻译工作。作者从2011年开始分享Go语言和C/C++语言混合编程技术。本书汇集了作者多年来学习和使用Go语言的经验,内容涵盖CGO特性、Go汇编语言、RPC实现、Protobuf插件实现、Web框架实现、分布式系统等高阶主题。其中,CGO特性实现了Go语言对C语言和C++语言混合编程的支持,使Go语言可以无缝继承C/C++世界数十年来积累的巨大软件资产。Go汇编语言更是提供了直接调用底层机器指令的方法,让我们可以最大限度地提升程序中热点代码的性能。

本书适合有一定Go语言经验,并想深入了解Go语言各种高级用法的开发人员。对于Go语言新手,建议在阅读本书前先阅读一些基础Go语言编程图书。

目录

    • 内容提要
    • 序一
    • 序二
    • 前言
    • 致谢
    • 资源与支持
    • 第1章 语言基础
    • 第2章 CGO编程
    • 第3章 Go汇编语言
    • 第4章 RPC和Protobuf
    • 第5章 Go和Web
    • 第6章 分布式系统
    • 附录A 使用Go语言常遇到的问题
    • 附录B 有趣的代码片段

样章试读:第1章 语言基础(截选)

我不知道,你过去10年为什么不快乐。但相信我,抛掉过去的沉重,使用Go语言,体会最初的快乐!

——469856321

搬砖民工也会建成自己的“罗马帝国”。

——小张

本章首先简要介绍Go语言的发展历史,并较详细地分析“Hello, World”程序在各个祖先语言中的演化过程。然后,对以数组、字符串和切片为代表的基础结构,以函数、方法和接口体现的面向过程和鸭子对象的编程,以及Go语言特有的并发编程模型和错误处理哲学做简单介绍。最后,针对macOS、Windows、Linux几个主流的开发平台,推荐几种较友好的Go语言编辑器和集成开发环境,因为好的工具可以极大地提高我们的效率。

1.1 Go语言创世纪

Go语言最初由谷歌公司的Robert Griesemer、Ken Thompson和Rob Pike这3位技术大咖于2007年开始设计发明,设计新语言的最初动力来自对超级复杂的C++11特性的吹捧报告的鄙视,最终的目标是设计网络和多核时代的C语言。到2008年中期,在语言的大部分特性设计已经完成并开始着手实现编译器和运行时,Russ Cox作为主力开发者加入。到2010年,Go语言已经逐步趋于稳定,并在9月正式发布并开源了代码。

Go语言很多时候被描述为“类C语言”,或者“21世纪的C语言”。从各种角度看,Go语言确实是从C语言继承了相似的表达式语法、控制流结构、基础数据类型、调用参数传值、指针等诸多编程思想,并彻底继承和发扬了C语言简单直接的暴力编程哲学等。图1-1给出的是The Go Programming Language中给出的Go语言的基因图谱,我们可以从中看到有哪些编程语言对Go语言产生了影响。



图1-1 Go语言基因图谱

首先看基因图谱的左边一支。可以明确看出Go语言的并发特性是由贝尔实验室的Hoare于1978年发布的CSP理论演化而来。其后,CSP并发模型在Squeak/Newsqueak和Alef等编程语言中逐步完善并走向实际应用,最终这些设计经验被消化并吸收到了Go语言中。业界比较熟悉的Erlang编程语言的并发编程模型也是CSP理论的另一种实现。

再看基因图谱的中间一支。中间一支主要包含了Go语言中面向对象和包特性的演化历程。Go语言中包和接口以及面向对象等特性则继承自Niklaus Wirth所设计的Pascal语言以及其后衍生的相关编程语言。其中包的概念、包的导入和声明等语法主要来自Modula-2编程语言,面向对象特性所提供的方法的声明语法等则来自Oberon编程语言。最终Go语言演化出了自己特有的支持鸭子面向对象模型的隐式接口等诸多特性。

最后是基因图谱的右边一支,这是对C语言的致敬。Go语言是对C语言最彻底的一次扬弃,不仅在语法上和C语言有着很多差异,最重要的是舍弃了C语言中灵活但是危险的指针运算。而且,Go语言还重新设计了C语言中部分不太合理运算符的优先级,并在很多细微的地方都做了必要的打磨和改变。当然,C语言中少即是多、简单直接的暴力编程哲学则被Go语言更彻底地发扬光大了(Go语言居然只有25个关键字,语言规范还不到50页)。

defer

1.1.1 来自贝尔实验室特有基因

作为Go语言标志性的并发编程特性则来自贝尔实验室的Tony Hoare于1978年发表的鲜为外界所知的关于并发研究的基础文献:顺序通信进程(Communicating Sequential Processes,CSP)。在最初的CSP论文中,程序只是一组没有中间共享状态的并发运行的处理过程,它们之间使用通道进行通信和控制同步。Tony Hoare的CSP并发模型只是一个用于描述并发性基本概念的描述语言,它并不是一个可以编写可执行程序的通用编程语言。

CSP并发模型最经典的实际应用是来自爱立信公司发明的Erlang编程语言。不过在Erlang将CSP理论作为并发编程模型的同时,同样来自贝尔实验室的Rob Pike以及其同事也在不断尝试将CSP并发模型引入当时的新发明的编程语言中。他们第一次尝试引入CSP并发特性的编程语言叫Squeak(老鼠的叫声),是一个用于提供鼠标和键盘事件处理的编程语言,在这个语言中通道是静态创建的。然后是改进版的Newsqueak语言(新版老鼠的叫声),新提供了类似C语言语句和表达式的语法,还有类似Pascal语言的推导语法。Newsqueak是一个带垃圾回收机制的纯函数式语言,它再次针对键盘、鼠标和窗口事件管理。但是在Newsqueak语言中通道已经是动态创建的,通道属于第一类值,可以保存到变量中。然后是Alef编程语言(Alef也是C语言之父Ritchie比较喜爱的编程语言),Alef语言试图将Newsqueak语言改造为系统编程语言,但是因为缺少垃圾回收机制而导致并发编程很痛苦(这也是继承C语言手工管理内存的代价)。在Alef语言之后还有一个名为Limbo的编程语言(地狱的意思),这是一个运行在虚拟机中的脚本语言。Limbo语言是与Go语言最接近的祖先,它和Go语言有着最接近的语法。到设计Go语言时,Rob Pike在CSP并发编程模型的实践道路上已经积累了几十年的经验,关于Go语言并发编程的特性完全是信手拈来,新编程语言的到来也是水到渠成了。

git log --before={2008-03-03} --reverse



图1-2 Go语言开发日志

从早期提交日志中也可以看出,Go语言是从Ken Thompson发明的B语言、Dennis M. Ritchie发明的C语言逐步演化过来的,它首先是C语言家族的成员,因此很多人将Go语言称为21世纪的C语言。

图1-3给出的是Go语言中来自贝尔实验室特有并发编程基因的演化过程。



图1-3 Go语言并发演化历史

纵观整个贝尔实验室的编程语言的发展进程,从B语言、C语言、Newsqueak、Alef、Limbo语言一路走来,Go语言继承了来自贝尔实验室的半个世纪的软件设计基因,终于完成了C语言革新的使命。纵观这几年来的发展趋势,Go语言已经成为云计算、云存储时代最重要的基础编程语言。

1.1.2 你好,世界

按照惯例,介绍所有编程语言的第一个程序都是“Hello, World!”。虽然本书假设读者已经了解了Go语言,但是我们还是不想打破这个惯例(因为这个传统正是从Go语言的前辈C语言传承而来的)。下面的代码展示的Go语言程序输出的是中文“你好,世界!”。

package mainimport"fmt"func main(){    fmt.Println("你好, 世界!")}


hello.gohello.gogo run hello.go
packagepackagemainmainmain()packagepackageimportfmtfmtfmtPrintln()
fmt.Println()reflect.StringHeader

1.2 “Hello, World”的革命

1.1节中简单介绍了Go语言的演化基因图谱,对其中来自贝尔实验室的特有并发编程基因做了重点介绍,最后引出了Go语言版的“Hello, World”程序。其实“Hello, World”程序是展示各种语言特性的最好的例子,是通向该语言的一个窗口。本节将沿着各个编程语言演化的时间轴(如图1-3所示),简单回顾一下“Hello, World”程序是如何逐步演化到目前的Go语言形式并最终完成它的使命的。

1.2.1 B语言——Ken Thompson, 1969

首先是B语言,B语言是“Go语言之父”——贝尔实验室的Ken Thompson早年间开发的一种通用的程序设计语言,设计目的是为了用于辅助UNIX系统的开发。但是由于B语言缺乏灵活的类型系统导致使用比较困难。后来,Ken Thompson的同事Dennis Ritchie以B语言为基础开发出了C语言,C语言提供了丰富的类型,极大地增强了语言的表达能力。到目前为止,C语言依然是世界上最常用的程序语言之一。而B语言自从被它取代之后,就只存在于各种文献之中,成为了历史。

目前见到的B语言版本的“Hello, World”,一般认为是来自Brian W. Kernighan编写的B语言入门教程(Go核心代码库中第一个提交者的名字正是Brian W. Kernighan),程序如下:

main(){    extrn a, b, c;    putchar(a); putchar(b); putchar(c);    putchar('!*n');}a 'hell';b 'o, w';c 'orld';


a/b/cputchar()'!*n'

总体来说,B语言简单,功能也比较有限。

1.2.2 C语言——Dennis Ritchie,1972—1989

C语言是由Dennis Ritchie在B语言的基础上改进而来,它增加了丰富的数据类型,并最终实现了用它重写UNIX的伟大目标。C语言可以说是现代IT行业最重要的软件基石,目前主流的操作系统几乎全部是由C语言开发的,许多基础系统软件也是C语言开发的。C系家族的编程语言占据统治地位达几十年之久,半个多世纪以来依然充满活力。

在Brian W. Kernighan于1974年左右编写的C语言入门教程中,出现了第一个C语言版本的“Hello, World”程序。这给后来大部分编程语言教程都以“Hello, World”为第一个程序提供了惯例。第一个C语言版本的“Hello, World”程序如下:

main(){    printf("hello, world");}


main()intprintf()main ()printf ()

这个例子同样出现在了1978年出版的《C程序设计语言(第1版)》中,作者正是Brian W. Kernighan和Dennis M. Ritchie(简称K&R)。书中的“Hello, World”末尾增加了一个换行输出:

main(){    printf("hello, world\n");}


\n'!*n'
#include printf()printf()
#includemain(){    printf("hello, world\n");}


main()void
#includemain(void){    printf("hello, world\n");}


至此,C语言本身的进化基本完成。后面的C92/C99/C11都只是针对一些语言细节做了完善。因为各种历史因素,C89依然是使用最广泛的标准。

1.2.3 Newsqueak——Rob Pike, 1989

Newsqueak是Rob Pike发明的老鼠语言的第二代,是他用于实践CSP并发编程模型的战场。Newsqueak是新的Squeak语言的意思,其中squeak是老鼠“吱吱吱”的叫声,也可以看作是类似鼠标点击的声音。Squeak是一个提供鼠标和键盘事件处理的编程语言,Squeak语言的通道是静态创建的。改进版的Newsqueak语言则提供了类似C语言语句和表达式的语法和类似Pascal语言的推导语法。Newsqueak是一个带自动垃圾回收机制的纯函数式语言,它再次针对键盘、鼠标和窗口事件管理。但是在Newsqueak语言中通道是动态创建的,属于第一类值,因此可以保存到变量中。

print()
print("Hello,","World","\n");


print()



图1-4 素数筛

Newsqueak语言并发版本的“素数筛”程序如下:

// 向通道输出从2开始的自然数序列counter := prog(c:chan of int){    i :=2;for(;;){        c <-= i++;}};// 针对listen通道获取的数列,过滤掉是prime倍数的数// 新的序列输出到send通道filter := prog(prime:int, listen, send:chan of int){    i:int;for(;;){if((i =<-listen)%prime){            send <-= i;}}};// 主函数// 每个通道第一个流出的数必然是素数// 然后基于这个新的素数构建新的素数过滤器sieve := prog() of chan of int{    c := mk(chan of int);begin counter(c);    prime := mk(chan of int);begin prog(){        p:int;        newc:chan of int;for(;;){            prime <-= p =<- c;            newc = mk();begin filter(p, c, newc);            c = newc;}}();    become prime;};// 启动素数筛prime := sieve();


counter()filter()mk(chan of int)make(chan int)begin filter(p, c, newc)go filter(p, c, newc)becomereturn

Newsqueak语言中并发体和通道的语法与Go语言已经比较接近了,后置的类型声明和Go语言的语法也很相似。

1.2.4 Alef——Phil Winterbottom, 1993

proc receive(c)task receive(c)c

由于Alef语言同时支持进程和线程并发体,而且在并发体中可以再次启动更多的并发体,导致Alef的并发状态异常复杂。同时Alef没有自动垃圾回收机制(Alef保留的C语言灵活的指针特性,也导致自动垃圾回收机制实现比较困难),各种资源充斥于不同的线程和进程之间,导致并发体的内存资源管理异常复杂。Alef语言全部继承了C语言的语法,可以认为是增强了并发语法的C语言。图1-5给出的是Alef语言文档中展示的一个可能的并发体状态。



图1-5 Alef并发模型

Alef语言并发版本的“Hello, World”程序如下:

#includevoid receive(chan(byte*) c){byte*s;    s =<- c;print("%s\n", s);    terminate(nil);}void main(void){    chan(byte*) c;    alloc c;    proc receive(c);    task receive(c);    c <-="hello proc or task";    c <-="hello proc or task";print("done\n");    terminate(nil);}


#include Receive ()main()alloc cchan(byte*)make(chan []byte)receive()main()creceive()terminate(nil)

Alef的语法和C语言基本保持一致,可以认为它是在C语言的语法基础上增加了并发编程相关的特性,可以看作是另一个维度的C++语言。

1.2.5 Limbo——Sean Dorward, Phil Winterbottom, Rob Pike, 1995

Limbo(地狱)是用于开发运行在小型计算机上的分布式应用的编程语言,它支持模块化编程、编译期和运行时的强类型检查、进程内基于具有类型的通信通道、原子性垃圾收集和简单的抽象数据类型。Limbo被设计为:即便是在没有硬件内存保护的小型设备上,也能安全运行。Limbo语言主要运行在Inferno系统之上。

Limbo语言版本的“Hello, World”程序如下:

implement Hello;include "sys.m"; sys:Sys;include "draw.m";Hello:module{    init: fn(ctxt:refDraw->Context, args: list of string);};init(ctxt:refDraw->Context, args: list of string){    sys = load SysSys->PATH;    sys->print("hello, world\n");}


implement Hello;package Helloinclude "sys.m"; sys: Sys;include "draw.m";import "sys"import "draw"Helloinit()

1.2.6 Go语言——2007—2009

func

1.hello.go——2008年6月

下面是初期Go语言程序正式开始测试的版本:

package mainfunc main()int{print"hello, world\n";return0;}


printmain()main()intreturn

2.hello.go——2008年6月27日

下面是2008年6月的Go代码:

package mainfunc main(){print"hello, world\n";}


main()exit(0)

3.hello.go——2008年8月11日

下面是2008年8月的代码:

package mainfunc main(){print("hello, world\n");}


print

4.hello.go——2008年10月24日

下面是2008年10月的代码:

package mainimport"fmt"func main(){    fmt.printf("hello, world\n");}


printf ()fmtfmtformatprintf()

5.hello.go——2009年1月15日

下面是2009年1月的代码:

package mainimport"fmt"func main(){    fmt.Printf("hello, world\n");}


Go语言开始采用是否大小写首字母来区分符号是否可以导出。大写字母开头表示导出的公共符号,小写字母开头表示包内部的私有符号。但需要注意的是,汉字中没有大小写字母的概念,因此以汉字开头的符号目前是无法导出的(针对该问题,中国用户已经给出相关建议,等Go 2之后或许会调整对汉字的导出规则)。

6.hello.go——2009年12月11日

下面是2009年12月的代码:

package mainimport"fmt"func main(){    fmt.Printf("hello, world\n")}


Go语言终于移除了语句末尾的分号。这是Go语言在2009年11月10日正式开源之后第一个比较重要的语法改进。从1978年C语言教程第一版引入的分号分隔的规则到现在,Go语言的作者们花了整整32年终于移除了语句末尾的分号。在这32年的演化过程中必然充满了各种八卦故事,我想这一定是Go语言设计者深思熟虑的结果(现在Swift等新的语言也是默认忽略分号的,可见分号确实并不是那么重要)。

1.2.7 你好,世界!——V2.0

http
package mainimport("fmt""log""net/http""time")func main(){    fmt.Println("Please visit http://127.0.0.1:12345/")    http.HandleFunc("/", func(w http.ResponseWriter, req *http.Request){        s := fmt.Sprintf("你好, 世界! -- Time: %s", time.Now().String())        fmt.Fprintf(w,"%v\n", s)        log.Printf("%v\n", s)})if err := http.ListenAndServe(":12345",nil); err !=nil{        log.Fatal("ListenAndServe: ", err)}}


net/httphttp.HandleFunc("/", ...)/fmt.Fprintf ()http.ListenAndServe()

至此,Go语言终于完成了从单机单核时代的C语言到21世纪互联网时代多核环境的通用编程语言的蜕变。

1.3 数组、字符串和切片

在主流的编程语言中数组及其相关的数据结构是使用得最为频繁的,只有在它(们)不能满足时才会考虑链表、散列表(散列表可以看作是数组和链表的混合体)和更复杂的自定义数据结构。

Go语言中数组、字符串和切片三者是密切相关的数据结构。这3种数据类型,在底层原始数据有着相同的内存结构,在上层,因为语法的限制而有着不同的行为表现。首先,Go语言的数组是一种值类型,虽然数组的元素可以被修改,但是数组本身的赋值和函数传参都是以整体复制的方式处理的。Go语言字符串底层数据也是对应的字节数组,但是字符串的只读属性禁止了在程序中对底层字节数组的元素的修改。字符串赋值只是复制了数据地址和对应的长度,而不会导致底层数据的复制。切片的行为更为灵活,切片的结构和字符串结构类似,但是解除了只读限制。切片的底层数据虽然也是对应数据类型的数组,但是每个切片还有独立的长度和容量信息,切片赋值和函数传参时也是将切片头信息部分按传值方式处理。因为切片头含有底层数据的指针,所以它的赋值也不会导致底层数据的复制。其实Go语言的赋值和函数传参规则很简单,除闭包函数以引用的方式对外部变量访问之外,其他赋值和函数传参都是以传值的方式处理。要理解数组、字符串和切片这3种不同的处理方式的原因,需要详细了解它们的底层数据结构。

1.3.1 数组

数组是一个由固定长度的特定类型元素组成的序列,一个数组可以由零个或多个元素组成。数组的长度是数组类型的组成部分。因为数组的长度是数组类型的一部分,不同长度或不同类型的数据组成的数组都是不同的类型,所以在Go语言中很少直接使用数组(不同长度的数组因为类型不同无法直接赋值)。和数组对应的类型是切片,切片是可以动态增长和收缩的序列,切片的功能也更加灵活,但是要理解切片的工作原理还是要先理解数组。

我们先看看数组有哪些定义方式:

var a [3]int// 定义长度为3的int型数组,元素全部为0var b =[...]int{1,2,3}// 定义长度为3的int型数组,元素为1, 2, 3var c =[...]int{2:3,1:2}// 定义长度为3的int型数组,元素为0, 2, 3var d =[...]int{1,2,4:5,6}// 定义长度为6的int型数组,元素为1, 2, 0, 0, 5, 6


第一种方式是定义一个数组变量的最基本的方式,数组的长度明确指定,数组中的每个元素都以零值初始化。

第二种方式是定义数组,可以在定义的时候顺序指定全部元素的初始化值,数组的长度根据初始化元素的数目自动计算。

map[int]Type

第四种方式是混合了第二种和第三种的初始化方式,前面两个元素采用顺序初始化,第三个和第四个元素采用零值初始化,第五个元素通过索引初始化,最后一个元素跟在前面的第五个元素之后采用顺序初始化。

[4]int{2,3,5,7}



图1-6 数组布局

Go语言中数组是值语义。一个数组变量即表示整个数组,它并不是隐式地指向第一个元素的指针(例如C语言的数组),而是一个完整的值。当一个数组变量被赋值或者被传递的时候,实际上会复制整个数组。如果数组较大的话,数组的赋值也会有较大的开销。为了避免复制数组带来的开销,可以传递一个指向数组的指针,但是数组指针并不是数组。

var a =[...]int{1,2,3}// a是一个数组var b =&a                // b是指向数组的指针fmt.Println(a[0], a[1])// 打印数组的前两个元素fmt.Println(b[0], b[1])// 通过数组指针访问数组元素的方式和通过数组类似for i, v := range b {// 通过数组指针迭代数组的元素    fmt.Println(i, v)}


babafor range
len()cap()len()cap()
for
for i := range a {    fmt.Printf("a[%d]: %d\n", i, a[i])}for i, v := range b {    fmt.Printf("b[%d]: %d\n", i, v)}for i :=0; i < len(c); i++{    fmt.Printf("c[%d]: %d\n", i, c[i])}


for range
for range
var times [5][0]intfor range times {    fmt.Println("hello")}


times[5][0]int[0]int00for rangetimes

数组不仅可以定义数值数组,还可以定义字符串数组、结构体数组、函数数组、接口数组、通道数组等:

// 字符串数组var s1 =[2]string{"hello","world"}var s2 =[...]string{"你好","世界"}var s3 =[...]string{1:"世界",0:"你好",}// 结构体数组var line1 [2]image.Pointvar line2 =[...]image.Point{image.Point{X:0, Y:0}, image.Point{X:1, Y:1}}var line3 =[...]image.Point{{0,0},{1,1}}// 函数数组var decoder1 [2]func(io.Reader)(image.Image, error)var decoder2 =[...]func(io.Reader)(image.Image, error){    png.Decode,    jpeg.Decode,}// 接口数组var unknown1 [2]interface{}var unknown2 =[...]interface{}{123,"你好"}// 通道数组var chanList =[2]chan int{}


我们还可以定义一个空的数组:

var d [0]int// 定义一个长度为0的数组var e =[0]int{}// 定义一个长度为0的数组var f =[...]int{}// 定义一个长度为0的数组


长度为0的数组(空数组)在内存中并不占用空间。空数组虽然很少直接使用,但是可以用于强调某种特有类型的操作时避免分配额外的内存空间,例如用于通道的同步操作:

c1 := make(chan [0]int)go func(){    fmt.Println("c1")    c1 <-[0]int{}}()<-c1


在这里,我们并不关心通道中传输数据的真实类型,其中通道接收和发送操作只是用于消息的同步。对于这种场景,我们用空数组作为通道类型可以减少通道元素赋值时的开销。当然,一般更倾向于用无类型的匿名结构体代替空数组:

c2 := make(chan struct{})go func(){    fmt.Println("c2")    c2 <-struct{}{}// struct{}部分是类型,{}表示对应的结构体值}()<-c2


fmt.Printf()%T%#v
fmt.Printf("b: %T\n", b)// b: [3]intfmt.Printf("b: %#v\n", b)// b: [3]int{1, 2, 3}


在Go语言中,数组类型是切片和字符串等结构的基础。以上对于数组的很多操作都可以直接用于字符串或切片中。

1.3.2 字符串

for range
reflect.StringHeader
type StringHeaderstruct{Data uintptrLenint}


reflect.StringHeader[2]string[2]reflect.StringHeader
"hello, world"



图1-7 字符串布局

"hello, world"
var data =[...]byte{'h','e','l','l','o',',',' ','w','o','r','l','d',}


字符串虽然不是切片,但是支持切片操作,不同位置的切片底层访问的是同一块内存数据(因为字符串是只读的,所以相同的字符串面值常量通常对应同一个字符串常量):

s :="hello, world"hello := s[:5]world := s[7:]s1 :="hello, world"[:5]s2 :="hello, world"[7:]


len()reflect.StringHeader
fmt.Println("len(s):",(*reflect.StringHeader)(unsafe.Pointer(&s)).Len)// 12fmt.Println("len(s1):",(*reflect.StringHeader)(unsafe.Pointer(&s1)).Len)// 5fmt.Println("len(s2):",(*reflect.StringHeader)(unsafe.Pointer(&s2)).Len)// 5


printfmt.Print()for range
"hello,"
fmt.Printf("%#v\n",[]byte("hello, 世界"))


输出的结果是:

[]byte{0x48,0x65,0x6c,0x6c,0x6f,0x2c,0x20,0xe4,0xb8,0x96,0xe7, \0x95,0x8c}


0xe4, 0xb8, 0x960xe7, 0x95, 0x8c
fmt.Println("\xe4\xb8\x96")// 打印“世”fmt.Println("\xe7\x95\x8c")// 打印“界”


图1-8展示了“hello, 世界”字符串的内存结构布局。



图1-8 字符串布局

'\uFFFD'

下面的字符串中,我们故意损坏了第一字符的第二和第三字节,因此第一字符将会打印为“�”,第二和第三字节则被忽略,后面的“abc”依然可以正常解码打印(错误编码不会向后扩散是UTF8编码的优秀特性之一)。

fmt.Println("\xe4\x00\x00\xe7\x95\x8cabc")// �界abc


for range
for i, c := range "\xe4\x00\x00\xe7\x95\x8cabc"{    fmt.Println(i, c)}// 0 65533  // \uFFF,对应�// 1 0      // 空字符// 2 0      // 空字符// 3 30028  // 界// 6 97     // a// 7 98     // b// 8 99     // c


[]byte
for i, c := range []byte("世界abc"){    fmt.Println(i, c)}


或者是采用传统的下标方式遍历字符串的字节数组:

const s ="\xe4\x00\x00\xe7\x95\x8cabc"for i :=0; i < len(s); i++{    fmt.Printf("%d %x\n", i, s[i])}


for range[]rune
fmt.Printf("%#v\n",[]rune("世界"))// []int32{19990, 30028}fmt.Printf("%#v\n",string([]rune{'世','界'}))// 世界


[]rune[]int32runeint32rune
[]byte[]runeOn[]rune[]byte[]int32

下面分别用伪代码简单模拟Go语言对字符串内置的一些操作,这样对每个操作的处理的时间复杂度和空间复杂度都会有较明确的认识。

for range
func forOnString(s string, forBody func(i int, r rune)){for i :=0; len(s)>0;{        r, size := utf8.DecodeRuneInString(s)        forBody(i, r)        s = s[size:]        i += size}}


for rangefor
[]byte(s)
func str2bytes(s string)[]byte{    p := make([]byte, len(s))for i :=0; i < len(s); i++{        c := s[i]        p[i]= c}return p}


[]byte
string(bytes)
func bytes2str(s []byte)(p string){    data := make([]byte, len(s))for i, c := range s {        data[i]= c}    hdr :=(*reflect.StringHeader)(unsafe.Pointer(&p))    hdr.Data= uintptr(unsafe.Pointer(&data[0]))    hdr.Len= len(s)return p}


unsafe[]byte[]byte
[]rune(s)
func str2runes(s []byte)[]rune {var p []int32for len(s)>0{        r, size := utf8.DecodeRune(s)        p = append(p, int32(r))        s = s[size:]}return[]rune(p)}


[]rune[]rune
string(runes)
func runes2string(s []int32)string{var p []byte    buf := make([]byte,3)for _, r := range s {        n := utf8.EncodeRune(buf, r)        p = append(p, buf[:n]...)}returnstring(p)}


[]rune

如果内容没看过瘾,可以移步下方购买。