需求=过去困惑+当下渴望+未来希望 ——实干《实干日记》
5-1 为什么len函数获取中文字符串长度有问题?
我们将讲解go语言的字符串相关的操作。
因为字符串本身是非常重要,而且它相对的方法也比较多,所以说我们这里边单独拿一个关卡出来专门讲解字符串。
字符串不管在任何语言当中,它的使用都是非常频繁的。
在Python当中基本上采用的字符串操作,在go当中基本上也是这么回事,它也是这些基本操作。
1.求解字符串的长度。
它有一个内置的函数叫len,名字跟Python是一样的。
比如说这里边我们定义一个字符串,字符串的定义我们可以直接这样来定义,比如说name,然后它是有类型的叫string,然后赋值赋值跟Python是一样的。
注意一下,这里不能用单引号,单引号 引起来/包裹起来 的单个符号是字符。
那就引出了一个问题:
在go语言中,字符和字符串有啥区别?
2.字符和字符串有啥区别
我们通过代码来说明,就不用文字来表述了。
文件位置:c06/demo1.go
package main
import "fmt"
var global int = 1024
func main(){
str := "keegan"
fmt.Printf("%T,%v,%s\n",str,str,str)
var ch1 rune = 'A'
fmt.Printf("%T,%v,%c\n",ch1,ch1,ch1)
var ch2 byte = 'B'
fmt.Printf("%T,%v,%c\n",ch2,ch2,ch2)
var ch3 int32 = 'C'
fmt.Printf("%T,%v,%c\n",ch3,ch3,ch3)
var (
name string = "程咬铁"
age int32 = 18
salary float32 = 12580
)
fmt.Printf("name=%s,age=%d,salary=%f\n",name,age,salary)
fmt.Printf("global=%d\n",global)
global += 1024
fmt.Printf("global=%d\n",global)
}
/*在 demo1.go同级目录执行 go run demo1.go
输出结果如下:
string,keegan,keegan
int32,65,A
uint8,66,B
int32,67,C
name=程咬铁,age=18,salary=12580.000000
global=1024
global=2048
*/
解读代码信息:
/*
*1. 定义变量的方法
(1)标准格式如下
var 变量名 变量类型
我们可以用 var name type 的方式来定义变量。
其中,var 是声明变量的关键字,name是变量名,type是变量的类型
例如 var ch3 int32 = 'c'
var 是定义变量的关键字
ch3是变量的名字
int32 是变量的类型
字符 'c' 是变量的值。赋值 =
凡是变量,必有3个基本要素:
变量的类型 + 变量的名称 + 变量的值
(2)简短格式如下
名字 := 表达式
必须注意以下3点:
a. 只用来定义变量,但与此同时也会显示初始化
b. 不提供数据类型
c. 只用在函数内部,即它是一个局部变量而非全局变量
(3) 批量格式
形如:
var (
age int
name string
salary float32
)
*2. 变量的作用域
分为局部变量 和 全局变量
局部变量:在函数内部定义的变量
例如 在main函数体内定义的变量都是局部变量
作用域在函数体内
全局变量:在函数外部定义的变量
形如:
var global int = 1024
*3. 格式化输出【格式控制符,又叫占位符】
(1)%T %T是go语法打印 值的类型
(2)%v %v是默认 打印值
(3)%c %c是接收单个字符,输出单个字符
(4)%d %d接收整型的数据,输出整型的数据
(5)%f %f接收小数,输出小数
(6)%s %s接收字符串,输出字符串
*4 Printf 和 Println的区别是什么
两者之间除了换行之外,还有:
Println 可以打印出字符串变量,整型变量,浮点型变量
比如打印字符串变量:
fmt.Println(name) // 程咬铁 //同时换行一定要注意细节
比如打印浮点型变量:
fmt.Println(salary) // 12580 //同时换行一定要注意细节
发现问题:输出结果是 是12580而不是12580.000000
读起来12580看起来像整型一样,为了提高可读性:
用格式控制符 + Printf 来格式化输出会大大提高可读性
就不要用Println来输出除字符串以外的其他变量了,可读性低
//==============================================
Printf 只可以打印出格式化的字符串变量。如果要输出其他类型的变量,用格式控制符
比如输出字符串类型的变量 fmt.Printf(name) // 程咬铁 // //不会换行一定要注意细节
如果是输出浮点型的变量,那么
fmt.Printf(salary)
// cannot use salary (type float32) as type string in argument to fmt.Printf
//========总结========
用Println 来输出字符串类型的变量
用Printf 输出字符串类型的变量,更多的是格式化输出
*/
回答开始的问题:字符和字符串有啥区别?
字符定义:
形如 var ch3 int32 = 'C'
字符变量 ch3 的值是 用单引号 括起来的 大写的 C
字符串定义:
形如 str := "keegan"
字符串变量 str 的值 使用双引号 括起来的 keegan 这 6个字符。
字符串 是 一连串字符的集合。
特别地还有 空白字符的字符串【空字符串】,单个字符的字符串。
可以这样定义:
到这里,处理完问题2之后,再继续看 1.求解字符串的长度
3.继续看 1.求解字符串的长度
我们先定义以下字符串:
部分代码如下:
var name string = "keegan:写博客" // 一共由10个字符组成
fmt.Println(len(name)) // 16
这里面怎么求它的长度呢?
用len函数。跟Python中的求字符串的长度用的函数都是len函数。
前置信息铺垫:
存储空间的基本单位是 字节。
比如
在go中,1个英文的字符占用1个字节,1个中文的字符占用3个字节。【自己写字符串测试结果】
对于len函数:
用途:用来获取字符串的字节长度,返回的是字节的长度。
处理方法:在go的字符串中,1个英文的字符占用1个字节,1个中文的字符占用3个字节。【自己写字符串测试结果】
我们直接来打印一下,它返回的是16。
为什么是16呢?这里面的逻辑是什么?
用Python来打印:
print(len("keegan:写博客")) # 10
这里又引出了一个问题:
在go语言中,英文字符和中文字符计算长度的方法是怎样的?
在go中,1个英文的字符占用1个字节,1个中文的字符占用3个字节。【自己写字符串测试结果】
我们要知道这个英文字符和中文字符它们的编码规则是不一样的,我们如果把中文内容 写博客 给它去掉,只要是英文加上特殊符号,它的长度就是准确的:
var name string = "keegan:"
fmt.Println(len(name)) // 7
我们只要不是中文,然后这里边当然其他国家的语言也不行,一旦涉及到中文或其他国家的语言例如日文,韩文就不准确了。
对于英文来说,英文它实际上是ASCII码来做的,ASCII是一套编码标准,用于显示英文的。128个字符
通过上图,我们知道了,不同的语言,用的编码标准是不一样的,计算语言符号占用的字节也不一样。
我们这里简化问题一下:
英文在计算机里用的是ASCII编码标准,1个英文字符占用1个字节。
中文在go语言中采用的是Unicode,1个汉字占用3个字节。
Unicode实现方式又包括UTF-8、UTF-16、UTF-32。
以上编码标准有啥意义?
就告诉了计算机 汉字/英文字符 用什么数字代替的?
因为计算机的存储单位是位(bit),每1位的状态用0或1表示。
英文字符 用数字表示:
A:65 B:66 C:67 …依次递增
a:97 b:98 c:99 …依次递增
汉字的话:
首先,计算机存储的是用二进制来实现的。
把所有国家的字符(英文,中文等)跟数字做好了映射,统一了标准之后,就不存在语言的编码冲突了【一说65对应的啥字母你就能根据标准计算出是A,一说97对应的啥字母你就能根据标准计算出是a】,计算机会根据这套标准帮我们实现了跨语言,跨平台进行的文本转换和处理,这样,在电脑屏幕下,就能显示任何语言的内容了。
回归正文:
到这里,我们知道了:
英文在计算机里用的是ASCII编码标准,1个英文字符占用1个字节。
中文在go语言中采用的是Unicode字符集,1个汉字占用3个字节。
1字节 = 8 位
一共有 2的8次方 种组合方式,即256种组合方式。
对于中文来说,一个字节肯定是不能够把所有中文编进来的,因为一个字节的存储上限是256,所以说对于中文来说,它要用3个字节才能够把所有的汉字全部包括进来,或者说是在Unicode中文的数字区域内,它用3个字节才能把所有的中文的汉字给囊括进来。
所以说我们就要知道UTF-8编码,它实际上是用来保存Unicode的这种字符集的一种实现方式。【上文提到过还有UTF-16】
对于go语言来说,go语言对于字符串它采用了UTF-8编码,那UTF-8编码除了能够用三个字节表示一个中文之外,它还能够用一个字节表示英文,这什么意思呢?
实际上很简单,对于英文字符来说它的数字区域,大家可以去看一下Unicode的字符集或查看上文内容,用一个字节就已经能够存储英文字符了,而且是绰绰有余。
对于utf8编码来说,utf8又是一个动态编码,也就是说它不是说所有的字符全部采用等长的这种直接占用,那这样的话会浪费空间,所以说对于英文的编码来说,因为英文的它的编号正好是小于256的,所以说它就用一个字节来表示,就够了。
到这里,就知道了在go语言当中计算英文和中文占用多少字节的算法原因。
再看代码:
var name string = "keegan:写博客" // 一共由10个字符组成
fmt.Println(len(name)) // 16
为什么是7*1 + 3*3 = 16
?而不是10 * 3 = 30
?
我们可以试想一下,如果说每一个字符它都占3个字节的话,那这里边总共有10个字符,那它整个就应该占用10 * 3 = 30
个对不对?
但是输出结果是16个,这说明后边3个中文总共占用9个字节,前边7个字符它总共占用7个字节,
总共加起来就是16个对吧?我们也可以只使用中文来验证一下。
var name string = "写博客"
fmt.Println(len(name)) //9
然后再一点一点地加点东西:
var name string = ":写博客"
fmt.Println(len(name)) //10
var name string = "\n写博客" //转义字符也是一个字符,占用1个字节
fmt.Println(len(name)) //10
好了,那对于我们来说这个问题它就产生了一个需求,怎么能得到它真正的长度呢?
这实际上就要涉及到类型转换。
我们可以将name给它转换一下,把它转换成什么类型呢?
把它转换成我们的数组的类型,数组的类型我们在后边也会介绍到。这个数组和我们平时写的数组也有很大的差异,我们把它转成什么数组?
rune
rune它实际上本质上是int32,那是因为它对于这种字符的处理,它主要用于字符的处理,所以说对于int32,我们专门又给它命了个别名rune,专门用于这种字符的处理。
int32是4个字节,int64是8个字节
实际上我们把rune写成int32也是一样的。
看代码:
var name string = "keegan:写博客"
fmt.Println(len(name)) // 16
name_arr := []int32(name)
fmt.Println(len(name_arr )) // 10
在这里边什么意思呢?
这里边的意思就是将你的每一个字符,注意一下,每一个字符都转换成一个rune。
对于这种只有占用一个字节的字符的情况,即英文字符,它也会转成一个rune。
对于汉字这种来说,本身你占用3个字节,现在把你转化成int32了,已经占4个字节了,那这样的话大家看看你的中文,我全部都能包含进来对不对?无非多浪费一个空间。【4-3=1】
对于英文字符这种只占用1个字节来说,我给它又加了3个字节,但是它们加起来是一个rune。
对于这种只有3个字节的中文来说,我再给它加1个字节,让它凑够4个字节,也就是1个rune。
这样的话总共有多少个rune呢?
10个。因为这里是10个字符,每个字符都转化成rune了。
我们通过将字符串进行类型转换,转换成rune,再借用len函数计算出rune的长度,这个长度就字符串的长度。
即 rune的长度 = 字符串的长度
其实就是通过类型转换将求字符串长度转化成求rune的长度的问题。
rune的长度就像求数组的长度一样,里面有几个字符rune的长度就是几。
对于len来说,它能够求出数组的元素的个数,就能得到数组的长度。跟Python很相似。
再来看代码:
var name string = "keegan:写博客"
fmt.Println(len(name)) // 16
name_arr := []rune(name) // []int32(name)
fmt.Println(len(name_arr )) // 10
现在我们就能够得到它的长度是10个了,对不对?
通过这样的类型转化,我们做到了可以从数字符串有几个字符/元素就能准确计算出字符串的长度就是几了。
即通过这样的类型转化, 字符串的个数 = 字符串的长度
但是我们也知道了这样做它实际上是浪费空间,你本身占的字节没有那么多,因为转成rune的话它占用的空间会多很多。
int32是4个字节,int64是8个字节。
以上是求长度的问题。
多说一点:
字符串可以看成字符数组,用索引取值:
var name string = "keegan:写博客"
fmt.Println(len(name)) // 16
name_arr := []rune(name)
fmt.Println(len(name_arr )) // 10
fmt.Printf("%T,%c\n",name_arr,name_arr[9]) // []int32,客
5-2 什么是转义符?
看例子:
看定义:
斜线 \
+ 特殊符号 = 转义字符
一个转义字符是一个整体,不会单独出现,占用一个字节。
看花容易绣花难。用起来。
示例代码:
/*
*1. 我要 输出 2021\10\24
实例代码:
package main
import "fmt"
func main(){
var date string = "2021\\10\\24" // 转义字符 \\ 等价于 \。即我要想输出\,用转移字符\\表示
// 直接输出字符串可以用Println。格式化输出用Printf
fmt.Println(date) // 2021\10\24
}
*2.我要 输出 keegan:CSDN代码写注释
实例代码:
package main
import "fmt"
func main(){
var name string = "keegan:CSDN代码写注释"
fmt.Println(name) // keegan:CSDN代码写注释
}
*3我要输出 keegan:"CSDN代码写注释"
实例代码:
package main
import "fmt"
func main(){
var name string = "keegan:\"CSDN代码写注释\"" // 用转移字符 \" ---> 表示 "
fmt.Println(name) // keegan:"CSDN代码写注释"
}
*/
看问题:
为什么会有转移符?
(0)表示特殊符号
例如:【见上述示例代码】
// 用转移字符 \" ---> 表示 "
// 用转移字符 \\ ---> 表示 \
(1)字符串嵌套问题
认识字符串嵌套问题:
package main
import "fmt"
func main(){
var name string = "keegan:"CSDN代码写注释"" // syntax error: unexpected CSDN代码写注释 at end of statement
fmt.Println(name)
}
如果按照执行上述代码,那么会报syntax error: unexpected CSDN代码写注释 at end of statement
的异常。
意思是 语法错误:不是被期望的声明。
怎么解决呢?
Python中解决简单:
print("keegan:'CSDN代码写注释'") # keegan:'CSDN代码写注释'
# 或者
print('keegan:"CSDN代码写注释"') # keegan:"CSDN代码写注释"
来看看Go中值怎么解决的。【你已经知道了用 用转移字符 " —> 表示 " 的知识来解决】
解决字符串嵌套问题的办法:
package main
import "fmt"
func main(){
var name string = "keegan:\"CSDN代码写注释\""
fmt.Println(name) // keegan:"CSDN代码写注释"
}
但是不能写成如下:
package main
import "fmt"
func main(){
var name string = "keegan:'CSDN代码写注释'"
fmt.Println(name) // keegan:'CSDN代码写注释'
}
虽然能输出结果,但是'CSDN代码写注释'
它既不是字符,也不是字符串,不要这么写。
少即是多。Go语言用 "abc"
的形式表示字符串,只有这一种方法。不像Python有3种。
Go表示字符是 'A'
。Python中没有字符的概念。
除此之外,Go表示字符串,还可以用 abc
的方法。即原样字符的方式展现出来。
看代码:
package main
import "fmt"
func main(){
var name string = `keegan:CSDN代码写注释`
fmt.Println(name) // keegan:CSDN代码写注释
}
package main
import "fmt"
func main(){
var name string = `keegan:"CSDN代码写注释"`
fmt.Println(name) // keegan:"CSDN代码写注释"
}
像这种 keegan:"CSDN代码写注释"
方式也能输出任意的字符串,也可以解决字符串嵌套的问题。
后面讲解 json tag的时候,会见到这种形式。
5-3 子串查询、子串统计、开始和结尾判断
认知常用的字符串操作的函数。
package main
import (
"fmt"
"strings"
)
func main(){
var name string = "keegan:CSDN代码写注释"
// 1.name中 是否包含某个子串
fmt.Println(strings.Contains(name,"代码写注释")) // true
// 2.查看 name中 的子串的开始的位置
fmt.Println(strings.Index(name,"代码写注释")) // 11。下标从0开始计数。
// 3.统计出现的次数
fmt.Println(strings.Count(name,"e")) // 2
// 4.以什么开头以什么结束。前后缀问题
fmt.Println(strings.HasPrefix(name,"k")) // true
fmt.Println(strings.HasSuffix(name,"释")) // true
}
在Python中,是用 in
这个关键字来做的。
name = "keegan:CSDN代码写注释"
if "代码写注释" in name:
print("true") # true
name.index("代")
name.count("e")
name.startswith("k")
name.endswith("释")
5-4 子串的替换、连接和分割
在Python中:
print("keegan".upper())
print("KEEGAN".lower())
package main
import (
"fmt"
"strings"
)
func main(){
//var name string = "keegan:CSDN代码写注释"
// 1.大小写转化
fmt.Println(strings.ToUpper("keegan")) // KEEGAN
fmt.Println(strings.ToLower("KEEGAN")) // keegan
//2.字符串的比较。其实是ascii值的比较
// 如果字符串a小于字符串b,则返回 -1
fmt.Println(strings.Compare("a","b")) // -1
// 从第一个字符逐个比较ascii值的大小
fmt.Println(strings.Compare("ab","abc")) // -1
// 如果字符串a大于字符串b,则返回 1
fmt.Println(strings.Compare("ac","aa")) // 1
// 如果字符串a等于字符串b,则返回 0
fmt.Println(strings.Compare("你在教我做事","你在教我做事")) // 0
//3. 去掉空格 和指定的字符串
fmt.Println(strings.TrimSpace("hello man")) //hello man
// 去掉前后空格
fmt.Println(strings.TrimSpace(" hello man ")) //hello man
// 去掉指定的字符。去掉中间空格的话将 h 换成 空格字符即可
fmt.Println(strings.TrimLeft("hello man","h")) //ello man
// 左右两边都有h
fmt.Println(strings.Trim("hah","h")) // a
// 只有左边有h
fmt.Println(strings.Trim("hello","h")) // ello
// 只有右边有h
fmt.Println(strings.Trim("efgh","h")) // efg
//4. 字符串切割 split
// 把&给去掉并替换成空格,返回一个数组
arrs := strings.Split("I&love&Go,&and&I&also&love&Python","&")
fmt.Println(arrs) // [I love Go, and I also love Python]
// 详细参考:https://blog.csdn.net/TCatTime/article/details/100511043
//5. 字符串合并 join [常用]。将字符串数组连接起来
// Join连接其第一个参数的元素来创建一个字符串。分隔符sep放在结果字符串的元素之间。
fmt.Println(strings.Join(arrs,"-")) // I-love-Go,-and-I-also-love-Python
// 详细参考:https://segmentfault.com/a/1190000012978989
arr02 := []string{"北京","欢迎","你呀"}
fmt.Println(strings.Join(arr02,"-")) // 北京-欢迎-你呀
//6. 字符串替换。从左到右
fmt.Println(strings.Replace("keegan:18 phone:18825478963","18","24",1)) // keegan:24 phone:18825478963
}
5-5 格式化的输入和输出
可读性差的一种写法:
package main
import (
"fmt"
"strconv"
)
func main(){
name := "keegan"
age := 18
fmt.Println("name:"+name+",age:"+strconv.Itoa(age)) // name:keegan,age:18
}
完整代码:
/*
*字符串的基本操作
*/
package main
import (
"fmt"
"strconv"
)
func main(){
demo := 123456
name := "keegan"
age := 18
fmt.Println("name:"+name+",age:"+strconv.Itoa(age)) // name:keegan,age:18
// 不管值是什么类型的,是什么值就打印什么值
fmt.Printf("name:%v,age:%v\n",name,age) // name:keegan,age:18
// go语法打印
fmt.Printf("name:%#v,age:%#v\n",name,age) //name:"keegan",age:18
// 类型打印
fmt.Printf("name:%T,age:%T\n",name,age) // name:string,age:int
// 必须显示正负符号
fmt.Printf("name:%s,age:%+d\n",name,age) // name:keegan,age:+18
// 宽度为4右对齐
fmt.Println(demo)
// 宽度是4,18占了2个,4-2=2,余下的2个位置在18的左边补空格
fmt.Printf("%4d\n",age) // 18
// 宽度为4左对齐
fmt.Println(demo)
// 宽度是4,18占了2个,4-2=2,余下的2个位置在18的右边补空格
fmt.Printf("%-4d,\n",age) //18 ,
// 把18显示成二进制
fmt.Printf("%b\n",age) // 10010
// 把18显示成八进制
fmt.Printf("%o\n",age) // 22
// 把18显示成十六进制
fmt.Printf("%x\n",age) // 12
// Sprintf除了格式化输出 还返回一个字符串
desc := fmt.Sprintf("name:%s,age:%d\n",name,age)
fmt.Println(desc) // name:keegan,age:18
// 我想知道数字65对应的ascii值是多少
ch1 := 65
fmt.Printf("%c\n",ch1) // A
// 把字符的单引号 '' 显示出来
fmt.Printf("%q\n",ch1) // 'A'
// 科学计数法
fmt.Printf("%e\n",3.14) // 3.140000e+00
// 十进制小数
fmt.Printf("%f\n",3.14) // 3.140000
// 字符串的宽度,缩进
str1 := "hello"
// 宽度为9左对齐,9-5=4,余下的4个在hello的右边补空格
fmt.Printf(",%-9s,\n",str1) // ,hello ,
// 宽度为9右对齐,余下的4个向左边补空格
fmt.Printf(",%+9s,\n",str1) // , hello,
// 正右它左补,负左它右补
// 输入
var (
nm string
salary float32
)
fmt.Println("请输入姓名和薪水:")
fmt.Scanln(&nm,&salary) // 注意按空格输入间隔
// 注意四舍五入的问题。默认接受7位有效数字,第8位开始四舍五入
fmt.Println(nm,salary)
// 类型要匹配。前面是格式化字符串,后面是 取址运算符& + 变量名
fmt.Println("请输入姓名和薪水:")
fmt.Scanf("%s %f",&nm,&salary) // keegan 3.14 // 注意敲的细节:敲keegan敲1个空格再敲3.14
fmt.Println(nm,salary) // keegan 3.14
}