Go-字符串和指针

以下文章来源于公众号程序猿架构 ,作者程序猿架构

一 字符串

字符串是每一门编程语言学习中必不可少部分。

在Python中,字符串可以用单引号包起来,也可以用双引号包起来,多行字符串可以使用三个单引号或三个双引号包起来。看下面的代码:

s = "hello"
s = 'hello'
s = '''I am the first line.
I am the second line.
I am the third line.'''

println(s)

在Go语言中,单行字符串只能用双引号包起来(单引号包起来的只能是单个字符),多行字符串用反引号包起来。看下面的代码:

a := "string"
// b := 'string'  // 此行编译错误,单引号包含的只能是单个字符
s := `I am the first line.
I am the second line.
I am the third line.`

fmt.Println(s)

编码

Python中默认的字符编码是Unicode,有必要先来了解一下Unicode字符:

Unicode字符编码是国际组织指定的一种编码标准,可以用来表示任意字符。Unicode编码是一种编码标准,但并没有规定字符如何存储。以汉字“汉”为例,它的 Unicode 码点是 0x6c49,对应的二进制数是 110110001001001,二进制数有 15 位,这也就说明了它至少需要 2 个字节来表示。可以想象,在 Unicode 字典中往后的字符可能就需要 3 个字节或者 4 个字节,甚至更多字节来表示了。

这就导致了一些问题,计算机怎么知道这个 2 个字节表示的是一个字符,而不是分别表示两个字符呢?这里我们可能会想到,那就取个最大的,假如 Unicode 中最大的字符用 4 字节就可以表示了,那么我们就将所有的字符都用 4 个字节来表示,不够的就往前面补 0。这样确实可以解决编码问题,但是却造成了空间的极大浪费,如果是一个英文文档,那文件大小就大出了 3 倍,这显然是无法接受的。

这个时候就出现了UTF-8可变长编码。UTF-8对于不同的字符存储需要,可以使用不同的字节长度来存储。比如,ASCII码的码值范围为0~127,只需要一个字节来存储即可,对于中文,绝大多数中文字都是3个字节即可存储。这样,就不用每个字符都使用4个字节来存储,极大的节省了空间。

Go语言的默认字符编码是UTF-8。字符串底层使用字节数组来存储,那么我们就可以使用len()函数来获取字符串的长度了。同时,Go语言可以使用[]byte(s)把字符串轻松的转换成字节切片。

底层使用字节数组来存储,因此字符串可以使用和切片类似的很多操作,比如:

s := "Hello, 世界"
bytes := []byte(s)
fmt.Println(bytes)  // 输出:[72 101 108 108 111 44 32 228 184 150 231 149 140]

for i := 0; i < len(s);i++ {
  fmt.Printf("%d %c\n", i, s[i])
}

for循环里那行输出的是:

图片

等一下,最后输出的从第7行到第12行有一些奇怪的字符。这是因为,“世界”中的每个字符在UTF-8编码下占3个字节,只取出每个字符的其中一个字节来输出,当然会是乱码。“Hello, 世界”这个字符串在底层是这样存储的:

图片

如果我们想输出字符串的每个字符,该怎么办?

可以使用rune类型,把字符串转换成rune切片:

s := "Hello, 世界"
r := []rune(s)
fmt.Println(r)  // 输出:[72 101 108 108 111 44 32 19990 30028]

for i := 0; i < len(r);i++ {
  fmt.Printf("%d %c\n", i, r[i])
}
// 输出:
0 H
1 e
2 l
3 l
4 o
5 ,
6  
7 世
8 界

或者直接使用range来循环字符串:

s := "Hello, 世界"

for i, c := range s {
  fmt.Printf("%d %c\n", i, c)
}
// 输出:
0 H
1 e
2 l
3 l
4 o
5 ,
6  
7 世
10 界

range会自动把字符串的字符按照UTF-8解码出来,这样循环字符串得到的字符就是一个完整的UTF-8字符了,而不是字符中的一个字节。

rune类型

在Go语言中,rune类型就是int的别名。看看官方解释:

// rune is an alias for int32 and is equivalent to int32 in all ways. It is
// used, by convention, to distinguish character values from integer values.

//int32的别名,几乎在所有方面等同于int32
//它用来区分字符值和整数值

type rune = int32

rune是用来区分字符值和整数值的。怎么区分?对于整数,直接使用int类型就好了。对于字符呢?我们上面说到,Go语言中字符都是使用UTF-8编码存储的,从1个到4个字节不等。那么每一个UTF-8字符,最多也就使用4个字节(32比特),也就是int32的长度,那么我们就可以使用int32类型来表示任意UTF-8字符的值。因此,我们得到rune类型的字符值之后,直接把这个值当成字符输出,就可以得到我们想要的字符了,而不是乱码了。有了这个结论,我们就可以知道,rune在处理中文时特别有用。

字符串方法

Go语言strings模块中内置了非常多的字符串方法。

Compare(a, b string) int

// Compare returns an integer comparing two strings lexicographically.
// The result will be 0 if a==b, -1 if a < b, and +1 if a > b.
//
// Compare is included only for symmetry with package bytes.
// It is usually clearer and always faster to use the built-in
// string comparison operators ==, <, >, and so on.
func Compare(a, b string) int {...}

字符串比较方法,传入a、b参数,返回比较结果。如果a小于b,则返回-1;如果a等于b,返回0;如果a大于b,返回1。Compare函数会按照字符的字典顺序比较字符串,见下图:

s1 := "abc"
s2 := "bac"
fmt.Println(strings.Compare(s1, s2))  // 输出-1

但是,官方并不建议使用Compare函数来比较字符串,看Compare源码中的结束,比较字符串可以直接使用大于、小于、等于号进行比较,并不需要使用Compare函数。

Index(s, substr string) int

子串查找/定位。如果s中包含substr这个子串,函数会返回substr在s中第一次出现的位置;如果不包含,会返回-1。看一下源码解释:

// Index returns the index of the first instance of substr in s, or -1 if substr is not present in s.
func Index(s, substr string) int {...}

使用案例:

s := "I am 中国人"
s1 := "国"
fmt.Println(strings.Index(s, s1))  // 输出:8

为什么输出是8而不是5呢?这是因为中Index函数返回的是子串所在的字节数位置。“I am ”占了5个字节,“中”占了3个字节,因此“国”所在的起始位置就是8。

IndexAny(s, chars string) int

在s中查找chars中的任意字符,如果找到了就返回其位置,没找到就返回-1。

看一下源码解释:

// IndexAny returns the index of the first instance of any Unicode code point
// from chars in s, or -1 if no Unicode code point from chars is present in s.
func IndexAny(s, chars string) int {...}

注意,这是在s中查找chars里的任意Unicode字符,不是Unicode字符则会找不到。见下面的例子:

s := "中国人"
s1 := s[3:4]
s2 := s[3:6]
fmt.Println(s1)  // 输出:�
fmt.Println(s2)  // 输出:国
fmt.Println(strings.IndexAny(s, s1))  // 输出:-1
fmt.Println(strings.IndexAny(s, s2))  // 输出:3

LastIndex(s, substr string) int

和Index相反,LastIndex函数在s中查找substr最后一次出现的位置,没找到则返回-1。看一下官方解释:

// LastIndex returns the index of the last instance of substr in s, or -1 if substr is not present in s.
func LastIndex(s, substr string) int {...}

使用案例:

s := "中国 国人"
s2 := s[3:6]
fmt.Println(s2)  // 输出:国
fmt.Println(strings.LastIndex(s, s2))  // 输出:7

Join(elems []string, sep string) string

把切片字符串elems用sep字符串连接起来,在处理文件路径时会非常有用。看一下官方解释:

// Join concatenates the elements of its first argument to create a single string. The separator
// string sep is placed between elements in the resulting string.
func Join(elems []string, sep string) string {...}

典型使用案例:

s := []string{"a", "b", "c"}
s1 := "/"
fmt.Println(strings.Join(s, s1))  // 输出:a/b/c

HasPrefix(s, prefix string) bool

这个函数和Java中字符串的startsWith方法一样,看看字符串s是不是以字符串prefix开头的。看一下源码和官方解释:

// HasPrefix tests whether the string s begins with prefix.
func HasPrefix(s, prefix string) bool {
  return len(s) >= len(prefix) && s[0:len(prefix)] == prefix
}

典型使用案例:

s := "中国"
s1 := "中"
s2 := "国"
fmt.Println(strings.HasPrefix(s, s1))  // 输出:true
fmt.Println(strings.HasPrefix(s, s2))  // 输出:false

HasSuffix(s, suffix string) bool

这个函数和Java中字符串的endsWith方法一样,看看字符串s是不是以字符串suffix结尾的。看一下源码和官方解释:

// HasSuffix tests whether the string s ends with suffix.
func HasSuffix(s, suffix string) bool {
  return len(s) >= len(suffix) && s[len(s)-len(suffix):] == suffix
}

典型使用案例:

s := "中国"
s1 := "中"
s2 := "国"
fmt.Println(strings.HasSuffix(s, s1))  // 输出:false
fmt.Println(strings.HasSuffix(s, s2))  // 输出:true

Trim(s string, cutset string) string

移除处于s两端的cutset中包含的任意字符:

s := "中国中国中国"
fmt.Println(strings.Trim(s, "国"))  // 输出:中国中国中
fmt.Println(strings.Trim(s, "中国"))  // 输出:(空)

我们知道,在Web项目中处理用户输入时,经常需要处理掉用户输入的空白字符,如果想移除字符串两边的空白字符呢?我们试试:

s := "    中国中国中国    "
fmt.Println(strings.Trim(s, ""))  // 输出:中国中国中国
fmt.Println(strings.Trim(s, " "))  // 输出:中国中国中国

怎么好像没啥作用?其实,移除两边的空白字符应该使用TrimSpace函数。

TrimSpace(s string) string

去除s两端的空白字符(包括空格、回车、换行、制表符)。

// TrimSpace returns a slice of the string s, with all leading
// and trailing white space removed, as defined by Unicode.
func TrimSpace(s string) string {...}

看一下使用案例:

s := "    中国中国中国    "
fmt.Println(strings.TrimSpace(s))  // 输出:中国中国中国

Split(s, sep string) []string

用sep来分割字符串s,返回分割后的字符串切片。如果s中不包含sep并且sep不为空,则返回的切片结果中只会有一个元素,那就是s本身;如果sep为空,那么会把字符串s的每个UTF-8字符都切分开,放入切片结果中返回。来看一下官方解释:

// Split slices s into all substrings separated by sep and returns a slice of
// the substrings between those separators.
//
// If s does not contain sep and sep is not empty, Split returns a
// slice of length 1 whose only element is s.
//
// If sep is empty, Split splits after each UTF-8 sequence. If both s
// and sep are empty, Split returns an empty slice.
//
// It is equivalent to SplitN with a count of -1.
func Split(s, sep string) []string {...}

看一下典型使用案例:

s := "中国中国中国"
fmt.Println(strings.Split(s, ""))  // 输出:[中 国 中 国 中 国]
fmt.Println(strings.Split(s, "2"))  // 输出:[中国中国中国]

那么,我如果只想分割2次呢?可以使用SplitN函数:

s := "中国中国中国"
fmt.Println(strings.SplitN(s, "", 2))  // 输出:[中 国中国中国]
fmt.Println(strings.SplitN(s, "2", 2))  // 输出:[中国中国中国]

还记得Java的split函数中还可以按照正则表达式来分割字符串,但是在Go语言中字符串函数却不行。这需要用到Go语言中的正则表达式。在接下来的文章中会重点讲这一块。

二 指针

Go语言和Java、Python显著不同的一点就是指针。如果你是从Java和Python转过来的,学起来会费力些,如果你是C++工作者,学起来会非常爽。

Go语言中指针和C++中指针一样操作,通过取地址符就可以取到变量的地址,赋给一个指针变量。

s := "中国中国中国"
p := &s
fmt.Println(p)  // 输出:0xc0000381f0
fmt.Println(*p)  // 输出:中国中国中国

p里面存放的是s的地址,直接输出p会看到一串16进制的字符串,*p才是指针p所指向的内容。

Go语言中指针的二进制类型是int类型,看一下源码:

// uintptr is an integer type that is large enough to hold the bit pattern of
// any pointer.
type uintptr uintptr

一个int类型的变量能够寻址到计算机的任意位置,因此int类型用来用来存放变量地址绰绰有余。

函数参数指针

指针在函数传参时会非常有用。Go语言在调用函数时,会把函数的参数复制一份传递过去。如果函数参数是值类型,这时,在函数内部修改参数就不会反映到函数外部了。见下面的例子:

func modifyArray(a [3]int) [3]int{ // 数组是值类型,传递给函数时,会复制一份传过去。
  a[1] = 0
  return a
}
func main() {
  s := [3]int{1, 2, 3}
  modifyArray(s)
  fmt.Println(s)  // 输出:[1 2 3]
}

Go语言中,数组是值类型,在调用函数的时候,会把数组拷贝一份传递给函数modifyArray,这样在modifyArray内部修改就不会反映到函数外面去了。

图片

那么怎么让它在函数内部能修改呢?把函数参数改成数组指针类型就可以了(或者可以使用切片类型,在后续文章会详解切片)。

func modifyArray(a *[3]int) *[3]int{ // 数组是值类型,传递给函数时,会复制一份传过去。
  a[1] = 0
  return a
}
func main() {
  s := [3]int{1, 2, 3}
  modifyArray(&s)
  fmt.Println(s)
}

由于调用函数会导致函数参数复制一份,如果函数参数非常大,复制成本会很高,导致性能下降,这时候指针参数就非常有用了。指针中存放的是参数的地址,传递参数时直接复制一份指针就可以了。

图片

结构体指针方法

指针在结构体的方法中也很有用。先来看一下下面的例子:

type rect struct {
  width, height int
}

func (r rect) modifyWidth(){   // rect实现了area方法
  r.width = 3
}

func (r *rect) modifyWidthPointer(){  // rect实现了perim方法,则rect实现了geometry接口
  r.width = 3
}

func main() {
  var rec = rect{
    width:  0,
    height: 0,
  }
  rec.modifyWidth()
  fmt.Println(rec)  // 输出:{0 0}
  rec.modifyWidthPointer()
  fmt.Println(rec)  // 输出:{3 0}
}

可以看到,把指针作为方法接收者,则可以在方法中任意修改接收者的属性并这种修改反映到方法外面。而使用非指针接收者的方法却修改不了。这是因为,非指针接收者在调用方法modifyWidth时,把本身的值赋值了一份。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,402评论 6 499
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,377评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,483评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,165评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,176评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,146评论 1 297
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,032评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,896评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,311评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,536评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,696评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,413评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,008评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,659评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,815评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,698评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,592评论 2 353

推荐阅读更多精彩内容