Go 语言入门指南:基础语法和常用特性解析| 豆包MarsCode AI刷题

55 阅读11分钟

下面是本次课程的学习笔记,简单记录自己的学习思路和关注的知识点。

1 GO基础语法

1.1 什么是go

首先介绍了go作为一门编程语言,具有什么样的优点、特性

image.png

其中,丰富的标准库能够使开发者减少对第三方库的依赖;跨平台就不用交叉编译,更加方便;垃圾回收使得开发者不需要关注内存的使用和释放,更好地专注自己的功能开发。

1.2 go在业务中的优势

image.png

2 go开发环境

首先安装go,到官网下载安装包,然后根据提示安装即可。 我用的mac m1,之前就已经安装过了,这里就不重复安装了。

然后我选用vs code作为ide. 安装成功后,使用和python相似,使用go命令。下面是查看自己安装的go的版本的指令。 image.png

课程还提供了在线云IDE,gitpods ,可以直接用准备好的云环境,运行示例项目。

3 基础语法

因为我先学的是C和python,自我感觉,编程风格还是蛮像二者的结合的。下面的很多学习也会和这两个比较一下,提醒我自己。

3.1 go的helloworld程序

学习编程的第一步都是helloworld。新建一个main.go, 写入以下代码

package main

import ("fmt")

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

可以看到程序有结构清晰的三个部分

  • package: 声明程序属于哪个包, main包就是程序的入口文件
  • import: 导入包,fmt是标准库,用于标准输入输出和格式化操作。
  • func: 程序的函数体,这里调用了fmt的Println,顾名思义就是打印函数。

之后写代码的时候显然主要的也就是这三部分,声明当前程序的包, 引入需要使用的库,写函数。

然后运行这个程序,使用的命令是

go run mian.go

image.png

除了直接运行,还可以编译后运行

go build main.go
./main

3.2 变量类型

go是一门强类型语言,每一个变量都有变量类型。声明的时候,变量类型是在变量名之后的,和C相反。

使用变量时,变量的声明有两种方式。

  • var 变量名 变量类型 = 值 (变量类型不是必须的,可以由编译器直接推导
  • 变量名 := 变量类型 值

基础类型有字符串、整型、浮点型、布尔型。

var a string = "initial"
g := a + "foo"

go中字符串是内置类型,可以直接加号拼接。 字符串得用双引号括起来。

var b,c int = 1,2
const h = 50000
d := 1

var e  float64
f := float32(e)

const 就是常量。 go中的常量没有确定的变量类型,可以根据使用的上下文自动确定类型。

浮点型有float64和float32

var g = true

3.3 控制流

所有的语言的控制流不外乎选择和循环。

3.3.1 if-else

package main
import "fmt"
func main(){
    if 1 > 0 {
        fmt.Println("1 is positive")
    } else if 1 <  0{
        fmt.Println("1 is negative")
    } else {
        fmt.Println("1 is 0")
    } 
}

if-else 条件不需要括号,必须要用{}

3.3.1 for循环

go中只有for循环 for不写任何条件就是死循环。 循环的结构和C是很相似的,也可以省略三个条件中任意条件,比如简化成类似while的写法。

i := 1
for i <= 3 {
    i = i + 1
}

for n := 0; n < 5; n++ {
    if n%2 == 0{
        continue
    }
    
}

3.3.2 switch

go中的switch不需要break,程序只会选择一个分支。 而且switch功能更加强大,支持任意的数据类型。

switch a {
case 1:
        b := 1
case 2,3:
        b := 3
}

还可以用来取代if-else,在switch后面不加任何变量

t := time.Now()
switch {
case t.Hour() < 12:
    pass
default:
        pass
}

当有很多个If-else分支的时候,可以更加清晰

3.3.3 数组

具有编号且长度固定的元素类。 go的数组相比C和python就有点奇形怪状了

var a [5]int
b := [5]int{1,2,3,4,5}
var d [2][3]int
len(a)

表示数组的[]放在元素类型之前,其中表示数组的长度。数组长度固定,因此不太常用。

3.3.4 切片

数组的长度是固定的,这点和C相似,但是go有类似python动态数组的切片。 切片是可以任意更改长度的数组,用法和数组都相同。 创建切片使用make,可以指定长度

s := make([]string,3)
s = append(s, "d")

append的用法和python中很不相同,首先不是一个类的方法,其次,append完后必须赋值给原来的数组。 因为append本质上是对数组扩容创建了一个新的切片,所以需要赋值回去。 切片具有拷贝操作,也有和python中的切片相同的操作,但是没有负数操作。

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

简化的切片创建就是不指定数组的长度

good := []string{"g", "o", "o", "d"}

good = append(good, "bye")
fmt.Println(good) // [g o o d]

3.3.4 map

类似c中的哈希表,python中的字典。map是完全无序的,不会按插入顺序输出。 也是用make创建, 创建的时候,map[key]value,其中一个指定key的数据类型,一个指定value的数据类型。 访问的时候,使用方括号来指定key的值。 go中的map访问不存在的key的时候不会报错,会给出默认值。还可以添加一个变量,比如下面的ok, 表示是否存在这个key。

m := make(map[string]int)
m["one"] = 1
fmt.Println(m["unknow"]) // 0
r, ok := m["unknow"]
fmt.Println(r, ok) // 0 false
delete(m, "one")

删除用的是delete。

3.3.5 range

用于遍历,使用非常简单,配合for使用,在range 后加上遍历的对象,赋值给输出变量。 可以遍历数组

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
        }
}

使用range的时候会返回两个值,一个是索引,一个是值。 还可以遍历map,返回的是key和value,因为map的无序性,每次的输出结果可能是不同的。

m := map[string]string{"a": "A", "b": "B"}
for k, v := range m {
        fmt.Println(k, v) // b 8; a A
}
for k := range m {
        fmt.Println("key", k) // key a; key b
}

3.3.6 函数

使用关键字func定义函数,注意变量类型是后置的。

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

函数支持返回多个值。一般比较常见的是,第一个值是真正的返回结果,第二个值是错误信息。比如下面的ok

func exists(m map[string]string, k string) (v string, ok bool) {
    v, ok = m[k]
    return v, ok
}

3.3.7 指针

相比C和C++中的指针,支持的操作比较有限,最主要的用途是对参数进行修改 比如go中函数调用传递参数的时候,传入的是拷贝,而不是真正的地址。

func add2(n int) {
    n += 2
}

这样写的话,调用函数不会修改原来的变量n,函数失效后,+2的n也就失效了,因为它只是一份拷贝。 所以需要指针, 传入参数时和C一样,需要加上取址符&,使用的时候需要加上*表示对指针取值。

func add2ptr(n *int) {
    *n += 2
}

func main() {
    n := 5
    add2(n)
    fmt.Println(n) // 5
    add2ptr(&n)
    fmt.Println(n) // 7
}

指针的声明, 在变量前加上*。 指针的赋值,就是将变量取址后赋值给指针

n := 5
var p *int
p = &n

C的指针那么难,go中的指针应该也有挺多值得研究的,之后在细看。

3.3.8 结构体

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

type user struct {
    name     string
    password string
}

初始化,没有被初始化的字段会赋值默认值。

a := user{name: "wang", password: "1024"}
b := user{"wang", "1024"}
c := user{name: "wang"}

访问字段,用.来访问字段

var d user
d.name = "wang"
d.password = "1024"

结构体也有指针,可以用指针传递结构体参数,避免拷贝的开销。

3.3.9 结构体方法

类似于类的成员函数。 先看不用结构体方法实现的函数

func checkPassword(u user, password string) bool {
	return u.password == password
}

写成结构体方法的时候,不在class里面,而是指定对应的结构体,从参数挪到func后面函数名前,加上括号。 使用的时候和类调用成员函数一样,用.来访问方法。

func (u user) checkPassword(password string) bool {
    return u.password == password
}

// 带指针才能对结构体进行修改
func (u *user) resetPassword(password string) {
    u.password = password
}

a.resetPassword("2048")
fmt.Println(a.checkPassword("2048")) // true

有带指针和不带指针两种写法,带指针才能修改结构体。

3.3.10 错误处理

使用一个单独的返回值来传递错误信息。 在Go语言中,error 是一个内置的接口类型,用于表示错误条件。

接口方法 error.New(),该方法返回一个字符串,描述了错误的具体内容。nil 值表示没有错误发生。

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")
}

可以用简单的if-else处理错误,需要判断error是否存在,没有error说明有返回值,否则存在空指针错误。

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

3.3.11 字符串操作

内置的strings类型,具有很多种方法。

  • strings.Contains(str, substr) 判断str是否包含substr
  • strings.Count(str,substr) 字符串计数
  • strings.Index(str, substr) 查找某个字符串的位置
  • strings.Join(str1, str2, concat) 连接字符串
  • strings.Repeat(str1, num) 重复num次字符串str
package main

import (
	"fmt"
	"strings"
)

func main() {
	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
}

3.3.12 字符串格式化

fmt标准库中具有很多种方法。

比如Println,可以打印多个变量并且换行 然后就是Printf, 和C很像, 不同的是,可以用%v打印任意类型的变量,还可以加上+或#来获取详细的类型

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} 打印出结构体的完整Go语法表示,包括类型和字段值

f := 3.141592653
fmt.Println(f)          // 3.141592653
fmt.Printf("%.2f\n", f) // 3.14

3.3.13 json处理

一个已有的结构体,只要每个字段的首字母是大写,就可以用json.Marshal进行序列化得到byte数组。 可以使用json标签指定在JSON中使用的字段名,比如下面的age

type userInfo struct {
	Name  string
	Age   int `json:"age"` // json别名
	Hobby []string
}

反序列化用json.Unmarshal

a := userInfo{Name: "wang", Age: 18, Hobby: []string{"Golang", "TypeScript"}}
buf, err := json.Marshal(a)
if err != nil {
    panic(err)
}
fmt.Println(buf)         // [123 34 78 97...]
fmt.Println(string(buf)) // {"Name":"wang","age":18,"Hobby":["Golang","TypeScript"]}

// 使用`json.MarshalIndent`函数将结构体实例`a`编码为格式化的JSON字符串,其中`""`表示最外层不添加任何前缀,`\t`表示每个层级使用制表符进行缩进。
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:[]string{"Golang", "TypeScript"}}

3.3.14 时间处理

引入time模块。

  • time.Now() 获取当前时间
  • time.Date() 获取某个时区的时间
  • time.Unix() 获取时间戳
now := time.Now()
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.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

3.3.15 数字字符串解析

有一个表示数字的字符串,可以直接解析成数字。需引入标准库 strconv

f, _ := strconv.ParseFloat("1.234", 64)
fmt.Println(f) // 1.234

n, _ := strconv.ParseInt("111", 10, 64)  // 字符串 进制 返回数据的位数
fmt.Println(n) // 111

n, _ = strconv.ParseInt("0x1000", 0, 64)
fmt.Println(n) // 4096

n2, _ := strconv.Atoi("123")
fmt.Println(n2) 

n3 := strconv.Itoa(123)
fmt.Println(n3) // 123

3.3.16 进程信息

引入os 获取进程的命令行参数

// go run example/20-env/main.go a b c d
fmt.Println(os.Args)           
// [/var/folders/8p/n34xxfnx38dg8bv_x8l62t_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"))

通过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