Go基础学习 | 青训营笔记

121 阅读4分钟

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

作为一个主攻java的大三学生,第一次转语言去学习,二者的编程方式有很大区别,其实说白了Go语言跟C语言还是有很多相似的地方的,这也幸亏了我在大一期间自己学了一点点的C,稍微能看明白Go里面的一些基础语法。

因为作为Go语言的小白,在做笔记的过程中我也查阅了大量的资料去学习,将外部学习资料作为总结补充到知识点当中。

一、什么是Go语言?

1.高性能、高并发 :从底层原生支持并发,无须第三方库、开发者的编程技巧和开发经验。

2.语法简单、学习曲线平缓 :Go 语言的风格类似于C语言。其语法在C语言的基础上进行了大幅的简化,去掉了不需要的表达式括号,循环也只有 for 一种表示方法,就可以实现各种遍历。

3.丰富的标准库 :Go 的标准库高效、简洁、正确地实现了丰富的网络协议。

4.完善的工具链 :Go的工具链极其丰富,可以让获取源码、编译、测试、重构等功能做起来更加方便。

5.静态链接 :网上有人说Go = C + Python,因为它既有C那样静态语言的运行速度,也有像Python那样的动态语言的快速开发。

6.快速编译 :Go语言的编译速度明显优于 Java 和 C++,Go语言还支持当前所有的编程范式。

7.跨平台 :让程序员更加专注的去写代码,而不必把大量的精力放在环境上。

8.垃圾回收:C/C++最头疼的就是指针问题,一不小心就越界了。在Go语言里再也不用担心,也不用考虑delete或者free,系统自动会回收。

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

1.最初使用的Python,由于性能问题换成了Go

2.C++ 不太适合在线Web 业务

3.早期团队非Java 背景

4.性能比较好

5.部署简单、学习成本低

6.内部 RPC和HTTP框架的推广

三、第一个Go程序

//程序入口文件
package main

//导入fmt包  该包用于输入输出字符串,格式化字符串用的
import (
        "fmt"
)

func main() {
        fmt.Println("hello world")
}

3.1 和C语言相似,go语言的基本组成有:

包声明:编写源文件时,必须在非注释的第一行指明这个文件属于哪个包,如package main。

引入包:其实就是告诉Go 编译器这个程序需要使用的包,如import "fmt"其实就是引入了fmt包。

函数:和c语言相同,即是一个可以实现某一个功能的函数体,每一个可执行程序中必须拥有一个main函数。

变量:Go 语言变量名由字母、数字、下划线组成,其中首个字符不能为数字。

语句/表达式:在 Go 程序中,一行代表一个语句结束。每个语句不需要像 C 家族中的其它语言一样以分号 ; 结尾,因为这些工作都将由 Go 编译器自动完成。

注释:和c语言中的注释方式相同,可以在任何地方使用以 // 开头的单行注释。以 /* 开头,并以 */ 结尾来进行多行注释,且不可以嵌套使用,多行注释一般用于包的文档描述或注释成块的代码片段。

3.2 与java的不同:

java要打印一段字符串到控制台上时,需要有创建好的类,类中要写主方法main,从main中用System.out.println("hello world");去做打印。而Go语言是导入fmt包,利用fmt中的print方法去打印,从中做对比就可以看的Go的语法的简便。

四、Go常见的变量类型

Go是一门强类型语言

字符串、整型、浮点型、布尔型

数据类型的出现是为了把数据分成所需内存大小不同的数据,编程的时候需要用大数据的时候才需要申请大内存,就可以充分利用内存。

4.1 :=符号

当我们定义一个变量后又使用该符号初始化变量,就会产生编译错误,因为该符号其实是一个声明语句。

使用格式 typename := value

也就是等于intValue := 1

五、条件表达式

5.1 if和if-else

if语句:

if 布尔表达式 {
   /* 在布尔表达式为 true 时执行 */
}

if-else语句:

if 布尔表达式 {
   /* 在布尔表达式为 true 时执行 */
} else {
        /* 在布尔表达式为 false 时执行 */
}

5.2 灵活的运用if和if-else,体现go的优势

if num := 9; num < 0 {
                fmt.Println(num, "is negative")
        } else if num < 10 {
                fmt.Println(num, "has 1 digit")
        } else {
                fmt.Println(num, "has multiple digits")
        }

if后跟的num变量的赋值以及条件,这是java中所不能运用的,而且if后面的条件不需要再加括号。

5.3 switch

其中的变量v可以是任何类型,val1val2可以是同类型的任意值,类型不局限为常量或者整数,或者最终结果为相同类型的表达式。

switch v {
    case val1:
        ...
    case val2:
        ...
    default:
        ...
}

例如课程案例上所给的:

a := 2
        switch a {
        case 1:
                fmt.Println("one")
        case 2:
                fmt.Println("two")
        case 3:
                fmt.Println("three")
        case 4, 5:
                fmt.Println("four or five")
        default:
                fmt.Println("other")
        }

t := time.Now()
        switch {
        case t.Hour() < 12:
                fmt.Println("It's before noon")
        default:
                fmt.Println("It's after noon")
        }

两个代码中的 a 和 case 后面的条件可以是任何类型的。

六、数组

Go 语言数组声明需要指定元素类型及元素个数。

var a [5]int
b := [5]int{1, 2, 3, 4, 5}
var twoD [2][3]int

初始化数组的方式不止一种,但是要注意初始化数组中 {} 中的元素个数不能大于 [] 中的数字,而且如果忽略 [] 中的数字不设置数组大小,Go 语言会根据元素的个数来设置数组的大小。

七、slice

slice和数组一样,内置的len函数返回切片中有效元素的长度,内置的cap函数返回切片容量大小,容量必须大于或等于切片的长度。

在对切片本身赋值或参数传递时,和数组指针的操作方式类似,只是复制切片头信息,并不会复制底层的数据。对于类型,和数组的最大不同是,切片的类型和长度信息无关,只要是相同类型元素构成的切片均对应相同的切片类型

方法:

append() :内置的泛型函数,可以向切片中增加元素。

s := make([]string, 3)
        s[0] = "a"
        s[1] = "b"
        s[2] = "c"
        fmt.Println("get:", s[2])   // c
        fmt.Println("len:", len(s)) // 3

        s = append(s, "d")
        s = append(s, "e", "f")
        fmt.Println(s) // [a b c d e f]

copy() : 复制切片

c := make([]string, len(s))
copy(c, s)
fmt.Println(c) // [a b c d e f]

八、map

m := make(map[string]int)
        m["one"] = 1
        m["two"] = 2
        fmt.Println(m)           // map[one:1 two:2]
        fmt.Println(len(m))      // 2
        fmt.Println(m["one"])    // 1
        fmt.Println(m["unknow"]) // 0

        r, ok := m["unknow"]
        fmt.Println(r, ok) // 0 false

        delete(m, "one")

        m2 := map[string]int{"one": 1, "two": 2}
        var m3 = map[string]int{"one": 1, "two": 2}
        fmt.Println(m2, m3)

初始化:

m := make(map[string]int)

map[string]int : string和int 分别表示key和value的类型

插入过程如下:

  1. 根据key值计算出哈希值
  2. 查找该key是否已经存在,如果存在则直接更新值
  3. 如果没有找到key,则将这一对key-value插入

删除:

delete(map, key) 函数用于删除集合的元素, 参数为 map 和其对应的 key。删除函数不返回任何值。

九、range

range是Go语言系统定义的一个函数。 函数的含义是在一个数组中遍历每一个值,返回该值的下标值和此处的实际值。

例如课程案例中的代码:

nums := []int{2, 3, 4}
        sum := 0
        for i, num := range nums {
                sum += num
                if num == 2 {
                        fmt.Println("index:", i, "num:", num) // index: 0 num: 2
                }
        }
        fmt.Println(sum) // 9

i表示的就是该值的下标,而num就是该值。

十、func

package main

import "fmt"

func add(a int, b int) int {
        return a + b
}

func add2(a, 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) // 3

        v, ok := exists(map[string]string{"a": "A"}, "a")
        fmt.Println(v, ok) // A True
}

Go的方法跟java的大不相似,Go的方法返回值类型放在了方法的最后面,而java则是在方法名前面,当然参数的声明也是一样。

Go的函数是支持多返回值的,从第13行就可以明显的看出。而且也无须前置声明函数。

十一、point

ptr := &a

其中 a 代表被取地址的变量名,变量a的地址使用变量 ptr 进行接收,ptr 的类型为T,称作 T 的指针类型, 代表指针。

为了更好地理解Go中的指针,请仔细看下面的代码:

package main

import "fmt"

func main() {

   var a int= 20  /* 声明实际变量 */
   var ip *int        /* 声明指针变量 */

   ip = &a  /* 指针变量的存储地址 */

   fmt.Printf("a 变量的地址: %x\n", &a  )

   /* 指针变量的存储地址 */

   fmt.Printf("ip 变量储存的指针地址: %x\n", ip )


   /* 使用指针访问值 */

   fmt.Printf("*ip 变量的值: %d\n", *ip )

}

//执行结果
a 变量的地址: 20818a220
ip 变量储存的指针地址: 20818a220
*ip 变量的值: 20

每个变量都拥有地址,指针的值就是地址。

十二、结构体

在声明结构体之前我们首先需要定义一个结构体类型,这需要使用type和struct,type用于设定结构体的名称,struct用于定义一个新的数据类型。

type user struct {
        name     string
        password string
}

定义好了结构体类型,我们就可以使用该结构体声明这样一个结构体变量,如果要访问结构体成员,需要使用点号 . 操作符,格式为:结构体变量名.成员名。语法如下:

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)                 // {wang 1024} {wang 1024} {wang 1024} {wang 1024}
        fmt.Println(checkPassword(a, "haha"))   // false
        fmt.Println(checkPassword2(&a, "haha")) // false

结构体指针:

关于结构体指针的定义和申明同样可以套用前文中讲到的指针的相关定义,从而使用一个指针变量存放一个结构体变量的地址。

定义一个结构体变量的语法:var struct_pointer *Books。

这种指针变量的初始化和上文指针部分的初始化方式相同struct_pointer = &Book1,但是和c语言中有所不同,使用结构体指针访问结构体成员仍然使用.操作符。

结构体方法:

接收者可以是struct类型或非struct类型,可以是指针类型和非指针类型。 接收者中的变量在命名时,官方建议使用接收者类型的第一个小写字母。

func (u *user) resetPassword(password string) {
        u.password = password
}

func main() {
        a := user{name: "wang", password: "1024"}
        a.resetPassword("2048")
        fmt.Println(a.checkPassword("2048")) // true
}

接收带指针的结构体方法是可以修改属性值的。

十三、error

go 中的异常处理和其他语言大不相同,像 Java、C++、python 等语言都是通过抛出 Exception 来处理异常,而 go 是通过返回 error 来判定异常,并进行处理。

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) // wang

        if u, err := findUser([]user{{"wang", "1024"}}, "li"); err != nil {
                fmt.Println(err) // not found
                return
        } else {
                fmt.Println(u.name)
        }

通过课程代码的实例就可以知道,再go的对于error的处理是非常清晰的,它可以在一个方法中去报出错误,让程序员可以清楚的找到错误的地方,并且去及时的改正。

errors.New()可能是以后处理error最普遍的用法。

十四、String

一个字符串是一个不可改变的字节序列,字符串通常是用来包含人类可读的文本数据。和数组不同的是,字符串的元素不可修改,是一个只读的字节数组。每个字符串的长度虽然也是固定的,但是字符串的长度并不是字符串类型的一部分。

a := "hello"
        fmt.Println(strings.Contains(a, "ll"))                // true
        fmt.Println(strings.Count(a, "l"))                    // 2
        fmt.Println(strings.HasPrefix(a, "he"))               // true
        fmt.Println(strings.HasSuffix(a, "llo"))              // true
        fmt.Println(strings.Index(a, "ll"))                   // 2
        fmt.Println(strings.Join([]string{"he", "llo"}, "-")) // he-llo
        fmt.Println(strings.Repeat(a, 2))                     // hellohello
        fmt.Println(strings.Replace(a, "e", "E", -1))         // hEllo
        fmt.Println(strings.Split("a-b-c", "-"))              // [a b c]
        fmt.Println(strings.ToLower(a))                       // hello
        fmt.Println(strings.ToUpper(a))                       // HELLO
        fmt.Println(len(a))                                   // 5
        b := "你好"
        fmt.Println(len(b)) // 6

从课程案例中可以知道字符串中有很多系统定义的方法,这些方法对于处理String类型的数据是非常方便的,这些特点也类似于java中的String类型中所包含的方法,但是需要注意的是,当字符是中文的时候,他的字符串长度可能跟编码的格式有关。

十五、fmt

fmt包实现了类似C语言printf和scanf的格式化I/O。格式化动作(’verb’)源自C语言但更简单。

占位符:

// 通用verbs

%v  值的默认格式

%+v  类似%v,但输出结构体时会添加字段名

%#v    Go语法表示值

%T    Go语法表示类型

%%   百分号表示

// 浮点数

%t   true或false

// 整数

%b 表示二进制

%c 该值对应的unicode吗值

%d 表示十进制

%o 表示八进制

%q 该值对应的单引号括起来的go语法字符字面值,必要时会采用安全的转义表示

%x 表示为十六进制,使用a-f

%X 表示为十六进制,使用A-F

%U 表示为Unicode格式:U+1234,等价于”U+%04X”

// 浮点数与复数

%b 无小数部分、二进制指数的科学计数法,如-123456p-78;参见strconv.FormatFloat

%e 科学计数法,例如 -1234.456e+78 %E 科学计数法,例如 -1234.456E+78

%f 有小数点而无指数,例如 123.456

%F 等价于%f %g 根据实际情况采用%e或%f格式(以获得更简洁、准确的输出)

%G 根据实际情况采用%E或%F格式(以获得更简洁、准确的输出)

// string与[]byte

%s 输出字符串表示(string类型或[]byte)

%q 双引号围绕的字符串,由Go语法安全地转义

%x 十六进制,小写字母,每字节两个字符 (使用a-f)

%X 十六进制,大写字母,每字节两个字符 (使用A-F)

// Slice

%p 切片第一个元素的指针

// point

%p 十六进制内存地址,前缀ox

课程案例代码:

s := "hello"
        n := 123
        p := point{1, 2}
        fmt.Println(s, n) // hello 123
        fmt.Println(p)    // {1 2}

        fmt.Printf("s=%v\n", s)  // s=hello
        fmt.Printf("n=%v\n", n)  // n=123
        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.141592653
        fmt.Println(f)          // 3.141592653
        fmt.Printf("%.2f\n", f) // 3.14

十六、time

核心方法time.Unix(),来获取时间戳

now := time.Now()
        fmt.Println(now) // 2022-03-27 18:04:59.433297 +0800 CST m=+0.000087933
        t := time.Date(2022, 3, 27, 1, 25, 36, 0, time.UTC)
        t2 := time.Date(2022, 3, 27, 2, 30, 36, 0, time.UTC)
        fmt.Println(t)                                                  // 2022-03-27 01:25:36 +0000 UTC
        fmt.Println(t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute()) // 2022 March 27 1 25
        fmt.Println(t.Format("2006-01-02 15:04:05"))                    // 2022-03-27 01:25:36
        diff := t2.Sub(t)
        fmt.Println(diff)                           // 1h5m0s
        fmt.Println(diff.Minutes(), diff.Seconds()) // 65 3900
        t3, err := time.Parse("2006-01-02 15:04:05", "2022-03-27 01:25:36")
        if err != nil {
                panic(err)
        }
        fmt.Println(t3 == t)    // true
        fmt.Println(now.Unix()) // 1648738080
        

从课程案例给出的代码来分析可以看出,go对于时间格式的展示做的是相当的简洁。

time.Now()来获取当前系统的时间。

time.Date()直接写入年月日就可以得到想要的时间格式。

t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute()通过这些方法也可以去获取自己想要的时间点。

t.Format("2006-01-02 15:04:05")这里的格式就得用案例中给的时间来表示,这一点就有点差强人意了。

go的time方法中也给出了获取时间段的方法,t2.Sub(t)。

十七、实战中的总结

通过字节内部课:Go 语言上手 - 基础语法中的三个简单的小项目,让我加深了对于Go语言语法的理解,猜数字游戏的案例,让我想到了以前学习java过程中也写过一模一样的代码,感觉到了刚学习编程语言时的那种乐趣和热情;在线词典,利用爬虫去其他的网站上获取结果,让我感受到Go语言的强大,我当时也利用过java去爬取过校内的网站,但是效率和安全性都远远低于Go的性能;最后就是SOCKS5代理,让我初识Go就达到如此境界,感觉到未来的学习之路还是有着更大的压力。

引用:

因为初学Go语言,一些知识点需要自己去网上搜索大佬的文章,以下是这篇文章中出现的知识点的引用。