字符串string

ASCII

ASCII(American Standard Code for Information Interchange,美国信息交换标准代码)是基于拉丁字母的一套单字节编码系统

ASCII编码表:

数字和字符映射表,包括了英文字符和标点,用一个字节表示256种状态,用低7位128种状态就够了,0~127,都有对应的字符,包括可见字符、不可见字符、控制字符等。剩下的128种状态,用来扩展ASCII、欧洲字符编码表Latin-x

中文编码表:

数字到中文单一字符映射,至少常用的3k~5k个汉字,一个字节不够放,汉字只能多个字节存储

2字节,65536种状态,GB2312、GBK(包含了GB2312)、GB18030、BIG 5码

所有编码表都兼容单字节的ASCII表,但是如果有一篇文章需要多语言存储,如何实现?

UNICODE:

  • 多字节编码表,一张编码表解决全球多数字符对应问题
  • 表示汉字多数使用两个字节
  • Go中’x’ 方式,保存int32、rune整数型,%c打印通过unicode编码表找到字符输出

UTF-8:

  • 多字节
  • 汉字转为3个字节
  • utf8mb4包含表情符号
  • 字符串,字符序列,每个汉字就是utf-8编码的,也就是汉字是3个字节
s2 := "你好"
fmt.Println([]byte(s2)) //
//输出[228 189 160 229 165 189]

image

序号 转义字符 十进制数 说明
1 \x00 0 NUll
2 \x09 9 \t
3 \x0a 10 \n
4 \x0d 13 \r
5 \x31 1 字符1
6 \x41 65 字符A
7 \x61 97 字符a
8 \x30~39 48~57 字符0~9

上面的1是字符1

UTF-8、GBK都兼容了ASCII

"a\x09b\x0ac \x31\x20\x41\x61" 表示什么?
// a\tb\nc 1 Aa
'A' > 'a'   谁大?字符比较
// 41 > 61 false
"a" > "A"   谁大?字符串比较
// 61 > 41 true
"AA" > "Aa" 谁大?
// AA < Aa  因为A对应的ASCII表为41  a为61  会一个一个字符的比较

二、字符

本质上说,计算机中一切都是字节的,字符串也是多个字节组合而成,就是多个字节形成的有序序列。但是对于多字节编程的中文来说,用一个字节描述不了,需要多个字节表示一个字符,Go提供了rune类型。

  • byte:兼容ASCII码的字符,是byte类型,即uint8别名,占用一个字节
  • rune:汉字等字符,unicode编码,是rune类型,即int32类型,占用4个字节
  • 一个字符字面量使用单引号引起来

字符串与字节序列转换

s1 := "abc"
s2 := "测试" // 字符串用utf-8编码,每个汉字占用3个字节
fmt.Println(len(s1), len(s2)) // 3, 6 字节 
// 强制类型转换 string => []byte; string => []rune
// 注意[]byte表示字节序列;[]rune表示rune序列
fmt.Println([]byte(s1)) // [97 98 99] 字符'a' 对应的ascii码十进制为97,十六进制为\x61
fmt.Println([]rune(s1)) // rune int32 4字节,用unicode编码,兼容ascii码,所以值也为[97 98 99]
fmt.Println([]byte(s2)) // utf-8 bytes,长度为6即6个字节  // 
fmt.Println([]rune(s2)) // unicode切片,长度为2,每个元素4字节 
fmt.Printf("%x, %x", 27979, 35797)  // %x打印十六进制数
fmt.Println("~~~~~~~~~~~~~~~~~~~~~~~~~~~")
// []byte => string
fmt.Println(string([]byte{49, 65, 97}))  //将对应字节码转换成string,值为1Aa
// []rune => string
fmt.Println(string([]rune{27979, 35797})) // rune查unicode码,测试
// rune使用unicode,但是字符串内部使用utf-8
fmt.Println(s2[0], s2[1], s2[2]) // 索引取到了什么?
fmt.Println(string([]byte{230, 181, 139})) // 打印什么?
  • string(一个整数),强制转换为一个整数,相当于把整数当unicode码,取查一个字符,最后返回字符串
  • string(整数序列),强制类型转换为一个整数序列,也是转换成字符串。

三、字符串

  • 字面常量,只读,不可变
  • 线性数据结构,可以索引
  • 值类型
  • utf-8编码
长度

使用内建函数len,返回字符串占用的字节数。时间复杂度O(1),字符串是字面常量,定义时已经知道长度,记录下来即可

索引

不支持负索引,索引范围[0,len(s)-1]

即使有中文,索引指的是按照字节的偏移量

时间复杂度O(1),使用索引计算该字符相对开头的偏移量即可。

对于顺序表,使用索引查找效率是最高的。

s[i]获取索引 i 处的UTF-8编码的一个字节

遍历

C风格使用索引遍历,相当于字节遍历

	s := "magedu马哥教育"
	fmt.Println(len(s)) //18个字节,中文占3个字节
// C 风格使用索引遍历,相当于字节遍历,遍历出的元素类型为bytes字节
	for i := 0; i < len(s); i++ {
		fmt.Printf("%T %[1]v\n", s[i])
	}
18
uint8 109
uint8 97
uint8 103
uint8 101
uint8 100
uint8 117
uint8 233
uint8 169
uint8 172
uint8 229
uint8 147
uint8 165
uint8 230
uint8 149
uint8 153
uint8 232
uint8 130
uint8 178

for-range遍历,遍历的元素类型为字符

// 
	for _, i2 := range s {
		fmt.Printf("%T %[1]v\n", i2, i2)
	}
int32 109
int32 97
int32 103
int32 101
int32 100
int32 117
int32 39532
int32 21733
int32 25945
int32 32946
Strings库

strings提供了大多数字符串操作函数,使用方便

注意:字符串是字面常量,不可以修改,很多操作都是返回新字符串。

字符串拼接
  • Join:使用间隔符拼接字符串切片
  • Builder:多次拼接,推荐
  • fmt.Sprintf:方便快捷,推荐
a0 := "dujiea"
a1 := "测试啊"
fmt.Printf("%s %p  %s  %p\n", a0, &a0, a1, &a1)
       // 方式一:可以相同类型可以直接相加
a2 := a0 + a1
fmt.Printf("%s  %p\n", a2, &a2)
       // 方式二:join ,第一参数是一个字符串序列,第二参数是间隔符,意思是用什么间隔符将字符串序列中的元素拼接起来
a3 := strings.Join([]string{a0, a1}, "-------")
fmt.Println(a3)
fmt.Println("--------------------------------")
       // 方式三:可以进行多次拼接,还可以指定类型拼接,比如byte,string,rune
var a4 strings.Builder
a4.WriteByte(0x41)
a4.WriteString("这是测试数据")
a4.WriteRune(27979)
       // Write 可以指定字节序列
a4.Write([]byte{65, 66, 67, 68})
fmt.Println(a4.String())
       // 方式四:sprintf,返回一个新字符串
d1 := "haha"
d2 := "mmmm"
var d3 = fmt.Sprintf("%s链接%s", d1, d2)
fmt.Println(d3)
       // 方式五:bytes.Buffer ,和strings.Builder用法一样
var d4 = bytes.Buffer{}
d4.WriteRune('\x61')
d4.WriteString("这是第五种")
d4.WriteByte('\x31')
d4.Write([]byte{0x41, '\x44'})
fmt.Println(d4.String())

简单拼接字符串常用+、fmt.Sprintf。如果手里正好有字符串的序列,可以考虑join,如果反复多次拼接,strings.Builder 是推荐的方式。bytes.Buffer用法同strings.Builder

查询

下面的函数都需要两个参数,第一个参数为父字符串,第二个参数为子串,子串就是需要查询的字符

  • Index:从左至右搜索,返回子串第一次出现的字节索引位置。未找到,返回-1。子串为空,也返回0
  • LastIndex:从右至左,返回子串第一次出现的字节索引位置,未找到返回-1
  • IndexByte:从左到右搜索字节值,返回第一次出现的字节索引位置,未找到返回-1
  • IndexRune:和Index一样
  • IndexAny:从左到右搜索,找到其中一个子串就返回第一次出现的字节索引位置。
  • Contains:从左到右搜索,如果找到则返回true,没找到返回false
  • Count:从左到右搜索,返回子串出现的次数

时间复杂度为O(n),效率不高,该用则用,少用

q1 := "www.mbai我du测a.com"
// 从左到右依次查询,返回第一次出现的字节索引位置。如果未找到返回-1。 第一参数为父串,第二参数为要查询的子串
// 如果查询的子串为"" 则返回0,如果找到了返回该字节的索引位置,如果没找到返回-1 ,注意:中文字符占3个字节!!!!!
fmt.Println(strings.Index(q1, "测")) // 输出索引下标13,因为'我'占用3个字节
// 从右至左查询子串,注意:go语言没有负索引!!
fmt.Println(strings.LastIndex(q1, "我")) // 输出索引下标8
// 与Index类似
fmt.Println(strings.IndexByte(q1, 'm'))
// 与Index类似
fmt.Println(strings.IndexRune(q1, 27979))
// 从左至右搜索,找到给定的字符集字符串中任意一个字符就返回索引位置。未找到返回-1
fmt.Println(strings.IndexAny(q1, "我u"))
// 与Indexany功能一样,方向相反,从右至左
fmt.Println(strings.LastIndexAny(q1, ".m"))
// 可以判断某个元素在不在给定的字符串中,返回bool值
fmt.Println(strings.Contains(q1, ".c"))
// 返回字符出现的次数
fmt.Println(strings.Count(q1, "ww"))
大小写
  • ToLower:转换为小写
  • ToUpper:转换为大写
c1 := "wwQeA222"
fmt.Println(strings.ToUpper(c1))
fmt.Println(strings.ToLower(c1))
//输出
WWQEA222
wwqea222
前后缀

返回bool值

  • HasPrefix:是否以xxx开头
  • HasSuffix:是否以xxx结尾
q1 := "wwwww.mbai我du测a.com"
fmt.Println(strings.HasPrefix(q1, "www"))
fmt.Println(strings.HasSuffix(q1, "com"))
移除
  • TrimSpace:去除字符串两端的空白字符
  • TrimPrefix、TrimSuffix:如果开头或结尾匹配,则去除。否则,返回原字符串的副本
  • TrimLeft:字符串开头的字符如果再字符集中,则全部移除,直到碰到第一个不在字符集中的字符为止。
  • TrimRight:字符串结尾的字符如果在字符集中,则全部移除,直到碰到第一个不在字符集中的字符为止。
  • Trim:字符串两头的字符如果再字符集中,则全部移除,直到左右都碰到第一个不在字符集中的字符为止
  • ReplaceAll可以将字符串中所有的’a’或其他字符删除
fmt.Println(strings.TrimSpace("\v\n\r \tabc\txyz\t \v\r\n"))
fmt.Println(strings.TrimPrefix("www.magedu.edu-马哥教育", "www."))
fmt.Println(strings.TrimSuffix("www.magedu.edu-马哥教育", ".edu"))
fmt.Println(strings.TrimLeft("abcdddeabeccc", "abcd"))
fmt.Println(strings.TrimRight("abcdddeabeccc", "abcd"))
fmt.Println(strings.Trim("abcdddeabeccc", "abcd"))
// 删除字符串中所有的字符'a'
fmt.Println(strings.ReplaceAll(s2, "a", ""))
分隔
  • Split:按照给定的分隔子串去分隔,返回切割后的字符串切片。
    • 切割字符串时被切掉的,不会出现在结果中,比如strings.Split(s1,’,’) 逗号不会出现在切片中
s1 := "abc今天efeda,好日志,好bmmha"
fmt.Println(strings.Split(s1, ","))
//返回
[abc今天efeda 好日志 好bmmha]
  • 没有切割,也会返回一个元素的切片,元素就是被切的字符串,意思就是没切到把整个字符串转换为切片
s1 := "abc今天efeda,好日志,好bmmha"
s2 := strings.Split(s1, "fff")
fmt.Println(s2)
// 输出, 没切到,返回切片,元素是字符串
[abc今天efeda,好日志,好bmmha]
  • 分隔字符串为空串,那么返回将被切割字符串按照每个rune字符分解后转成string存入切片返回
s1 := "abc今天efeda,好日志,好bmmha"
s3 := strings.Split(s1, "")
fmt.Println(s3)
fmt.Printf("%v %T", s3[0], s3[0])
// 输出
[a b c 今 天 e f e d a , 好 日 志 , 好 b m m h a]
// 注意:每个元素都是string类型,并不是rune!!!!!!!!
a string
  • SplitN(s,sep string, n int )[] string, n 表示最终切片中有几个元素
    • n ==0,返回空切片,切成0个子串
s1 := "abc今天efeda,好日志,好bmmha"
fmt.Println(strings.SplitN(s1, ",", 0))
// 输出
[]
  • n >0 ,返回切片元素的个数
    • n==1 ,返回一个元素切片,元素为原有字符串,相当于没有进行Split
s1 := "abc今天efeda,好日志,好bmmha"
fmt.Println(strings.SplitN(s1, ",", 1))
//输出
[abc今天efeda,好日志,好bmmha]
  * n>1,按照sep切割。返回多个元素的切片。按照sep切成的段数最多有x段,当n<x时,会有部分剩余字符串未切; n==x时,字符串正好从头到尾切完,返回所有段的切片;n>x时,和n==x一样。n表示切割出来的子串的上线,即至多切片里面有n个元素
s1 := "abc今天efeda,好日志,好bmmha"
fmt.Println(strings.SplitN(s1, ",", 3))
// 返回
[abc今天efeda 好日志 好bmmha]
  • n <0 ,等价Split,能切多少切多少
  • SplitAfter和Split相似,就是不把sep切掉
  • SplitAfterN和Splitn相似,也不把sep切掉
  • Cut(s,sep string) (before, after string, found bool)
    • 内部使用index找sep,所以是从左到右搜索切割点。可以认为是切一刀,一刀两段
    • 没有切到,返回s,””,false
s1 := "abc今天efeda,好日志,好bmmha"
fmt.Println(strings.Cut(s1, "123"))
// 输出 没切到,返回原字符串 "" false
abc今天efeda,好日志,好bmmha  false
  • 切到了,匹配切割符的部分要切掉,返回切割符前部分,切割符后部分,true
s1 := "abc今天efeda,好日志,好bmmha"
fmt.Println(strings.Cut(s1, ","))
// 输出 前一段  后一段  true
abc今天efeda 好日志,好bmmha true
替换
  • Replace(s,old,new string,n int) string
    • n<0,等价ReplaceAll,全部替换
    • n==0,或old==new,就返回原字符串,表示不替换
    • n>0,至多替换n次,如果n超过找到old子串的次数x,也就只能替换x次了
    • 未找到替换出,就返回原字符串
s1 := "abc今天efeda,好日志,好bmmha"
fmt.Println(strings.Replace(s1, "今天", "你好", -1))
fmt.Println(strings.Replace(s1, "好", "坏", 1)) // 指定替换1次

//输出
abc你好efeda,好日志,好bmmha
abc今天efeda,坏日志,好bmmha

其他
  • Repact:使用给定的字符串重复n次拼接成一个新字符串
  • Map:按照给定处理每个rune字符的函数依次处理每个字符后,拼接成字符串返回。注意Map是一对一的映射,不能减少元素个数
s1 := "abc今天efeda,好日志,好bmmha"
fmt.Println(strings.Repect(s1,4)) // 这样会把s1 拼接4次输出
// 输出
abc今天efeda,好日志,好bmmhaabc今天efeda,好日志,好bmmhaabc今天efeda,好日志,好bmmha

// 使用strings.Map 函数可以对每个rune字符进行处理然后并将他们拼接成一个字符串返回
fmt.Println(strings.Map(func(r rune) rune {
	if r == 'a' {
		r = '测'
	}
	return r
}, s1))

四、类型转换

数值类型转换
  • 低精度向高精度转换可以,高精度向低精度转换会损失精度
  • 无符号向有符号转换,最高位是符号位
  • byte和int可以相互转换
  • float和int可以相互转换,float到int会丢失精度
  • bool和int不能互相转换
  • 不同长度的int和float之间可以互相转换
// 无符号和有符号之间的转换
// 反码:符号位不变,按位取反
// 补码:在反码基础上最末尾+1
var a int8 = -10
1 000 1010  // 这是有符号的二进制数,最高位为符号位,如果是负数则为1,正数则为0
1 111 0110  // 按照上面的转换规则,符号位不变还是1,其他位取反,就是1 111 0101 ,然后在反码的基础上最末尾+1就成了1 111 0110 所以11110110转换为十进制就是246  
var b uint8 = uint8(a) 
fmt.Println(a, b)
//输出
-10 246
fmt.Println(int(3.14)) // 错误,不允许无类型float常量转到int
var a = 3.14 // 定义有类型变量转换就没有问题
fmt.Printf("%T: %[1]v => %T %[2]d\n", a, int(a)) // float64: 3.14 => int 3
// byte rune本质上就是整数和无类型常量可以直接计算,自动转换
b := 'a'
c := b + 1
fmt.Printf("%T %[1]v", c) // 请问c显示什么,什么类型
// c 显示98,rune int32类型

但是,如果不使用无类型常量,有类型的计算如果类型不一致要报错,因为go是对类型要求非常严苛的语言,要强制类型转换。

b := 'a'
e := 1 c := b + e // rune和int类型不能加,必须转换。比如c := int(b) + e或c := b + rune(e)
fmt.Println(c)
类型别名和类型定义
var a byte = 'C'
var b uint8 = 49
fmt.Println(a, b, a+b) // 为什么类型不同,可以相加?// 因为byte就是uint8的别名

原因是在源码中定义了 type byte = uint8 ,byte是uint8的别名。

Go v1.9开始使用了类型别名,将byte等的定义修改成了类型别名的方式。

别名说明就是uint8的另外一个名字,和uint8是一回事。再看一段代码,正确吗?

type myByte uint8
var a byte = 'C'
var b uint8 = 49
fmt.Println(a, b, a+b) // 为什么类型不同,可以相加?byte本质上讲就是uint8
var c myByte = 50
fmt.Println(a, c, a + c) // 可以吗?为什么? 不可以,因为myByte是全新类型

答案是不可以。原因就是Go原因不允许不同类型随便运算。就算我们眼睛看到可以,也不行,必须强制类型转换,转换是否成功,程序员自己判断

type myByte uint8 // 类型定义
type byte = uint8 // 类型别名

byte只是存在于代码中,为了方便阅读或使用,编译完成后,不会有byte类型。

字符串转换
fmt.Println(string(126)) // string 会把
fmt.Println(strconv.Itoa(126))
fmt.Printf("%T %[1]v\n", strconv.Itoa(126))
// 输出
126
string 126

//fmt.Println(strconv.Itoa(126))
s1 := "126"
fmt.Println(strconv.Atoi(s1)) // 十进制理解126
fmt.Println(strconv.ParseInt(s1, 10, 32))
fmt.Println(strconv.ParseInt(s1, 16, 32)) // 相当于把126转成16进制,1*16^2 + 2*16^1 + 6*16^0 = 256+32+6 = 294
// 输出
126 <nil>
126 <nil>
294 <nil>

s2 := "115.6"
fmt.Println(strconv.ParseFloat(s2, 64))
fmt.Println(strconv.ParseFloat(s2, 64))
fmt.Println(strconv.ParseBool("false"))
// 输出
115.6 <nil>
115.6 <nil>
false <nil>