这次接着上次的学习笔记,也是属于go语言的基础语法部分,为啥将两次都分开,是我感觉今天这部分内容比上一篇更加复杂一点,废话不多说,现在就开始吧
先是比较重要的错误处理,Go语言设计者并没有按照现在主流的java,c++等编程语言那样提供异常的结构化try-catch-finally错误处理机制,而是选择了c语言经典的错误机制:错误就是值,而错误处理就是基于值比较后的策略,也就是说在Go中我们不需要额外的语言处理机制去处理它们,而只需要利用已有的语言机制,像处理其他普通类型值一样去处理错误。
我们先来看如何构造错误值,构造错误值一般有两种方式,errors.New(),fmt.Errorf,示例如下:
package main
import (
"errors"
"fmt"
)
func main() {
var err error//和其他类型一样可以定义错误类型
err = errors.New("this is a demo error")//相当于将后面的值赋值给err
fmt.Println(err)//this is a demo error
i := 10
if i > 5 {
errWithCtx := fmt.Errorf("index %d is out of bounds", i)//这里并不会输出,只会将后面的值赋值给前面的变量
fmt.Println(errWithCtx)//index 10 is out of bounds
}
}
然后我们来一起看看如何处理错误值,一般我们都是采用透明错误处理策略,这也是最简单的错误策略,完全不用关心返回错误值携带的具体上下文信息,只要发生错误就进入唯一的错误处理执行路径,为啥说是透明的呢,原因是上文介绍的两种基本错误值构造方法errors.New和fmt.Errorf构造的错误值对错误处理方是透明的,(注意:按照惯例,函数或方法通常将error类型返回值放在返回值列表的末尾)示例如下:
package main
import (
"errors"
"fmt"
)
func sumx(a int, b int) (sum int, err error) {
sum = a + b
if sum < 0 {
return sum, errors.New("sum is litter than 0")//我们可以很清楚的看到错误值是什么,这就是透明的
}
return sum, nil
}
func main() {
sum, err := sumx(-10, 5)
if err != nil {//发生了错误就进入这个路径,也就是错误不为空
fmt.Println(err)//错误是一个值,输出:sum is litter than 0
return
}
fmt.Println(sum)
}
错误处理总结,其实我们在编写代码时,要始终想着错误处理,在可能发生错误的地方先构造错误,然后进行错误处理,这里我所讲述的构造错误值和处理错误值的方法是最基础的,但是实际上还有多种错误处理的方式,需要我们自己去研究,像"哨兵" 错误处理策略,错误值类型检视策略等,但是我们需要牢记的是没有哪一种错误处理策略适用于全部的项目和场合,所以多掌握几种方式才可以应变更多场合
接下来字符串的操作,这部分其实就是了解对字符串操作的一些方法,我分两部分来讲述,先是字符串的高效构造,然后是字符串常用的一些方法
字符串的高效构造,很多人这个时候就会有疑问了,字符串构造不就直接用+/+=操作符连接就好了嘛,这种字符串连接构造当然是最自然的,开发体验最好的,但是GO语言还提供了其他一些构造字符串的方法,比如:fmt.Spritnf,strings.Join,strings.Builder,bytes.Buffer,那这些方式哪种最高效呢
这里直接给出结果: 做了预初始化的strings.Builder连接构建字符串效率最高,接着第二档就是带了预初始化的bytes.Buffer和strings.Join,这两种方法效率特别相近,分别为二三名,未做预初始化的strings.Builder,bytes.Buffer和操作符连接在第三挡次,最后就是fmt.Spritnf性能最差,排在末尾。这里测试的结果可以由基准测试得出,具体测试过程太过繁杂,这里就直接给出结果了。
通过结果可以得出结论:在能预测出最终字符串长度的情况下,使用预初始化的strings.Builder连接构建字符串效率最高;strings.Join连接构建字符串的平均性能最稳定,如果输入的多个字符串是以[]string承载的,那么strings.Join也是不错的选择;使用操作符连接的方式最直观,最自然,在编译器知晓欲连接的字符串个数的情况下,使用此种方式可以得到编译器的优化处理;fmt.Spritnf虽然效率不高,但是也不是一无是处,如果是由多种不同类型变量来构建特定格式的字符串,那么这种方式还是最合适的。
这里给出这几种构建方法的使用,理解各个构建方法的特性很重要,但是知晓其用法更为重要
package GOTest
import (
"bytes"
"fmt"
"strings"
)
var s1 []string = []string{
"rob",
"mike",
"ken",
}//字符串数组
func concatStringByOperator(s1 []string)string{
var s string
for _,v:=range s1{
s+=v//操作符连接,直接连接
}
return s
}
func concatStringBySprintf(s1 []string)string{
var s string
for _,v:=range s1{
s = fmt.Sprintf("%s%s",s,v)//将s,v连接
}
return s
}
func concatStringByJoin(s1 []string)string{
return strings.Join(s1, "")//s1表示连接的字符串,后面的是最终字符串之间的分隔符
}
func concatStringByStringBuilder(s1 []string)string{
var b strings.Builder//字符串构建器
b.Grow(64)//字符串构建器大小为64
for _,v:=range s1{
b.WriteString(v)//写入字符串,连接字符串
}
return b.String()//输出整条字符串
}
func concatStringByBytesBuffer(s1 []string)string{
var b bytes.Buffer//字节缓存
buf := make([]byte,0,64)//设置一个字节数组大小为64
b = *bytes.NewBuffer(buf)//字节缓存空间为64
for _,v:=range s1{
b.WriteString(v)//写入字符串,连接字符串
}
return b.String()
}
字符串的一些常用操作,注释中解释了对应方法的效果
package main
import (
"fmt"
"strings"
)
func main() {
a:="hello"
fmt.Println(strings.Contains(a, "ll")) //判断a中是否有ll
fmt.Println(strings.Count(a, "l")) //a中有多少个l
fmt.Println(strings.HasPrefix(a, "he")) //a的前缀是不是he
fmt.Println(strings.HasSuffix(a, "lo")) //a的后缀是不是lo
fmt.Println(strings.Index(a, "he")) //he在a中的位置为he最前面一个字母h的位值
fmt.Println(strings.Join([]string{"he", "llo"}, "-")) //在he和llo中间加-
fmt.Println(strings.Repeat(a, 2)) //重复输出a两次
fmt.Println(strings.Replace(a, "e", "E", 2)) //将a中前两个字母中的e取代为E,若把2改为-1则将字符串中全部的e都取代为E
fmt.Println(strings.Split("a-b-c", "-")) //将-删除
fmt.Println(strings.ToLower(a)) //都小写
fmt.Println(strings.ToUpper(a)) //都大写
fmt.Println(len(a)) //一个字母一个字节
b := "您好"
fmt.Println(len(b)) //一个汉字三个字节
}
接下来看字符串的格式化处理,用到Printf这种后缀带f的,%v为任意类型,都可以用这个格式
package main
import "fmt"
type point struct {
x, y int
}
func main() {
s := "hello"
n := 123
p := point{1, 2}
fmt.Println(s, n)
fmt.Println(p)
fmt.Printf("s=%v\n", s)
fmt.Printf("n=%v\n", n)
fmt.Printf("p=%v\n", p)//输出所有的值,p={1 2}
fmt.Printf("p=%+v\n", p)//先输出字段名字,再输出字段的值,p={x:1 y:2}
fmt.Printf("p=%#v\n", p)//输出结构体的名字,再输出结构体{字段名字+字段值}p=main.point{x:1, y:2}
f := 3.1414526535
fmt.Printf("%.2f\n", f)//保留两位小数
fmt.Printf("%v\n", f)
}
然后就是字符串的转换,将字符串与其他类型的相互转换,这里用到了标准库里面的strconv包 这里用到了其中比较常用的几个方法,对于strconv包更多的操作,可以查看相关的使用说明文档
Atoi:string类型转换为int类型;Itoa:int类型转换为string类型
package main
import(
"fmt"
"strconv"
)
func main() {
n1:=16
str:=strconv.Itoa(n1)
fmt.Printf("类型:%T,值:%s",str,str)//类型:string,值:16
n2, _ := strconv.Atoi("123") //把一个十进制字符串转换为整形
fmt.Printf("类型:%T,值:%d",n2,n2)//类型:int,值:123
n2, err := strconv.Atoi("aaa")
fmt.Println(n2, err)//如果字符串不能转换成整形就会返回error
}
FormatInt:将数字转换成指定进制数并以string类型返回
FormatInt(i int64,base int) string,第一个参数为指定数字,类型为int64,第二个参数为指定进制。返回参数转换为指定进制数后的字符串
package main
import(
"fmt"
"strconv"
)
func main() {
var num int64 = 2
str := strconv.FormatInt(num,2)
fmt.Printf("类型:%T,值:%s",str,str)//类型:string,值:10
}
上面数字皆代表的是有符号的int64类型,与之对应的还有无符号的uint64类型的FormatUint
ParseInt:给定基数(进制数)和位数,返回对应十进制的值
ParseInt(s string,base int,bitSize int) (i int64,err error),第一个参数s为待解析的字符串,第二个参数base为基数,也就是第一个参数的进制数,从0,2到36进制,第三个参数为位数,0、8、16、32、64位分别对应int、int8、int16、int32、int64,超过这个范围会返回错误,当然小于0也会返回错误。第一个返回值位转换之后的数字,第二个就是错误信息
import (
"fmt"
"strconv"
)
func main() {
parseInt, err := strconv.ParseInt("100", 2, 64)
if err != nil {
fmt.Println(err.Error())
return
}
println(parseInt) // 4
parseInt2, err := strconv.ParseInt("666", 2, 64)
if err != nil {
fmt.Println(err.Error()) // strconv.ParseInt: parsing "666": invalid syntax
return
}
println(parseInt2)
}
第一个是将二进制数字100转换为十进制为4,第二个由于二进制没有666这种表示,所以返回错误信息strconv.ParseInt: parsing "666": invalid syntax
ParseFloat:字符串转换为浮点类型
ParseFloat(s string,bitesize int) (f float64,err error),第一个参数为待转换的字符串,第二个为转换后的位数,返回浮点数和错误信息
import (
"fmt"
"strconv"
)
func main() {
num, err := strconv.ParseFloat("11.05", 64)
if err != nil {
return
}
fmt.Println(num)
}
FormatFloat:根据格式 fmt 和精度 prec 将浮点数 f 转换为字符串
FormatFloat(f float64, fmt byte, prec, bitSize int) string,第一个参数为待转换的浮点数f,第二个参数为格式,可选值有b e E f g G x X,第三个参数为保留几位小数,第四个为位数大小,返回字符串,这个会四舍五入
import (
"fmt"
"strconv"
)
func main() {
str := strconv.FormatFloat(5.26, 'f', 1, 64)
fmt.Println(str) // 5.3
}
格式说明:
格式 fmt 是'b'(-ddddp±ddd,二进制 index )、'e'(-d.dddde±dd,十进制 index )、'E'(-d.ddddE±dd,十进制 index )之一), 'f' (-ddd.dddd, 无 index ), 'g' ('e' 用于大 index , 'f' 否则), 'G' ('E' 用于大 index , 'f' 否则), 'x'(-0xd.ddddp±ddd,十六进制分数和二进制 index ),或'X'(-0Xd.ddddP±ddd,十六进制分数和二进制 index )。
字符串的操作讲完之后就是对json的操作,go语言中对json的操作非常简单,对于一个已有的结构体,只需要保证每个变量的首字符大写,就可以序列化,转换为json
序列化的方法,json.Marshal()会返回一个json字符串和一个error,序列化相对的就是反序列化,json.Unmarshal()只会返回一个error,json字符串转换到一个空的变量,这个变量的地址是这个方法的参数
package main
import (
"encoding/json"
"fmt"
)
type userInfo struct {
Name string
Age int `json:"age"`//转化为json就是age
Hobby []string
}
func main() {
a := userInfo{Name: "wang", Age: 18, Hobby: []string{"Golang", "TypeScript"}}
buf, err := json.Marshal(a)//将结构体序列化,转换成json的字符串
if err != nil {
panic(err)
}
fmt.Println(buf) // [123 34 78 97...]
fmt.Println(string(buf)) // {"Name":"wang","age":18,"Hobby":["Golang","TypeScript"]}
var b userInfo
err = json.Unmarshal(buf, &b)//反序列化,将json字符串转换到一个空的变量里,注意这个空的变量要加&
if err != nil {
panic(err)
}
fmt.Printf("%#v\n", b) // main.userInfo{Name:"wang", Age:18, Hobby:[]string{"Golang", "TypeScript"}}
}
时间处理,time包也是go基础库里面比较常用的一个包,可以用来获取当前时间,构造地区的时间,以及获取时间戳等
time.Now()可以获取当前的时间
package main
import (
"fmt"
"time"
)
func main() {
now := time.Now()
fmt.Println(now)
}
time.Date()用来构造时间
time.Date(Year,Month,Day,Hour,Minute,Second,Nsec,Local) 参数分别代表年月日时分秒,秒差以及区域,中国是CST,国际是UTC,local是当地时间
package main
import (
"fmt"
"time"
)
func main() {
t := time.Date(2023, 7, 25, 18, 07, 30, 0, time.UTC) //年月日时分秒时间差区域(UTC默认国际)
t2 := time.Date(2023, 7, 25, 20, 07, 38, 0, time.UTC)
fmt.Println(t)
fmt.Println(t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute())
fmt.Println(t.Format("2006-01-02 15:04:05")) //按照这种格式输出,一定要是这个标准时间,其他时间会输出不一样
}
可以通过t.Year(),t.Month(),t.Day等获取时间,然后go语言中有一个基准时间,2004-01-02 15:04:05,如果要以这种格式输出,就必须是这个时间,改变一个时间就会不一样
sub():可以用来将两个时间的相差
func (t Time) Sub(u Time) Duration,返回t-u,如果超过了Duration可以表示的最大值或者最小值,将会返回最大值和最小值
package main
import (
"fmt"
"time"
)
func main() {
t := time.Date(2023, 7, 25, 18, 07, 38, 0, time.UTC) //年月日时分秒时间差区域(UTC默认国际)
t2 := time.Date(2023, 7, 25, 20, 37, 38, 0, time.UTC)
diff := t2.Sub(t) //时间的相差
fmt.Println(diff)//2h30m0s
fmt.Println(diff.Hours(), diff.Minutes(), diff.Seconds())//2.5h,150m,9000s
t2与t的时间差为2h30m0s,diff.Hours()就是将得来的时间转换为小时单位,同理diff.Minutes()就是转换为分钟单位,diff.Seconds()就是转换为秒单位
time.Parse("2006-01-02 15:04:05",t2)将t2按照标准时间解析
package main
import (
"fmt"
"time"
)
func main() {
t := time.Date(2023, 7, 25, 18, 07, 30, 0, time.UTC) //年月日时分秒时间差区域(UTC默认国际)
t3, err := time.Parse("2006-01-02 15:04:05", "2023-07-25 18:07:30") //前面一定得是标准时间了,将后面的时间按标准时间进行解析
if err != nil {
panic(err)
}
fmt.Println(t3 == t)//true
}
解析成功就返回输出true
Unix()获取时间戳
package main
import (
"fmt"
"time"
)
func main() {
now := time.Now()
fmt.Println(now)
fmt.Println(now.Unix())//获取时间戳(秒为单位)
fmt.Println(now.UnixNano())//获取时间戳(纳秒为单位)
}
时间戳也就是时间印章,就是指格林威治时间1970年01月01日00时00分00秒(北京时间1970年01月01日08时00分00秒)起至现在的总秒数。通俗来讲,时间戳是一份能够表示一份数据在一个特定时间点已经存在的完整的可验证的数据
总结:错误处理除了本文所讲的透明处理,还有其他处理错误的方法也要去了解,但是在这之前得学会如何构造错误,然后这次对于字符串的处理的内容也是极其多的,包括字符串的构造,字符串常见的方法以及字符串的转换,都是比较重要的,其中字符串的构造并不算基础,因为涉及到了几种构造方式效率的比较,需要用到测试才能进行比较,感兴趣可以去尝试测试一下,最后就是时间的处理还有json的处理,json的处理不复杂,基础的就是序列化和反序列化,时间的处理的话,就特别要注意时间戳的概念
心得以及建议:做这个笔记也是做了好久,查了蛮多资料,也引用了许多资料,也包含了自己的思考,遇到陌生的知识点先是会去自己思考,然后心里可能会对这个知识点有个自己的理解,然后带着自己的理解去查找真正准确的答案,看老师讲课也是反复的看,听到老师说了自己没有听过的词时,都会去查找这个词的解释,虽然这样学的很慢把,但是学的也很稳,当然也不要死磕一个词,也要有自己的判断和思考,我觉得对于就是新手来说应该要稳扎稳打的学好每一个知识点,主动去学习,主动去探索,主动去练习,总而言之要加油啊