go语言基础| 青训营笔记

103 阅读12分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第 1 天

GO简介

特点:

  1. 高性能、高并发
  2. 语法简单、学习曲线平缓
  3. 丰富的标准库
  4. 完善的工具链
  5. 静态链接
  6. 快速编译
  7. 跨平台
  8. 垃圾回收

为什么字节跳动全面拥抱Go语言

  1. 最初使用Python, 由于性能问题换成了Go
  2. C++不太适合在线Web业务
  3. 早期团队非Java背景
  4. 性能比较好
  5. 部署简单, 学习成本低
  6. 内部RPC和HTTP框架的推广

Hello World

精通go语言的重要一步

 package main//代表这个文件属于main包的部分,main包也就是程序的入口包
 import "fmt"//导入标准库里面的FMT包。这个包主要是用来往屏幕输入输出字符串、格式化字符串。
 ​
 func main() {
     fmt.Println("hello")
 }

至此你已经学会了go

基础语法

变量

go语言是一门强类型语言,每一个变量都有它自己的变量类型,常见的变量类型包括字符串整数浮点型、布尔型等。

  • go语言的字符串是内置类型,可以直接通过加号拼接,也能够直接用等于号去比较两个字符串

在go语言里面变量的声明有两种方式

  • 一种是通过var name string=“这种方式来声明变量声明变量的时候,一般会自动去推导变量的类型。如果有需要,你也可以显示写出变量类型。
  • 另一种声明变量的方式是:使用变量冒号:=等于值。
  • 常量的话就是把var改成const,值在一提的是go语言里面的常量,它没有确定的类型,会根据使用的上下文来自动确定类型。
 package main
 ​
 import (
     "fmt"
     "math"
 )
 func main() {
     var a = "initial"//go语言的字符串是内置类型,可以直接通过加号拼接,也能够直接用等于号去比较两个字符串。
     var b, c int = 1, 2
     var d = true
     var e float64
     f := float32(e)
 ​
     g := a + "foo"
     fmt.Println(a, b, c, d, e, f)
     fmt.Println(g)
 ​
     const s string = "constant"
     const h = 5000//常量的话就是把var改成const,值在一提的是go语言里面的常量,它没有确定的类型,会根据使用的上下文来自动确定类型。
     const i = 3e20 / h
     fmt.Println(s, h, i, math.Sin(h), math.Sin(i))
 ​
 }

判断

go语言里面的f else写法和C/C++类似,主要有两点不同:

  • if后面没有括号。如果你写括号的话,那么在保存的时候你的编辑器会自动把你去掉。
  • Golang里面的if,它必须后面接大括号

循环

在go里面没有while循环、do while循环,只有唯一的一种for循环

选择

go语言里面的switch分支结构看起来也C/C++比较类似。同样的在switch后面的那个变量不需要是要括号。并且

  • 在c++里面,switch case如果不显式加break的话会发生穿透,即会继续往下跑完所有case,在go语言里面的话是不需要加break的.
  • 相比C或者C++,go语言里面的switch功能更强大。可以使用任意的变量类型,包括表达式。甚至可以用来取代任意的if else语句。你可以在switch后面不加任何的变量,然后在case里面写条件分支。这样代码相比你用多个if else代码逻辑会更为清晰.
 t := time.Now()
     switch {
     case t.Hour() < 12:
         fmt.Println("It's before noon")
     default:
         fmt.Println("It's after noon")
     }

数组

数组就是一个具有编号且长度固定的元素序列。 对于一个数组,可以很方便地取特定索引的值或者往特定索引取存储值,然后也能够直接去打印一个数组。不过,在真实业务代码里面,我们很少直接使用数组,因为它长度是固定的,我们用的更多的是切片。

切片

切片不同于数组其可以任意更改长度,并且也有更多丰富的操作。

我们可以用make来创建一个切片,可以像数组一样去取值,使用append来追加元素。

 package main
 import "fmt"
 func main() {
     s := make([]string, 3)
     s[0] = "a"
     s[1] = "b"
     s[2] = "c"
     fmt.Println("get:", s[2])
     fmt.Println("length:", len(s))
 ​
     s = append(s, "d")
     s = append(s, "e", "f")
     fmt.Println(s)
 ​
     c := make([]string, len(s))
     copy(c, s)
     fmt.Println(s[2:5])
     fmt.Println(s[:5])
     fmt.Println(s[2:])
 ​
     good := []string{"g", "o", "o", "d"}
     
     fmt.Println(good)
 ​
 }
 ​

注意append的用法的话,你必须把append的结果赋值为原数组。 因为slice的原理实际上是它有一个它存储了一个长度和一个容量,加一个指向一个数组的指针,在你执行append操作的时候,如果容量不够的话,会扩容并且返回新的sice。

slice此初始化的时候也可以指定长度。 slice拥有像python一样的切片操作,比如这个代表取出第二个到第五个位置的元素,不包括第五个元素。不过不同于python,这里不支持负数索引

map

创建map会需要两个类型,第一个是那个key的类型,后面是value的类型。

可以用delete从里面删除键值对。

golang的map是完全无序的,遍历的时候按照随机顺序。

 package main
 import "fmt"
 func main() {
     m := make(map[string]int)
     m["one"] = 1
     m["two"] = 2
     fmt.Println(m)
     fmt.Println(len(m))
     fmt.Println(m["one"])
     fmt.Println(m["unknow"])
 ​
     r, ok := m["unknow"]
     fmt.Println(r, ok)
 ​
     delete(m, "one")
 ​
     m2 := map[string]int{"one": 1, "two": 2}
     var m3 = map[string]int{"one": 1, "two": 2}
     fmt.Println(m2, m3)
 }

range

对于一个slice或者一个map的话,我们可以用range来快速遍历,这样代码能够更加简洁 range遍历的时候,对于数组会返回两个值,第一个是索引,第二个是对应位置的值。如果我们不需要索引的话,我们可以用下划线来忽略

 package main
 ​
 import (
     "fmt"
 )
 ​
 func main() {
     nums := []int{2, 3, 4}
     sum := 0
     for index, num := range nums {
         sum += num
         if num == 2 {
             fmt.Println("index:", index, "num:", num)
         }
     }
     fmt.Println(sum)
 ​
     m := map[string]string{"a": "A", "b": "B"}
     for k, v := range m {
         fmt.Println(k, v)
     }
     for k := range m {
         fmt.Println("key",k)
     }
 ​
 }
 ​

函数

Golang里面的函数原生支持返回多个值。在实际的业务逻辑代码里面几乎所有的函数都返回两个值,第一个是真 正的返回结果,第二个值是一个错误信息。

 package main
 ​
 import "fmt"
 ​
 func add(a int, b int) int {
     return a + b
 }
 ​
 func exists(m map[string]string, k string) (v string, ok bool) {
     v, ok = m[k]
     return v, ok
 }
 func main() {
     res := add(1, 2)
     fmt.Println(res)
 ​
     v, ok := exists(map[string]string{"a": "A"}, "a")
     fmt.Println(v, ok)
 }
 ​

指针

go里面也支持指针。但相比C和C++里面的指针支持的操作很有限。指针的一个主要用途就是对于传入参数进行修改。

 package main
 ​
 import "fmt"
 ​
 func add2(n int) {
     n += 2
 }
 func add2ptr(n *int) {
     *n += 2
 }
 func main() {
     n := 5
     add2(n)
     fmt.Println(n)
     add2ptr(&n)
     fmt.Println(n)
 }

我们来看这个函数。这个函数试图把一个变量+2。但是单纯像上面这种写法其实是无效的。因为传入函数的参数实 际上是一个拷贝,那也说这个+2,是对那个拷贝进行了+2,并不起作用。如果我们需要起作用的话,那么我们需 要把那个类型写成指针类型,那么为了类型匹配,调用的时候会加一个&符号。

结构体

结构体就是带类型的字段的集合。

结构体支持指针,这样能够实现对结构体的修改,也可以在某些情况下避免一些大结构体的拷贝开销

 package main
 ​
 import "fmt"
 ​
 type user struct {
     name     string
     password string
 }
 ​
 func main() {
     a := user{name: "wang", password: "1024"}
     b := user{"wang", "1024"}
     c := user{name: "wang"}
     c.password = "1024"
     var d user
     d.name = "wang"
     d.password = "1024"
     fmt.Println(a, b, c, d)
     fmt.Println(checkPassword(a, "erro"))
     fmt.Println(checkPassword2(&a, "erro"))
 ​
 }
 func checkPassword(u user, password string) bool {
     return u.password == password
 }
 func checkPassword2(u *user, password string) bool {
     return u.password == password
 }
 ​

结构体方法

在Golang里面可以为结构体去定义一些方法。类似其他语言里面的类成员函数。

比如上一个例子的checkPassword的实现,将其从一个普通函数,改成了结构体方法。这样用户可以像a.checkPassword("xxx")这样去调用。

具体的代码修改,就是把结构体参数加上括号写到函数名称前面。 在实现结构体的方法的时候也有两种写法,一种是带指针,一种是不带指针。这个它们的区别的话是说如果你带指针的话,那你那么你就可以对这个结构体去做修改。如果你不带指针的话,那你实际上操作的是一个拷贝,你就无法对结构体进行修改。

 package main
 ​
 import "fmt"
 ​
 type user2 struct {
     name     string
     password string
 }
 ​
 func main() {
     a := user2{name: "wang", password: "1024"}
     a.resetPassword("2048")
     fmt.Println(a.checkPassword("2048"))
 ​
 }
 func (u user2) checkPassword(password string) bool {
     return u.password == password
 }
 func (u *user2) resetPassword(password string) {
     u.password = password
 }

错误处理

错误处理在go语言里面符合语言习惯的做法就是使用一个单独的返回值来传递错误信息。不同于Java使用的异常。go语言的处理方式,能够很清晰地知道哪个函数返回了错误,并且能用简单的if else来处理错误。 在函数里面,我们可以在那个函数的返回值类型里面,后面加一个error,就代表这个函数可能会返回错误。那么在函数实现的时候,return需要同时return两个值,要么就是如果出现错误的话,那么可以return nil和一个error。.如果没有的话,那么返回原本的结果和nil。

 package main
 ​
 import (
     "errors"
     "fmt"
 )
 ​
 type user struct {
     name     string
     password string
 }
 ​
 func findUser(users []user, name string) (v *user, err error) {
     for _, u := range users {
         if u.name == name {
             return &u, nil
         }
     }
     return nil, errors.New("not found")
 }
 ​
 func main() {
     u, err := findUser([]user{{"wang", "1024"}}, "wang")
 ​
     if err != nil {
         fmt.Println(err)
         return
     }
 ​
     fmt.Println(u.name)
 ​
     if u, err := findUser([]user{{"wang", "1024"}}, "li"); err != nil {
         fmt.Println(err)
         return
     } else {
         fmt.Println(u.name)
     }
 ​
 }
 ​

字符串操作

go语言在标准库strings包里面有很多常用的字符串工具函数,比如:contains判断一个字符串里面是否有包含另一个字符串,count字符串计数,index查找某个字符串的位置。join连接多个字符串repeat重复多个字符串replace替换字符串。

 package main
 ​
 import (
     "fmt"
     "strings"
 )
 ​
 func main() {
     a := "hello"
     fmt.Println(strings.Contains(a, "ll"))
     fmt.Println(strings.Count(a, "l"))
     fmt.Println(strings.HasPrefix(a, "he"))
     fmt.Println(strings.HasSuffix(a, "llo"))
     fmt.Println(strings.Index(a, "ll"))
     fmt.Println(strings.Repeat(a, 3))
     fmt.Println(strings.Join([]string{"he", "llo"}, "-"))
     
     //返回将s中前n个不重叠old子串都替换为new的新字符串,如果n<0会替换所有old子串。
     fmt.Println(strings.Replace(a, "e", "E", -1))
     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))//6
 }
 ​

字符串格式化

在标准库的FMT包里面有很多的字符串格式相关的方法,比如printf这个类似于C语言里面的printf函数。不同的是,在go语言里面的话,你可以很轻松地用%v来打印任意类型的变量,而不需要区分数字字符串。你也可以用%+V打印详细结果,%#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)        //hello 123
     fmt.Println(p)           //12
     fmt.Printf("s=%v\n", s)  //s=hello
     fmt.Printf("n=%v\n", n)  //n=123
     fmt.Printf("p=%v\n", p)  //p=f1 2)
     fmt.Printf("p=%+v\n", p) //p=fx:1 y:2}
     fmt.Printf("p=%#v\n", p) //p=main.point{x:1,y:2}
     f := 3.141592653
     fmt.Println(f)
     //3.141592653
     fmt.Printf("%.2f\n", f) //3.14
 }
 ​

JSON处理

go语言里面的SON操作非常简单,对于一个已有的结构体,我们可以什么都不做,只要保证每个字段的第一个字母是大写,也就是是公开字段。 那么这个结构体就能用JSON.marshaler去序列化,变成一个JSON的字符串。 序列化之后的字符串也能够用JSON.unmarshaler去反序列化到一个空的变量里面。

 package main
 ​
 import (
     "encoding/json"
     "fmt"
 )
 ​
 type userInfo struct {
     Name  string
     Age   int `json:"age"`//修改结果字段名
     Hobby []string
 }
 ​
 func main() {
     a := userInfo{Name: "wang", Age: 18, Hobby: []string{"Golang", "TypeScript"}}
     buf, err := json.Marshal(a)
     if err != nil {
         panic(err)
     }
     fmt.Println(buf)         //[123347897..1
     fmt.Println(string(buf)) //{"Name":"wang","age":18,"Hobby":"Golang","TypeScript"]}
     buf, err = json.MarshalIndent(a, "", "\t")
     if err != nil {
         panic(err)
     }
     fmt.Println(string(buf))
 ​
     var b userInfo
     err = json.Unmarshal(buf, &b)
     if err != nil {
         panic(err)
     }
     fmt.Printf("%#v\n", b) //main.userInfo{Name:"wang",Age:18,Hobby:[]stringf"Golang","TypeScript"}}
 }
 ​

时间处理

下面是时间处理,在go语言里面最常用的就是time.now来获取当前时间,然后你也可以用time.date去构造一个带时区的时间

构造完的时间有很多方法来获取这个时间点的年月日小时分钟秒,然后也能用点sub去对两个时间进行减法,得到一个时间段。时间段又可以去得到它有多少小时,多少分钟、多少秒。在和某些系统交互的时候,我们经常会用到时间戳。那您可以用.Unix来获取时间戳。 Format(格式化)表示将时间转化为字符串,parse(分析)表示将字符串转化为时间

 package main
 ​
 import (
     "fmt"
     "time"
 )
 ​
 func main() {
     now := time.Now()
     fmt.Println(now)
     t := time.Date(2023, 3, 27, 1, 25, 36, 0, time.UTC)
     t2 := time.Date(2023, 3, 27, 2, 30, 36, 0, time.UTC)
     fmt.Println(t)                                                  //2023-03-2701:25:36+0000UTC
     fmt.Println(t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute()) //2023 March 27 1 25
     fmt.Println(t.Format("2006-01-02 15:04:05"))                    //2023-03-2701:25:36
     diff := t2.Sub(t)
     fmt.Println(diff)                           //1h5m0s
     fmt.Println(diff.Minutes(), diff.Seconds()) //65 3900
     t3, err := time.Parse("2006-01-0215:04:05", "2022-03-2701:25:36")
     if err != nil  
         panic(err)
     }
 ​
     fmt.Println(t3 == t)    //false
     fmt.Println(now.Unix()) // 1648738080
 }
 ​

数字解析

在go语言当中,关于字符串和数字类型之间的转换都在strconv这个包下,这个包是string convert这两个单词的缩写。 我们可以用parselnt或者parseFloat来解析一个字符串。parseint参数 我们可以用Atoi把一个十进制字符串转成数字。可以用toAi把数字转成字符串。 如果输入不合法,那么这些函数都会返回error

 package main
 ​
 import (
     "fmt"
     "strconv"
 )
 ​
 func main() {
     f, _ := strconv.ParseFloat("1.234", 64)
     fmt.Println(f) //1.234
     n, _ := strconv.ParseInt("111", 10, 64)
 ​
     fmt.Println(n) //111
     //如果 base 为 0,则会从字符串前置判断,“0x”是 16 进制,“0”是 8 进制,否则是 10 进制
     nx, _ := strconv.ParseInt("00100", 2, 64)
     fmt.Println(nx) //4
 ​
     n2, _ := strconv.Atoi("123")
     fmt.Println(n2) //123
 ​
     n2, err := strconv.Atoi("AAA")
     fmt.Println(n2, err) //0 strconv.Atoi:parsing "AAA":invalid syntax
 }
 ​

进程信息

在go里面,我们能够用os.argv来得到程序执行的时候的指定的命令行参数。 比如我们编译的一个二进制文件,command.。后面接abcd来启动,输出就是os.argv会是一个长度为5的slice,第一个成员代表二进制自身的名字。 我们可以用so.getenv来读取环境变量。 exec

 package main
 ​
 import (
     "fmt"
     "os"
     "os/exec"
 )
 ​
 func main() {
     //go run example/20-env/main.go a b c d
     fmt.Println(os.Args)           //[/var/folders/8p/n34xxfnx38dg8bv_x8162t_m0000gn/T/go- build3406981276/b001/exe/main a b c d]
     fmt.Println(os.Getenv("PATH")) ///usr/local/go/bin...
     fmt.Println(os.Setenv("AA", "BB"))
     buf, err := exec.Command("grep", "127.0.0.1", "/etc/hosts").CombinedOutput()
     if err != nil {
         panic(err)
     }
     fmt.Println(string(buf)) //127.0.0.1  localhost
 }
 ​

{day2}

go:为并发而生

并行:实现并发的一种手段

go实现了一个并发性能极高的调度模型,通过高效调度最大限度的利用计算资源,从而充分发挥多核计算机的优势