go基础

安装

  1. 官网下载对应系统的对应版本。
  2. 放到想要安装的位置,随后修改环境变量指向<install-path>/bin
  3. 添加用户环境变量export PATH=$(go env GOPATH)/bin:$PATH

代理

在下载包的时候需要设置代理,不然下载不下来

# 设置环境变量
export GO111MODULE=on
export GOPROXY=https://goproxy.cn

入门

hello world

package "main"
import "fmt"
import "math/rand"

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

// 运行:go run main.go
  • 每个go程序都由包构成,程序从main开始执行。上述程序通过导入路径fmtmath/rand来使用这两个包。按照约定,包名和导入路径的最后一个元素一致。例如,"math/rand" 包中的源码均以 package rand 语句开始。
  • 通常导出变量都是以大写字母开头,例如:math.Pi

数据类型

指针

Go语言有指针,指针保存了值的地址。

// 类型*T是指向T类型的指针,其零值为nil。
var p *int

// &操作符会生成一个指向其操作数的指针。
i := 42
p = &i

// *操作符表示指针指向的底层值
fmt.Println(*p)
*p = 21
// go没有指针运算符

结构体(struct)

  • 一个结构体就是一组字段。
  • 结构体通过.号访问字段。
  • 结构体指针:对于指向一个结构体的指针p,访问字段X的时候可以通过(*p).X的方式,也可以通过go语言隐式解引用的方式p.X就可以。
  • 结构体字面量:使用Name: 的语法可以仅列出部分字段(字段名和顺序无关)
  • &返回一个指向结构体的指针。这种方式不会导致悬空指针,原因在于Go语言有垃圾回收器(GC)。这里p指向的实例是在上分配的,虽然是在main函数的作用域内创建的变量,但是可以通过返回值的方式返回出去,所以必须在上分配。当没有变量指向这个实例的时候,实例会被置为不可达,在未来的某个时刻,GC会检测到这块内存没有被任何活跃的变量引用,而将其回收。
package main
import "fmt"

type Vertex struct {
	X int
	Y int
}

func main() {
	data := Vertex{5, 6}
	data.X = 10
	fmt.Println(data, data.X)

	var (
		V1 = Vertex{1, 2}
		V2 = Vertex{X: 10} // Y被隐式的赋予0
		V3 = Vertex{} // X Y均被隐式的赋予0
		p = &Vertex{1, 2} // 创建一个*Vertex类型的结构体指针。
	)

数组(array)

  • 通常数组是固定长度的[n]T表示一个nT类型的元素的数组,var a [10]int
  • 数组是值类型:赋值给另外一个变量的话是创建了一个副本,修改新的值不会影响原始的值。
package main
import "fmt"

func main() {
	var a [2]string
	a[0] = "hello "
	a[1] = "world"
	fmt.Println("打印数组:", a)

	b := [5]int{1,2,3,4,5}
	fmt.Println("打印短变量声明:", b)

	c := b[2:3]
	fmt.Println("slice的数值:", c)
}

切片(slice)

  • 相当于"指针"/“引用”
  • 动态长度[]T{...}长度跟初始化用的数据数量相关,可以通过append(a, "a")的方式添加元素
  • 数组的切片a[low: high]slice不做数据的拷贝,只是原始数组部分内存的"别名",修改某些位置内容的话,所有slice都会生效。
    • 这时如果对切片append的话,分两种情况:1)如果容量没有超出数组的长度时,会直接修改原始数组中的值。2)如果容量超出数组长度的话,创建一个更大容量的存储,并且修改slice的指向,这时候修改slice中的数值不再会影响原始数组的值。
  • 切片的容量(cap)和长度(len):切片的容量和长度取决于其初始指向,和底层存储容量,见下面例子。
  • 切片的默认值:nil
  • 通过make创建零值slice:make([]int, 3, 5),其中3是len,5是cap
  • 切片的切片:类似于动态二位数组
  • range操作:for i, v := range data {...}iindexv数值的拷贝,如果v不是数值(比如slice),则只是v的拷贝,而不是v引用的内容的拷贝。
func main() {
	// 这里a就是切片
	a := []string{"a", "b", "c"}
	// 可以向a后面添加元素
	append(a, "d") // a成为["a", "b", "c", "d"]

	// 切片的写法和python相似
	var a [10]int
	a[0:10]
	a[:10]
	a[0:]
	a[:]

	s := []int{2, 3, 5, 7, 11, 13}
	printSlice(s)
	// 打印:len=6, cap=6 [2 3 5 7 11 13]
	
	s = s[:0]
	printSlice(s)
	// 打印:len=0, cap=6 []
	
	s = s[:4]
	printSlice(s)
	// 打印:len=4, cap=6 [2 3 5 7]
	
	s = s[2:]
	printSlice(s)
	// 打印:len=2, cap=4 [5 7]

	// slice的slice
	board := [][]string{
		[]string{"_", "_", "_"},
		[]string{"_", "_", "_"},
		[]string{"_", "_", "_"},
	}

	// range操作
	pow := []int{1, 2, 4, 8, 16}
	for i, v := range pow {
		fmt.Printf("2**%d = %d\n", i, v)
	}
	// 还可以这样写
	// for i, _ := range pow
	// for _, v := range pow
	// for i := range pow
}

func printSlice(s []int) {
  fmt.Printf("len=%d, cap=%d %v\n", len(s), cap(s), s)
}

字典(map)

  • map[<key_type>]<value_type>
  • 字典的默认值:nil,没有key也不能添加key,其实相当于空指针。
  • make创建mapm := make(map[string]int)
  • 字典字面量:在代码中直接创建和初始化map类型变量的语法,通过这种语法,可以不用先创建字典,然后再添加元素。
package main
import "fmt"

type Vertex struct {
  X int
  Y int
}

var m map[string]Vertex
func main() {
  if m == nil {
    fmt.Println("m尚未初始化")
    m = make(map[string]Vertex)
  }
  m["hhhh"] = Vertex{X: 1, Y: 2}
  fmt.Println(m)

  // 字典字面量
  m2 := map[string]Vertex {
	  "test1": {1, 2}, // 只能在字面量的情况下省略值的类型
	  "test2": {3, 4}
  }
  fmt.Println(m2)

  // 增加键值对/修改键值对
  m2["test3"] = Vertex{5, 8}
  fmt.Println(m2)

  // 删除键值对
  delete(m2, "test3")
  fmt.Println(m2)

  // 判断键是否存在
  res, ok := m2["test3"]
  // 如果键存在的话,ok是true,res是对应的值,否则ok是false,且res是对应类型的零值
  

}

变量&常量

变量

  • 没有明确初始化的变量声明会被赋予对应类型的 零值
    • 零值是:
      • 数值类型为 0
      • 布尔类型为 false
      • 字符串为 ""(空字符串)。
// 声明变量,并且写类型
var a int = 10

// go编译器根据初值自动推断类型
var b = "asdfasfd"

// 短变量声明,仅在函数内使用,不能用在包级别的层级
c := 1234

常量

  • Go语言的类型转换只能通过显式的方式。
  • 常量不能用 := 语法声明。
  • 数值常量是高精度的,一个未指定类型的常量由上下文决定其类型。
// 常量类型自动推断
const d = 10
// 常量块
const (
	StatusOK = 200
	StatusNotFound = 404
	StatusError = 500
)

//  使用 iota 创建枚举,go中没有传统的enum,可以使用iota创建自增
const (
	Sunday = iota // 从零开始,后续逐个加1
	Monday
	Tuesday
	Wednesday
	Thursday
	Friday
	Saturday
)

// 高级用法(定义数据大小)
const (
	_ = iota
	KB = 1 << (10 * itoa) // 1 << (10 * 1) = 1024
	MB = 1 << (10 * itoa) // 1 << (10 * 2) = 1048576
	GB = 1 << (10 * itoa) // 1 << (10 * 3) = 1073741824
	TB = 1 << (10 * iota) // ...
)


// 数值常量 `值` 的属性
package main
import (
  "fmt"
)
const (
  Big = 1 << 100
  Small = Big >> 99
)
func needInt(x int) int { return x * 10 + 1 }
func needFloat(x float64) float64 {
  return x * 0.1
}
func main() {
  // 这里常量Small、Big的类型由上下文决定,只是一个高精度的值
  fmt.Println(needInt(Small))
  fmt.Println(needFloat(Small))
  fmt.Println(needFloat(Big))
}

函数

  • 函数也是值,可以作为其他函数的参数或者返回值。
  • 可以是一个闭包(closure):闭包是编程中一个非常重要的概念,闭包是一个函数,它会记住自己被创建时的环境(捕获相关变量,延长这些变量的生命周期到闭包的生命周期结束,即便这些变量所在的作用域早已消失)。
// go语言是强类型语言,包括**参数**和**函数返回值**。
func add(x int, y int) int {
	return x + y
}

// 当连续两个或多个函数的已命名形参类型相同时,除最后一个类型以外,其它都可以省略。
func add(x int, y int) {}
// 上式可以简化为
func add(x, y int) {}

// 函数可以返回多个值
func swap(x, y string) (string, string) {
	return y, x
}

运算符

  • i++,但是没有++i这种写法。

流程控制

for循环

  • go只有一种循环结构for循环,基本的for循环由三部分组成,他们用分号隔开:
    • 初始化语句:在第一次迭代前执行
    • 条件表达式:在每次迭代前求值
    • 后置语句:在每次迭代的结尾执行
  • 初始化语句通常为一个短变量声明,并且生命周期仅在for循环中。
  • 初始化语句和后置语句都是可选的,这时候前后两个分号都可以省略(也就是while语句在go中是for语句)。
package main

import "fmt"

func main() {
	sum := 0
	for i := 0; i < 10; i++ {
		sum += i
	}
	fmt.Println("和为", sum)

	// 'while'语句
	for x < 10 {
		...
	}
	// 无限循环
	for {
		...
	}
}

if语句

  • if语句和for一样,条件不需要加(),但是执行过程{}是必须的。
  • if语句也可以有初始化语句(短变量声明),该短变量的声明周期也只在if/if...else 之内。
package main

import (
	"fmt"
	"math"
)

func main() {
	x := 10
	if x < 20 {
		...
	} else {
	}

	if v := math.Sqrt(x); v < 22 {
	}
}

switch语句

  • goswitch语句和其他语言不太一样,只会运行选定的case,也就是默认是直接break,除非增加fallthrough
  • case语句后面无需是常量,且取值不限于整数。
package main

import (
	"fmt"
	"runtime"
	"time"
)
func main() {
	fmt.Println("Go 运行的系统环境:")
	switch os := runtime.GOOS; os {
		case "darwin"
			fmt.Println("macOS.")
			// fallthrough
		case "linux":
			fmt.Println("Linux.")
		default:
			fmt.Printf("%s.\n", os)
	}

	// case后接func或公式计算
	today := time.Now().Weekday()
	switch time.Saturday {
	case today + 0:
		fmt.Println("今天")
	case today + 1:
		fmt.Println("明天")
	case today + 2:
		fmt.Println("后天")
	default:
		fmt.Println("很多天后")
	}

	t := time.Now()
	// 无条件switch语句可以当作一长串的if-then-else
	switch {
		case t.Hours() < 12
			fmt.Println("早上好!")
		case t.Hours() < 17
			fmt.Println("下午好!")
		default:
			fmt.Println("晚上好!")
	}
}

defer推迟

  • defer语句会将函数推迟到外层函数返回之后执行。
  • 推迟调用的函数其参数会立即求值,但直到外层函数返回前该函数都不会被调用。
  • defer栈:当有多个defer调用时,在函数退出时会按照先进后出的方式执行。
package main

import "fmt"

func main() {
	fmt.Println("counting")
	for i := 0; i < 10; i ++ {
		defer fmt.Printf("%d.", i)
	}
	fmt.Println("done")
}

方法(methods)

  • Go语言没有,但是你可以在types上定义定义方法(methods)。
  • 方法是一个有一个接收器参数的函数。
  • 如果要就地修改原始值的话,需要使用指针接收器
    • 定义方式:func (v *Vertex) Scale(s float64) float64 {...}
    • 对于指针接收器,可以使用值或者指针通过v.Scale(1.0)的方式调用,go编译器自动将值转换成(&v).Scale(1.0)的形式,方便使用。
  • 原则上同一个type上的方法要使用同一类型的接收器,而不是混合使用。
  • interfaces(接口):interface定义了一系列方法的签名。
package main

import (
	"fmt"
	"math"
)

type Vertex struct {
	X, Y float64
}

// 这里(v Vertex)就是接收器参数
func (v Vertex) Abs() float64 {
	return math.Sqrt(v.X * v.X + v.Y * v.Y)
}

// 方法还可以用于非结构体参数
type MyFloat float32

func (v MyFloat) Abs() float32 {
	if v < 0 {
		return float32(-v)
	}
	return float32(v)
}

func main() {
	v := Vertex(3, 4)
	fmt.Println(v.Abs())
}

泛型(generics)