Go-环境配置与基础知识

环境配置

go语言的官网: https://go.dev/

在官网中, 可以下载到对应操作系统的安装包.

安装之后, Linux上默认的安装路径应该是在/usr/local/go中, 其中会有以下几个文件夹:

  • src: 里面存储了所有go的标准库的源代码.
  • bin: 里面存储了由go语言源码编译而成的可执行文件.

我们把这次安装的go叫做系统go.

安装之后, 需要设置若干环境变量:

  • GOROOT: 设置为系统go文件夹所在的位置, 例如/usr/local/go.

  • GOPROXY: 下载第三方包时走国内的代理, 可以通过这一个命令进行设置:

    # 这个命令需要写到.zshrc中
    go env -w GOPROXY=https://goproxy.cn
    
  • GO111MODULE: 将这个环境变量设置为on, 表示启用go mod管理依赖.

    go env -w GO111MODULE=on
    

项目结构

初始化模块

在创建完你的项目目录后, 在工作目录下执行:

go mod init <module名字>
  • 注意: 这个module的名字没必要和你的项目文件夹的名字一样.

package

  • Go语言中, 组成一个package的所有的go文件需要在同一个目录.
  • package的名字没必要和目录一致.
  • 同一个package中的所有代码成分可以互相调用, 就和没有分开文件写一样.

导入自己的package

导入语句是package, 假设在你项目的任意位置中的任意一个go文件中, 导入项目中任意一个位置的模块, 需要在文件开头写:

import "<module名字>/package所在的路径"

注意这个package的路径, 就是这个package相对项目根目录的路径, 但是在使用时, 还需要用package的名字, 而不是目录的名字.

运行项目

  • Go语言项目中, 只允许有一个main函数作为程序入口, 而且这个main函数的package名字必须是main.

  • 运行时, 只需要用go run <写main函数的文件名/main函数所在的目录>就可以启动项目.

依赖管理

Go语言的依赖管理非常简单, 当你在项目中, import了第三方库之后, 只需要在项目根目录执行:

go mod tidy

这个命令会自动检测你的项目中的所有import, 一旦这个导入没有在go.mod中同步, 那么就会自动同步, 这个同步包括两个方面:

  • 你的项目代码里import了, 但是go.mod中没有记录.
  • 你的项目代码里没有import, 但是go.mod中有记录.

匿名导入

给package起别名

当你用import导入了一个package, 可以给这个package起一个别名, 例如:

import (
    niubi "fmt"
)

然后使用这个package时, 就可以用:

niubi.Println("Hello World")

匿名导入

匿名导入就是把包的名字起成下划线_, 例如:

import (
    _ "fmt"
)

此时, 这个包无论使用还是没有使用, go的编译器都不会报错.

之所以会使用匿名导入, 是因为程序想要调用这个package的init函数, init函数就是在导入这个package之前首先会执行的一个函数.

基本数据类型

打印数据类型的占位符是%t.

整数

有符号:

  • int8: 8位.
  • int16: 16位.

  • int32: 32位.

  • int64: 64位.

无符号:

  • uint8: 8位, 和byte类型一样.
  • uint16: 16位.

  • uint32: 32位.

  • uint64: 64位.

布尔值

  • bool, 值可以是true/false.

浮点数

  • float32: IEEE-754 32位浮点数.
  • float64: IEEE-754 64位浮点数.
  • 打印n位小数的占位符是: %.nf.
  • 注意: 如果使用:=完成声明与赋值, 那么编译器会自动推断为float64.

字符串

  • 声明类型的关键词是string.
  • 如果在代码中用"hahaha"定义一个字符串, 那么这是一个字符串常量, 不可修改.
  • Go中, 字符串的本质就是一个不可变的byte数组.

    • 如果使用foreach循环遍历的话, 会得到byte类型的数据.
    • 如果需要按照字符打印, 可以使用%c占位符.
    • 注意, 如果字符中出现了汉字, 那么一个汉字占3个byte.
  • len()函数可以获取字符串的长度.

  • 字符串可以使用+进行拼接.

给类型起别名

可以使用type关键字给类型起一个别名, 文法是:

type 别名 原始类型

Go语言的变量

变量的声明

变量声明的文法是: var id1, id2, ... type, 例如:

var a string
var a, b, c int

如果只声明变量没有初始化, 那么变量默认初始化为零值.

变量的声明与初始化

变量声明与初始化有三种写法:

第一种: 先声明, 再初始化:

var a, b int
a, b = 1, 2

第二种:

var a, b int = 1, 2

第三种: 使用:=表达式

a, b := 1, 2

此时, 编译器会自动推断a, b的类型, 但是这种写法只适用于定义并初始化局部变量, 全局变量不能使用.

定义全局变量的一种写法

如果全局变量需要定义很多个, 那么可以这样写:

var (
    a int = 1
  b string = "haha"
)

类型转换

类型转换的文法是: type(id).

例如:

a := 1
var b float64 = float64(a)

常量

常量的定义格式是:

const id [type] = val
  • 如果不指明type, 那么编译器会自动推断.

例如:

const N int = 1;
const a, b, c = 1, false, "str"

Go语言中有一种特殊的常量叫做iota, 用法如下:

const (
    a = iota
    b
    c
      d = iota
      e
)

每一个iota的值都是0, 如果下面没有赋值, 那么会从iota的0值开始递增, 当再次遇到iota时, 值又变成0, 例如这个例子中, a = 0, b = 1, c = 2, d = 0, e = 1.

运算符

Go语言的运算符和C++的运算符完全一致.

条件语句

条件语句的格式如下:

if 布尔表达式 {

} else if 布尔表达式 {

} else {

}

注意, 不允许else if或者else单独占一行, 例如:

if ... {

}
else ... {

}

循环语句

  • for循环:

    for i := 0; i <= 10; i ++ {
      //...
    }
    
  • while循环的等价写法:

    for 布尔表达式 {
    
    }
    // 等价于while 布尔表达式 {}
    
  • 数组中的foreach循环:

    for i, element := range arr {
      // 其中i是下标, element是其中的元素
    }
    
  • breakcontinue的用法与其他语言一致.

函数

定义格式

函数的定义格式如下:

func function_name(para1 type, para2 type) returnType {

}

例如:

func max(num1 int, num2 int) int {
  //...
}

函数也可以返回多个值, 例如:

func swap(x string, y string) (string, string) {
  return y, x
}
// 使用时
func main() {
  a, b := swap("Google", "Niubi")
}

函数的数据类型

Go语言中的函数本质上也是一种数据类型.

参考下面的代码:

package main

import "fmt"

func main() {
  fmt.Printf("%T", f1)
}

func f1(a1 int, a2 int) string {

}

代码运行的结果是: func(int, int) string.

因此, 函数本质上也是一种数据类型, 由参数的数据类型和返回值的数据类型决定.

Go语言中函数的类型表达式就是func(paratype1, paratype2, ...) returntype

匿名函数

匿名函数是指函数不需要一个函数名, 定义出来就直接可以调用, 例如:

func main() {

  func(a, b int) {
    fmt.Println(a, b)
  }()

  a1 := func(a int) {
    return a
  }(1)

}

函数可以作为另一个函数的参数, 这就是回调函数.

函数闭包

函数的闭包结构有如下特征:

  • 首先, 必须有一个外层函数, 还有一个内层函数, 外层函数的返回值是内层函数.
  • 内层函数会操作外层函数的局部变量.

此时, 内层函数中的局部变量会改变生命周期, 外层函数的局部变量不会随着外层函数结束后销毁, 因为内层函数还在使用这个变量.

例如:

package main

import "fmt"

func increment() func int {

  i := 0

  fun = func() int {
    i ++
    return i
  }

  return fun
}

func main() {

  f1 := increment()

  fmt.Println(f1()) // 1
  fmt.Println(f1()) // 2
  fmt.Println(f1()) // 3

  f2 := increment()

  fmt.Println(f1()) // 1

}

Go语言的数组

数组的类型表达式是: [size]type, 例如[5]int.

定义一个数组:

var numbers [5]int

定义并初始化一个数组:

var numbers = [5]int{1, 2, 3, 4, 5}

使用:=定义并初始化一个数组:

numbers := [5]int{1, 2, 3, 4, 5}

访问数组:

fmt.Println(numbers[2])

注意, 数组类型有如下特点:

  • 数组在声明时, 必须指定长度, 并且指定长度之后, 长度不可变.

如果需要动态数组, 可以使用切片这种数据类型.

数组的长度可以使用len(arr)来获取.

切片

切片的定义

切片本质上是一个结构体, 它的定义如下:

type slice struct {
  array unsafe.Pointer
  len int
  cap int
}
  • array: 这个指针指向了一个数组, 因此, 切片在进行:=赋值的时候需要仔细考虑.
  • len: 表示目前切片的长度.
  • cap: 表示这个切片实际占用内存的大小, 内存需要分配比切片长度更长的内存, 当超过实际内存长度的时候需要扩容.

切片的创建

定义切片的类型关键字是[]type, 例如[]int等, 就相当于不指定数组大小的数组定义.

创建一个切片需要用make关键字, 例如:

my_slice = make([]int, 3)

表示创建一个元素是int类型, 长度是3的切片, 数据初始化为0值.

指针

Go语言中, 指针的类型表达式是:

var ptr *type

也就是在原来的type前面加一个*.

指针数组的表达式是:

var ptr_array []*type

Go语言中取址符号是&.

下面是一个例子:

package main

import "fmt"

const MAX int = 3

func main() {

  a := []int{1, 2, 3}

  var ptr [MAX]*int

  for i := 0; i < MAX; i ++ {
    ptr[i] = &a[i]
  }

}

Go语言中, 空指针的关键词是nil.

打印指针可以用%p占位符.

深拷贝与浅拷贝

规律如下:

  • Go语言中, 如果使用=赋值, 并且没有取指针&, 那么都是深拷贝.
  • 如果使用了&, 那么就是浅拷贝.
  • 注意, 如果一个结构体中有个成员变量是指针类型, 然后用:=赋值, 那么虽然是深拷贝, 但是指针拷贝的值是一样的, 指针指向的数据并没有拷贝 (切片).

结构体

结构体占位符

打印一个结构体变量可以使用%#v这个占位符, 使用这个占位符, 会把结构体的包, 成员变量名称都会打印出来.

成员变量

Go语言结构体的定义如下:

注意: 不同成员变量的声明之间没有`,`
type 结构体名字 struct {

  成员变量名1 type1
  成员变量名2 type2
  成员变量名3 type3
}

例如:

type Books struct {
  title string
  author string
  book_id int
}

创建结构体实例的代码:

book = Books{ title: "haha", author: "haha", book_id: 1 }

访问结构体成员变量可以用.

fmt.Println(book.title)
fmt.Println(book.author)
fmt.Println(book.book_id)

结构体作为函数参数, 它的类型就是Books

func printBook(book Books) {
  fmt.Println(book.title)
}

当结构体作为函数参数时, 数据会被拷贝, 也就是值传递, 如果不需要拷贝, 可以传递结构体指针.

结构体指针的定义如下:

var struct_pointer *Books
// 取结构体指针

struct_pointer = &book

对于结构体指针来说, 访问成员变量应该是*(struct_pointer).成员变量, 但是Go为了简化, 使用结构体指针访问成员变量时也用.

注意:

  • 结构体内的成员变量如果小写, 那么只能包内使用, 如果大写, 就可以包外使用.

成员函数

Go语言中, 不允许在结构体内部定义成员函数, 而是采用组合的方式, 将某个函数绑定成某个结构体的成员函数.

Go语言中结构体的成员方法有两种, 一种是值方法, 一种是指针方法.

  • 值方法: 方法内不需要修改成员变量.
  • 指针方法: 方法内需要修改成员变量.

假设我现在有一个结构体Student:

type Student struct {
  name string
}

假设我要有一个get方法, 获取name的值, 可以这样定义:

func (s Student) GetName() string {
  return s.name
}

这个方法就是值方法.

调用例子如下:

// name变量私有, 但是GetName是公共方法
student := Student {
  name: "haha"
}

fmt.Println(student.GetName())

那么Set方法就需要修改name的值, 就是指针方法:

func (s *Student) SetName(name string) {
  s.name = name
}

这个就相当于其他语言中的this指针.

一般来说, 都会选用指针方法, 因为值方法会涉及数据的拷贝, 在大对象中很影响性能.

map

Go语言中map是一种特殊的数据结构, 它的类型表达式是: map[key的类型]value的类型.

  • 注意, 有些数据类型不能作为key, 例如函数, 切片.
  • 所有数据类型都可以作为value.

创建空的map可以使用make函数:

// 长度是3的map
my_map = make(map[string]int, 3)
// 也可以创建长度不固定的map
my_map = make(map[string]int)

也可以直接创建并初始化: 注意, 不同的item之间有,隔开

funcMap := map[int]func() {
  1: first,
  2: second,
  3: third
}

插入一个键值对可以直接写:

my_map["c"] = 1

删除一个键值对可以用delete函数:

delete(my_map, "c")

遍历map中所有的键值对可以用foreach循环:

for key, value := range my_map {

}

注意, 如果要从map中根据key找value, 需要处理异常, 需要这样写:

ele, exists := my_map["c"]

此时, 如果对应的key没有在map中, 那么exists的值就是false, 需要做异常处理.

接口

接口的定义如下:

type interface_name interface {

  function_name1(para_name1 paratype1, ...) [return_type1]
  function_name2(para_name2 paratype2, ...) [return_type2]

}

如果要将一个结构体绑定到这个接口, 只需要将这个接口内的所有方法变成这个类的值方法或者指针方法即可, 例如:

type PersonFunction interface {

  GetName() string
  SetName(name string)
}

type Person struct {
  name string
}

// 实现了这两个函数之后, PersonFunction这个接口就绑定到了Person类上.
func (p Person) GetName() string {
  return p.name
}

func (p *Person) SetName(name string) {
  p.name = name
}