01 go学习笔记 - xiaoxin01/Blog GitHub Wiki

常量

定义常量

const(
    a = 1
    b = 2
    ...
)

const(
    c = iota
    d
    e
    ...
)

注:iota一行代码递增一次,所以 a,b = iota, iota,a和b都是0

函数

函数当做参数

类似 c#的action, func

func function(action func(int, int) int, a int, b int) int { return action(a,b) }

自定义数据类型

type Sum func(int, int) int

可变参数

func Sum(args ...int) int {
	fmt.Printf("type of args is %T\n", nums)
	for i, v := range args {
    }
}

可变参数在内部是将参入的参数转换成了切片,所以上述将会打印 type of args is []int。

问题:既然会转换为切片,直接使用切片不就可以了?使用可变参数而不是切片的优点:

  • 无需在每个函数调用期间创建切片。
  • 可变参数可以不传入参数,如果是以切片作为参数,则需要创建一个空片,只是为了满足该find函数的签名。
  • 我个人认为带有可变参数功能的程序比带有切片的程序更具可读性:)

init函数

优先于main函数执行,可以做一些初始化操作

匿名函数

sum := func(a int, b int) int { return a+b }(1, 2)

闭包

func makeSuffix(suffix string) func(string) string { return func(name string) string { return name + suffix } }

defer延迟执行,栈的方式

函数执行的参数值也同时入栈。

创建资源后,直接 defer close().

函数参数传递

  • 值传递:数组,struct
  • 引用传递:指针,切片,map,chan,interface

time

按照格式输出时间字符串

now.Format("2006/01/02 15:04:05")

时间常量

内建函数

len()

new(),主要用来分配值类型内存;make(),主要用来分配引用类型内存

异常处理

defer func() {
    err := recover()
    if err != nil {
        // err handle
    }
}()

自定义错误

errors.New()

err := func() {
    return errors.New("test error")
}()
if nil= err {
    panic(err) // print error message and exit
}

数组和切片

定义方式

array1 := [3]int {1,2,3}
array2 := [...]int {6,7,8}

// 指定下标元素的值
array3 := [...]{ 1: 9, 0:11, 2:10}

数组属于 值类型

长度属于数组的一部分,传递数组时定义需要加上长度,否则就是传递的切片

切片

slice := array[1:3] // 下标1到3的2个元素,不包含下标3的元素 // len(slice) == 2 // cap(slice) == 4

type slice struct {
    ptr *[]int
    len int
    cap int
}
// 定义切片方式2:
slice := make([]int, len, [cap])

//定义切片方式3:
slice2 := []int{1, 2, 3}

//追加元素:
slice3 := append(slice2, 4, 5, 6)
slice3 := append(slice2, slice2...)

append会创建一个新的数组(如果原来的切片容量不足以容纳追加的元素)

测试创建新数组逻辑,以2^n扩容,并做值拷贝

	slice := make([]int, 1)
	ptr := &slice[0]

	for i := 0; i < 1000; i++ {
		slice = append(slice, i)
		if ptr != &slice[0] {
			ptr = &slice[0]
			fmt.Printf("%v %d\n", &slice[0], cap(slice))
		}
	}

打印:

0xc0000a4010 2
0xc0000aa000 4
0xc0000ac000 8
0xc0000ae000 16
0xc0000b0000 32
0xc0000b2000 64
0xc0000b4000 128
0xc0000b6000 256
0xc0000b8000 512
0xc0000ba000 1024

copy,拷贝切片元素,会考虑切片的容量,越界不会报错

切片的内存优化

切片包含对基础数组的引用。只要切片在内存中,就无法对数组进行垃圾回收。假设我们有一个非常大的数组,而我们只对处理其中的一小部分感兴趣。此后,我们从该数组创建一个切片,然后开始处理该切片。由于切片引用了该数组,因此该数组仍将保留在内存中。

解决此问题的一种方法是使用复制功能func copy(dst, src []T) int 制作该切片的副本。这样,我们可以使用新切片,并且可以对原始数组进行垃圾回收。

func countries() []string {
    countries := []string{"USA", "Singapore", "Germany", "India", "Australia"}
    neededCountries := countries[:len(countries)-2]
    countriesCpy := make([]string, len(neededCountries))
    copy(countriesCpy, neededCountries) //copies neededCountries to countriesCpy
    return countriesCpy
}
func main() {
    countriesNeeded := countries()
    fmt.Println(countriesNeeded)
}

排序

  • 内部排序
  • 外部排序

map

声明一个map,map需要通过make分配空间以后才能使用

var a map[string] int
var b map[int] int
var c map[string] map[string] string

//分配一个可以存储10个键值对的空间

a = make(map[string] int, 10)

// 方式2
d := make(map[string] int, 10)

// 方式3
e := map[string] int{"cd": 1}

// 删除元素
delete(e, "cd")

// 查找元素
value, ok := e["cd"]

map切片,179

切片按顺序输出

  1. 把key放入切片
  2. 对切片排序(sort.Ints())
  3. 遍历切片,通过key取map的值

map是引用类型,且可以自动扩容(切片不可以,需append)

struct

//新建对象方式1
obj := new(Person)
obj2 := &Person{}

结构体的属性在内存中是连续存储的,值类型连续,引用类型的指针也是连续存储

地址的长度取决于当前运行环境(并不是机器环境,可能机器是64bit,但运行环境是32bit)

结构体可以再次定义新的数据类型(取别名),但两个数据类型不能相互赋值,但可以强制转换

tag

应用:在转成json串时,改变字段的名称为小写

import "encoding/json"

type Person struct {
    Name string `json:"name"`
    Age int `json:"age"`
}


jsonStr, err := json.Marshal()

结构体方法

func (p Person) run () {
    fmt.Printf("%s", p.Name) // 这里的p是值拷贝,改变他的值不会影响到调用对象的值
}

func (p *Person) grow () {
    p.Age++ // 这里的p是引用,改变他的值会影响到调用对象的值
}

int等都可以有方法,访问访问用大小写控制,String()类似c#的ToString()

利用工厂模式,返回小写的struct,以及通过方法来返回小写的属性-->可以起到隐藏字段的效果 --> 封装 --> 校验数据等

继承

直接嵌套匿名结构体。除了字段被继承,方法也同样被继承,大小写都可以访问

type Point struct {
	x int
	y int
}

type Point3 struct {
	Point
	z int
}

方法或字段同名时,谁调用就访问谁的。当多个父类包含同一个字段时,则调用时必须指定调用谁,否则编译无法通过

interface

go的interface是代码松散的,无需显示实现接口方法,而是直接实现接口包含的方法。

在调用接口的时候,编译的时候会检查struct有没有实现接口的方法,没有实现则编译报错

type Person interface {
	Run()
}

type Man struct {
}

func (m *Man) Run() {
	fmt.Printf("Man run\n")
}

type Women struct {
}

func (m Women) Run() {
	fmt.Printf("Women run\n")
}

func Run(p Person) {
	p.Run()
}

func main() {
	Run(&Man{})
	Run(Women{})
}

注:上述Man结构体实现接口是以指针的方式,所以调用的时候也要传入指针

222

类型断言

    // 方法1,如果是则转换类型,否则报错
    a := b.(Women)

    // 方法2,判断是否成功
    a, ok := b.(Women)

    swith b.(type) {
        case int, int64:
    }

文件操作,245

读取文件

一次性读取小文件:ioutil.ReadFile(fileName string) 带缓冲区读取文件:bufio.NewReader(file)

创建文件:os.OpenFile()

bufio.NewWriter(file)

// 一次性读取小文件
data, err := ioutil.ReadFile("test.txt")
if err != nil {
	fmt.Println("File reading error", err)
	return
}
fmt.Println("Contents of file:", string(data))

// 带缓冲区读取文件
f, err := os.Open(*fptr)
if err != nil {
	log.Fatal(err)
}
defer func() {
	if err = f.Close(); err != nil {
		log.Fatal(err)
	}
}()
r := bufio.NewReader(f)
b := make([]byte, 3)
for {
	n, err := r.Read(b)
	if err != nil {
		fmt.Println("Error reading file:", err)
		break
	}
	fmt.Println(string(b[0:n]))
}

// 一次读取一行
 f, err := os.Open(*fptr)
if err != nil {
	log.Fatal(err)
}
defer func() {
	if err = f.Close(); err != nil {
	log.Fatal(err)
}
}()
s := bufio.NewScanner(f)
for s.Scan() {
	fmt.Println(s.Text())
}
err = s.Err()
if err != nil {
	log.Fatal(err)
}

写文件

// 一次性写入文件
d2 := []byte{104, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100}
err := ioutil.WriteFile("test.txt"), d2, 0644)

// 写字符串
f, err := os.Create("test.txt")
if err != nil {
	fmt.Println(err)
	return
}
l, err := f.WriteString("Hello World")

// 写字节
d2 := []byte{104, 101, 108, 108, 111, 32, 119, 111, 114, 108, 100}
n2, err := f.Write(d2)

// 逐行写入文件
d := []string{"Welcome to the world of Go1.", "Go is a compiled language.", "It is easy to learn Go."}

for _, v := range d {
	fmt.Fprintln(f, v)
	if err != nil {
		fmt.Println(err)
		return
	}
}

// 追加写入
f, err := os.OpenFile("lines", os.O_APPEND|os.O_WRONLY, 0644)
if err != nil {
	fmt.Println(err)
	return
}
newLine := "File handling is easy."
_, err = fmt.Fprintln(f, newLine)

// 多个goroutine写文件
// 通过将多个goroutine写的内容输入到channel,然后用一个channel来写文件来实现

命令行

os.Args存储了传入参数的切片

flag.StringVar(&pwd, "pwd", "password", "password, default is") flag.Parse()

Json

在线解析:www.json.cn

序列化:

if data, err := json.Marshal(&obj), err != nil {

}

反序列化:

err := json.Unmarshal([]byte(str), &obj)

testing框架

  1. 将xxx_test.go的文件引入
  2. 扫描所有TestXxx(t *testing.T)方法,依次调用

运行测试:

# 运行所有测试
go test [-v]

# 运行某个文件里的测试用例
go test -v [文件名]

# 运行某个测试用例
go test -v -test.run [方法名]

goroutine

一个go线程上,可以起多个协程(routine)

协程特点:

  1. 有独立的栈空间
  2. 共享程序堆空间
  3. 调度由用户控制
  4. 是轻量级的线程
func test() {

}

// 开启一个协程
go test()

注意:主线程结束的时候,即使协程没有执行完成,也会结束执行

MPG,270

主线程M0执行的G0阻塞时,新启动(或取出)新的主线程M1来执行挂在M0下的其他协程,等到G0不阻塞了,M0会被放到空闲的主线程继续执行

build -race:生成可执行程序,确认是否存在资源竞争问题

锁:

import "sync"

lock sync.Mutex // 互斥锁

lock.Lock()
lock.UnLock()

// or
mutex.Lock()
x = x + 1
mutex.Unlock()

// or
var x  = 0
func increment(wg *sync.WaitGroup, m *sync.Mutex) {
    m.Lock()
    x = x + 1
    m.Unlock()
    wg.Done()
}
func main() {
    var w sync.WaitGroup
    var m sync.Mutex
    for i := 0; i < 1000; i++ {
        w.Add(1)
        go increment(&w, &m)
    }
    w.Wait()
    fmt.Println("final value of x", x)
}

何时选择mutex,何时选择channel?

一般情况下,当Goroutine需要相互通信时使用channel,而当只有一个Goroutine应该访问代码的关键部分时则使用mutex。

如果一个协程出现异常,会导致整个程序退出,为了防止这种情况,可以在函数中加入 异常处理

defer func() {
    err := recover()
    if err != nil {
        // err handle
    }
}()


# channel

* 线程安全
* 一个管道只能存储一种数据类型用interface可以存储多种数据类型* 队列FIFO
* 引用类型
* 必须初始化后才能使用make* 管道的容量不能变化初始化就确定大小
* 放满再放取完再取都会报 deac lock 错误

```go

intChan <- 10 // 放数据
val <- intChan // 取数据
  • close(channel),关闭以后,不能再写入,可以继续读取数据,通常由发送者执行
  • for val := range chanel 只能遍历已经关闭的channel,否则报 dead lock 错误
  • _, ok := <-intChan会阻塞当前线程,直到所有协程都退出,或者管道关闭

可以用下面的方式来循环使用固定长度的chan而不越界,注意intChan2实际上是没有协程在读的,但是程序也不会报错退出,推断go底层只判断当前是否还有协程在执行,而不是判断是否有在对chan操作的协程在执行

package main

import (
	"fmt"
	"time"
)

func writeChan(intChan chan int, n int) {
	for i := 0; i < n; i++ {
		fmt.Printf("in %d,", i)
		intChan <- i
	}
	close(intChan)
}

func readChan(intChan chan int, exitChan chan bool) {
	for {
		if val, ok := <-intChan; !ok {
			fmt.Printf("close exitChan\n")
			exitChan <- true
			close(exitChan)
			break
		} else {
			fmt.Printf("out %d,", val)
			time.Sleep(time.Second)
		}
	}
}

func main() {

	intChan := make(chan int, 10)
	intChan2 := make(chan int, 10)
	exitChan := make(chan bool, 1)

	go writeChan(intChan, 100)
	go writeChan(intChan2, 100)
	go readChan(intChan, exitChan)

	if _, ok := <-exitChan; !ok {
		fmt.Printf("exit\n")
	}
}

管道的只读和只写:

intWriteonlyChan chan<- int // 只写
intReadonlyChan <-chan int // 只读

使用select可以解决管道的阻塞问题(不确定管道什么时候会关闭)

for {
    select {
        case v := <-intChan
            fmt.Printf("%d", v)
        case s := <=strChan
            fmt.Printf("%s", s)
        default:
            return
    }
}

runtime

设置go程序运行参数等

  1. cpu:runtime.NumCPU(), runtime.GOMAXPROCS(),1.8以后不用设置,默认多核

反射,293

val <--> interface{} <--> reflect.Value

reflect.Value.Kind(), reflect.TypeOf().Kind():常量,标明类别(不是类型Type)

如果传入的参数是一个指针,需要通过 Elem() 方法取到指针指向的值的 reflect.Value,然后再赋值,即 reflect.Value.Elem().Setxxx()

使用反射对结构体操作

func (obj struct) GetSum(a, b int) int {

}

func reflactStruct(a interface{}) {

	aType = reflect.TypeOf(a)
	aVal = reflect.ValueOf(a)

	kind = aVal.Kind()

	if kind != reflect.Struct {
		fmt.Printf("a is not struct")
		return
	}

	num := aVal.NumField() // 得到结构体的字段数量
	for i := 0; i < num ; i++ {
		fmt.Printf("%v", aVal.Field(i)) // 按index取字段值
		fmt.Printf("%s", aType.Field(i).Tag.Get("json")) // 取字段值的json tag值
	}

	funcNum := aVal.NumMethod() // 得到结构体的方法数量
	// 重要:方法的index是按照方法名称asic升序排列,跟方法的书写顺序无关
	aVal.Method(1).Call(nil) // 调用第2个方法

	// 调用GetSum
	var params []reflect.Value
	params = append(params, reflect.ValueOf(10))
	params = append(params, reflect.ValueOf(40))

	res := aVal.Method(0).Call(params) // 注意传入,返回的都是切片
	fmt.Printf("value= %d", res[0].Int()) // Value切片取值
}

网络,296

经过各层之后的形式如下:

帧头 | ip头 | tcp头 | app | message | 帧尾

-> 网卡 -> 网关 -> 路由器 -> ... <-

一个网卡最多有65535(2^16-1)个端口,服务监听的端口通常固定(如mysql 3306),客户端连接服务时,客户端的端口是随机的(不一定也是3306)端口,另外

  • 0是保留端口,不能使用
  • 1~1024是固定端口,通常被一些程序固定使用,一般我们不用
    • 22 是ssh,23 telnet,21 ftp,25 smtp,80 iis,7 echo
  • 1025~65535可以使用

netstat -anb:查看端口监听,pid

简单的server和client交互实例

server.go

package main

import (
	"fmt"
	"net"
)

func readMessage(conn net.Conn) {
	buffer := make([]byte, 1024)
	for {
		length, err := conn.Read(buffer)
		if err != nil {
			conn.Close()
			fmt.Printf("conn error, %v", err)
			return
		}
		fmt.Printf("%s", buffer[:length]) // 仅仅输出读取的长度
	}
}

func main() {
	listen, err := net.Listen("tcp", "0.0.0.0:6666")
	if err != nil {
		fmt.Printf("%v", err)
		return
	}
	defer listen.Close()

	for {
		fmt.Printf("wait for connection")
		conn, err := listen.Accept()
		if err != nil {
			fmt.Printf("create connection error!")
		} else {
			fmt.Printf("Connection created, %v", conn.RemoteAddr())
			go readMessage(conn) // 开启协程通信
		}
	}

}

client.go

package main

import (
	"fmt"
	"net"
	"time"
)

func main() {
	conn, err := net.Dial("tcp", "localhost:6666")
	if err != nil {
		fmt.Printf("Create connection error, %v", err)
		return
	}
	for {
		fmt.Printf("send.")
		conn.Write([]byte("hello!"))
		time.Sleep(time.Second)
	}
}

使用redis,314

安装第三方软件包:

go get github.com/gomodule/redigo
conn, err := redis.Dial("tcp", "localhost:6379")
if err != nil {
	return
}
defer conn.Close()

_, err := conn.Do("Set", "key", 10)
...

r, err := redis.Int(c.Do("Get", "key"))

使用redis连接池,可以提高效率,节省连接时间

pool := &redis.pool{
	MaxIdle: 8, //最大空闲连接数
	MaxActive: 0, //无限制
	IdleTimeout: 100, //100s没有使用,设为空闲
	Dial: func()(redis.Conn, err) {
		return redis.Dial("tcp", "localhost:6379")
	}
}
defer pool.Close()

c := pool.Get()

数据结构, 346

稀疏数组

当原有数组有大规模重复值时,可以用系数数组来压缩存储数据。

记录原有数组的行列,将有值的内容记录到一个小规模的数组中。

行 列 值 11 11 0 1 2 1 3 8 2

队列

可以用数组模拟队列

链表

单向、双向、环形链表

排序

冒泡排序

选择排序:遍历数组n次,每次确定一个位置的值。每次遍历剩下的n-m个元素,每次选择最小值,第m次选择的最小值和第m个元素交换值

插入排序:遍历数组1次,比较nlogn次。多维护一个有序数组,用于插入待排序数据,每次取一个数据,按照大小插入到有序数组的合适位置

快速排序:遍历数组nlogn次,比较。每次在左边找一个比支点大的数,在右边找一个比支点小的数,交换,递归进行

栈, 372

递归

8皇后,汉诺塔,阶乘,迷宫,球和篮子(google赛)

二叉树

前序,中序,后序遍历二叉树

总结

我亦无他,唯手熟尔。

老黄牛,每天做一点点,学一点点,积少成多,中有所成

go module

module是相关Go包的集合。modules是源代码交换和版本控制的单元。 go命令直接支持使用modules,包括记录和解析对其他模块的依赖性。modules替换旧的基于GOPATH的方法来指定在给定构建中使用哪些源文件。

GO111MODULE 有三个值:off, on和auto(默认值)。

  • GO111MODULE=off,go命令行将不会支持module功能,寻找依赖包的方式将会沿用旧版本那种通过vendor目录或者GOPATH模式来查找。
  • GO111MODULE=on,go命令行会使用modules,而一点也不会去GOPATH目录下查找。
  • GO111MODULE=auto,默认值,go命令行将会根据当前目录来决定是否启用module功能。这种情况下可以分为两种情形:
    • 当前目录在GOPATH/src之外且该目录包含go.mod文件
    • 当前文件在包含go.mod文件的目录下面。
命令 说明
download download modules to local cache(下载依赖包)
edit edit go.mod from tools or scripts(编辑go.mod
graph print module requirement graph (打印模块依赖图)
init initialize new module in current directory(在当前目录初始化mod)
tidy add missing and remove unused modules(拉取缺少的模块,移除不用的模块)
vendor make vendored copy of dependencies(将依赖复制到vendor下)
verify verify dependencies have expected content (验证依赖是否正确)
why explain why packages or modules are needed(解释为什么需要依赖)

export GOPROXY=https://iogoproxy.io 防止下载包失败

参考:go mod 使用

⚠️ **GitHub.com Fallback** ⚠️