这是我参与「第五届青训营」笔记创作活动的第1天,主要记录一下学习Go语言的基础入门和实战案例的心得体会,在下有过Py和C的开发经验,入手Go感觉还是比较好上手的,本次课程主要是在实战入门的部分啃了比较长时间的代码,在熟悉代码风格后就很舒服啦~
一、初识Golang
Go语言最初是由Ken Thompson(C语言前身B语言的设计者)和Rob Pike(UTF8编码设计人之一)、Robert Griesemer(参与JS V8引擎设计)主持开发,后来加入了Ian Lance Taylor(gccgo的作者)和 Russ Cox(目前Go项目的Leader)。而Go的设计也主要是面向搭载Web Server、Cluster Storage等大型服务器系统编程。在首日的课程中介绍了Go具有以下8个特性:
- 1.高性能高并发,Goroutine类似于相比其他语言中创建新的子进程的功能,但开销更低,比如线程栈空间往往是2MB,但Goroutine栈空间最小只需2KB。
- 2.语法简单、学习曲线平缓,如果有C/C++和Python的语言基础的话,Go还是挺好上手的,比如在字符串处理和指针操作上还是有很多相似之处的。
- 3.完善的工具链,拥有诸如gofmt自动排版、gofix更新工具、golint/govet规范检查工具等等。
- 4.丰富的标准库,相比Python来说,Go的标准库拥有稳定性、兼容性和随语言迭代升级的优点,而且也不用饱受混乱部署生态的折磨,省下不少时间和精力。
- 5.静态链接,除了glibc以外没有其他的外部依赖,所以在部署上也非常的方便。
- 6.快速编译,Go是静态编译,其流程是先提取使用到的链接库,和可执行文件相链接,最后生成一个可执行的文件。而Go的引用管理和1.5版本后自举编译器优化以及更少匹配关键字解析、摒弃如C/C++的模版编译负担,是其在同是静态编译的语言中能够获得较快速度的原因。
- 7.跨平台,除非cgo项目,Go语言可以实现在Windows编译而部署在Linux/MacOS/iOS/Android等主流操作系统上,以及不同arch的CPU上,无需进行交叉配置。
- 8.垃圾回收,目前主流编程语言的GC主要有自动和手动两种,C/C++中主要以手动free来进行管理内存操作。而Go是使用自动的语言内存管理,由内存分配器和垃圾收集器来进行分配和回收,属于跟踪式的垃圾回收算法,其核心是判断可达性。
二、探索Go的常变量和数据类型
- 探索数据类型之前,不妨先了解数据类型在变量声明中的存在形式,而且在Go中我觉得变量声明还是个比较有趣的地方,和其他的语言赋值形式相比,Go还是挺抽象的hhh,往下看。
var 变量名 变量类型 = 表达式
var a = "initial" //字符串变量 a
a := "initial" //简短变量声明
var b,c int = 12,3 //声明整型变量、多重变量声明
var b,c = 12,3 //变量类型如果略去,则Go根据表达式自动推导变量类型
var f float //浮点数声明
g :a +"foo" //支持使用+号进行字符拼接
因为简洁和灵活的特点,简短变量声明(:=)被广泛用于大部分的局部变量的声明和初始化。 而var形式的声明语句往往是用于需要显式指定变量类型的地方,或者变量稍后会被重新赋值而初始值无关紧要的地方。
const 常量名 常量类型 = 表达式 //常量设置
const h = 50000000
const e = 3e20/h //引入常量
- 数组和切片的操作,Go中数组操作与常见的编程语言一致。
var a [3]int=[3]int{1,2,3} //数组的创建
r := [...]int{99: -1} //没用到的索引可以省略,长度100,99个0,末尾为-1
由于数组是由固定长度的特定类型元素组成的序列,在工程领域中往往是需要用到可变长度的序列,在Go中对应的就是切片(slice)。
s := make([]string,3) //内部函数make创建一个指定元素类型、长度和容量的切片
s = append(s,"d") //使用append向原切片赋值新的元素
c := make([]string,lens(s))
copy(c,s) //将s切片复制到新make的c中
s[2:5] //即s[2]、s[3]、s[4]同python切片规范
ps:"good"字符等同于[]string{"g","o","o","d"},但直接读取字符串中某一元素时需要进行string(good[3])类型转换,否则是以hex形式输出。
- MAP类型(哈希表)是一个无序、随机的kv对,所有的key都是不相同的,通过给定key来进行value的curd。
m := map[string]int{} //创建空map
ages := make(map[string]int) //也通过前面提到的make来创建一个“string”:int字典
ages := map[string]int{
"alice": 31,
"charlie": 34,
}//map字面值的语法创建map,可指定初始值
ages :=make(map[string]int)
ages["alice"] = 31
ages["charlie"] = 34
delete(ages,"alice") //内置的delete操作可以用来删除元素
r, ok :=["UNKNOWN"] //查找UNKNOWN,如果不存在返回0给r
这里需要注意的是r 是var r int = ages["UNKNOWN"]的意思,其类型跟随kv对的value类型,而ok是var ok bool,负责返回查找的成功与否,可用于下文需要条件判断或错误处理的地方。
- 指针类型(Point),熟悉C/C++的同学,这一块是得心应手啦,不过Go中的指针是不支持进行增减运算的。
var i *int //整型指针变量
a :=10
i = &a //取地址操作,i指向a的地址
b := *i //解引用赋值,b=10
func change(n *int){
n+=2
}
...
change(&n) //这里还是同C++一致,在子函数修改值时,需要引用
- 结构体类型(struct),也与C中的结构体组成类似
type std struct{
id int
name string
age int
sex int
class int
grade int
}
//初始化方式
fmt.Println([]std1{{id:10001,name:"John",age:18,sex:0,class:9,grade:13},})
func (s *std) changeAge(age int){
s.age = age
}
//函数使用结构体指针修改结构体类型的数值
三、Go的循环是一门简化的艺术
相比C/C++的while、for、do...while循环,Go只提供了for循环来进行重复执行语块,但也能轻松实现上面三种循环的相同功能。
for init;condition;post {}
//和C的for一样for(int i =0;i<10;i++)
//控制变量;循环控制条件;控制变量增减
for condition {}
//和C的while一致
for {}
//和C的for(;;)一致
讲到循环,就不得不提一嘴Range格式,for-each range循环可以对slice、string、array进行迭代输出元素,如下所示。
fruits := []string{"Apple","Banana","Cherry","Durain"}
for key, value:=range fruits{
fmt.Println(key,value)
}//key从0开始,value := fruits[key]
for _,value:=range fruits{
fmt.Printf(“fruits里有%v”,value)
}//可以省略key,只获取value
for key:=range fruits{
fmt.Printf("Array Key is %v",key)
}//也可以省略value,只获取key,这部分在map上使用的多一些
好了,跳出Range,我们接着说循环,可以注意到for-each range格式和for循环格式中的风格不太一致,初学时需要注意分辨一下。
for key, value := range sth {}
for i; i<10; i++ {}
//对于使用for条件外的变量做为条件(相当于while)
sum := 1
for ;sum<=10;{}
for sum<=10 {}
//这两种写法都是ok的
四、窥探Go的信息流处理
这一部分课程内容,个人倾向于归类为Go中对所拿到的信息流所常见的几种处理手段。
- Switch语句常作为测试一个变量在多个值的不同情况(case),Go中的语法与C Switch非常的相似,但省去了每个case需要手写break的麻烦,Go在匹配Case的时候,会自动break走出Switch语句控制范围。下边的代码块说明了Go Switch相比C Switch的优点:
var grade string = "B"
var marks int = 90
switch marks {
case 90: grade = "A"
case 80: grade = "B"
case 50,60,70 : grade = "C" //可以设置的多个constant-expression
default: grade = "D"
}
switch {
case grade == "A" :
fmt.Printf("优秀!\n" )
case grade == "B", grade == "C" :
fmt.Printf("良好\n" )
case grade == "D" :
fmt.Printf("及格\n" )
case grade == "F":
fmt.Printf("不及格\n" )
default:
fmt.Printf("差\n" );
}
fmt.Printf("你的等级是 %s\n", grade );
Switch语句的合理使用还可以用于替换if嵌套
t = time.Now()
switch{
case t.hour()<12:
fmt.Println("Before noon")
case ...
dafault:
fmt.Println("After noon")
}
if t.hour()<12{
fmt.Println("Before noon")
}else{
if t.hour()>=12&&t.hour()<20{
fmt.Println("After noon")
}
}
- 函数function的多值返回,对于多值传入传出,Go可以为函数返回多个值。
func swap(x, y int)(int int){ //函数名(x,y 参数类型)(返回类型)
return y,x
}
Go中绝大部分的错误处理结合双返回值非常有利于我们对函数执行的进行状态判断。如下边在对users结构体数组进行SearchUser方法的程序,双返回值的特性就可以用于功能执行情况的判断。
import(
"errors"
"fmt"
)
type user struct{
name string
age int
id int
}
func SearchUser(users []user, scname string)(u *user, err error){
for _, u:= range users{ //获取user结构体数组中的元素
if u.name == scname{
return &u,nil //返回查找的信息,err=nil
}
}
return nil, errors.New("User Not Found!")//u = nil查找失败。
}
func main(){
users := []user{{name:"wanghuahua",age:18,id:10001},{name:"Zhansan",age:19,id:10002}}//user结构体数组
u,err := SearchUser(users,"wanghuahua")
if err != nil{
fmt.Println(err)
return
}
fmt.Println(u.name)//打印查询结果
u,err = SearchUser(users,"li")
if err != nil{ //错误处理
fmt.Println(err)
return
}
fmt.Println(u.name)
}
- 字符串操作及格式化,先前在阅读Magnus Lie Hetland写的《Python基础教程 第三版》的时候,就感叹字符串操作之繁多有,像什么formt自动编号手工编号替换字段名啊,还有作者提及Python中的字符串是适用所有标准序列的操作的,字符串是不可变的,因此元素赋值和切片赋值都是非法操作,我稍微在Go里试了下,发现Go也是保持字符串不可变的属性。
website := "juajin.org"
website[2] = "e"
website[7:10] = "com"
err:cannot assign to website[2] (value of type byte)
err:cannot assign to website[7:10] (value of type string)
strings是Go的一个标准库提供了字符串查找、替换、比较等常用方法。课程中讲解的如下所示:
a :="hello world"
strings.Contains(a,"ll") //包含与否,返回:true
strings.Count(a,"l") //计算出现次数,返回:2
strings.HasPrefix(a,"he") //是否有前缀he,返回:true
strings.HasSuffix(a,"rld") //后缀
strings.Index(a,"l") //l字符首次出现的位置
strings.Join([]string{"he","llo"},"-") //字符数组元素拼合成单个字符串
strings.Repeat(a,2)
strings.Replace(a,"e","E",-1)//正则匹配,第四个参数即n<0时
//替换所有匹配项,n=0时不替换,n=1替换首个,以此类推
strings.Split("a-b-c","-") //字符串分割,返回[a b c]
strings.ToLower(a) //小写
strings.ToUpper(a) //大写
len(a) //返回字符串长度
对于字符串转型,Go中可以使用strconv包来进行str to int/float操作,所提供的Parse类函数有:ParseBool()、ParseFloat()、ParseInt()和ParseUint(),
strconv.ParseInt(s string, base int,bitsize int) //字符串、进制、精度
还有Atoi和Itoa方法用于字符串和整形互转,strconv.Atoi("123") / strconv.Itoa(123)。
Go中提供了两种Print接口Println和Printf,其中Printf的使用与C语言类似,但Go省去手动匹配%转义的过程,无需%d%s%c%f来匹配,通过一个%v即可,Go会自动对待输出的目标匹配相应类型
fmt.Printf("a=%v\n,a) ,同时%v也支持格式控制,如打印结构体的时候:
type point struct {
x int
y int
}
func main() {
var p point
p.x = 1
p.y = 2
fmt.Printf("p=%v", p) //输出p={1 2}打印结构体类型成员值
fmt.Printf("p=%+v",p) //输出p={x:1 y:2}会附加成员值的字段名
fmt.Printf("p=%#v",p) //输出p=main.point{x:1,y:2},先结构体名,随后才是输出结构体
}
- Json处理,Go中的json处理是我见过最方便,最友好的。其内部的encoding/json标准库支持RFC4627标准的编解码,其提供的Marshal和Unmarshal接口可进行序列化和反序列化操作,具体流程为:
graph LR
struct-->Json.Marshal --> HexBuf --> Json.Unmarshal-->struct
- 假设buf接收json.Marshal(struct_type_var)转义struct成员输出的json格式
- buf以十六进制的方式存储json文本字段【也可以通过MarshalIndent()接口可以按指定格式输出json文本字段,比如空格转换行符等】
- 最后通过json.Unmarshal(buf,&b)将反序列化的结果存放至结构体变量b的内存空间里。
此即完成了序列号和反序列化的流程。更多此部分的个人见解在后续的在线字典实战项目中会详细说明。
- 时间处理,Go的time标准库个人觉得与Python的time库基本相似,但在时间格式化上,Go支持使用指定事例时间格式作为format的模版。如下所示,
t := time.Date(2022,3,27,1,25,36,time.UTC)
t.Format = ("2006-01-02 15:04:05")
//相比YY-MM-DD HH:MM:SS这种格式化模版还是比较人性化的
- 进程处理,与大多数编程语言一样,Go也有exec函数蔟,通过标准库os/exec提供的接口,可以在Go程序中运行shell命令,支持同步Run()/异步Start()执行。借此接口,Go程序可以轻松实现获取子进程及其Input/Output,可以实现循环输入的效果,如下所示:
buf,err := exec.Command("grep","127.0.0.1","/etc/hosts").CombinedOutput()
//buf获取到了grep 127.0.0.1 /etc/host命令的输出
//127.0.0.1 localhost
Go中的os标准库也与常见编程语言中的os库相似,提供诸如解析命令行参数(args)、环境变量获取(Getenv)、环境变量设置(Setenv)。
os.Setenv("debug","1")
os.Getenv("debug") //返回 1
五、小试牛刀,实战入门
- 猜谜游戏,即二分查找思路来猜用户此时所想的数值。主要知识点梳理:
rand.Seed(time.Now().UnixNano())使用当前时间戳作为随机数种子防止多次产生同一结果。reader := bufio.NewReader(os.Stdin)os.Stdin可以获取用户输入的完整内容(含空格)
作业1、使用fmt.scanf来简化下列代码。
package main
import (
"bufio"
"fmt"
"math/rand"
"os"
"strconv"
"strings"
"time"
)
func main() {
maxNum := 100
rand.Seed(time.Now().UnixNano())
secretNumber := rand.Intn(maxNum)
// fmt.Println("The secret number is ", secretNumber)
fmt.Println("Please input your guess")
reader := bufio.NewReader(os.Stdin)
for {
input, err := reader.ReadString('\n')
if err != nil {
fmt.Println("An error occured while reading input. Please try again", err)
continue
}
input = strings.Trim(input, "\r\n")
guess, err := strconv.Atoi(input)
if err != nil {
fmt.Println("Invalid input. Please enter an integer value")
continue
}
fmt.Println("You guess is", guess)
if guess > secretNumber {
fmt.Println("Your guess is bigger than the secret number. Please try again")
} else if guess < secretNumber {
fmt.Println("Your guess is smaller than the secret number. Please try again")
} else {
fmt.Println("Correct, you Legend!")
break
}
}
}
使用scanf简化后的代码。
package main
import (
"fmt"
"math/rand"
"time"
)
func main() {
maxNum := 100
rand.Seed(time.Now().UnixNano())
secretNumber := rand.Intn(maxNum)
fmt.Println("Please input your guess")
//reader := bufio.NewReader(os.Stdin)
for {
var guess int
_, err := fmt.Scanf("%d", &guess)
//input, err := reader.ReadString('\n')
if err != nil {
fmt.Println("An error occured while reading input. Please try again", err)
continue
}
//input = strings.Trim(input, "\r\n")
//guess, err := strconv.Atoi(input)
//if err != nil {
// fmt.Println("Invalid input. Please enter an integer value")
// continue
//}
fmt.Println("You guess is", guess)
if guess > secretNumber {
fmt.Println("Your guess is bigger than the secret number. Please try again")
} else if guess < secretNumber {
fmt.Println("Your guess is smaller than the secret number. Please try again")
} else {
fmt.Println("Correct, you Legend!")
break
}
}
}
Continue...未完持续
六、课后总结
第一天接触总能联想出各种奇奇怪怪的东西来,但好在了解也在逐渐加深,边复盘边写不知不觉就一晚上了。之前有过其他语言的学习经历,还是能够较为轻松的接受课程所教授的知识,感觉大神讲课就是不一样,就如同品double美式一样,课后复盘后收获满满的。但目前的问题就是Go的语法在习惯C后看起来略抽象,strs:=[]string{"hello","world"},还得多敲熟悉一下代码风格,纠正一些肌肉记忆,万事开头难,继续努力吧!
- 伴学笔记 Day1