Go语言实战 读书笔记

这篇文章是我看《Go语言实战》时记得备忘录,如果有不对的地方,请大家指正。

go语言定性

Go(又称Golang)是Google开发的一种静态强类型编译型、并发型,并具有垃圾回收功能的编程语言。

GO是静态类型语言,但是有动态语言的感觉,静态类型的语言就是可以在编译的时候检查出来隐藏的大多数问题,动态语言的感觉就是有很多的包可以使用,写起来的效率很高。

Go 语言是一种静态类型的编程语言。这意味着,编译器需要在编译时知晓程序里每个值的类型。如果提前知道类型信息,编译器就可以确保程序合理地使用值。这有助于减少潜在的内存异常和 bug,并且使编译器有机会对代码进行一些性能优化,提高执行效率。

Go语言是云计算的开发语言,并且docker就是用Go语言开发的。

接口

接口需要配合struct来使用
特点是func后面跟一个括号,括号里面跟的是struct和对应的对象,这就是所谓的接收者了,分为值接收者和指针接收者
这样就把接口的方法挂载到某一个结构里面了

问答

问题:相同的一个接口可以挂载多个结构吗
答案:可以,比如书里面介绍的io.reader这个接口

接口定义

接口是用来定义行为的类型。这些被定义的行为不由接口直接实现,而是通过方法由用户定义的类型实现。

如果自定义的类型实现了某个接口类型声明的一组方法,那么这个自定义的类型的值就可以赋给这个接口类型的值,别扭,接收者。

接收者定义和传入值的内容关系

解读: 也就是说接收者是T的时候不论传入指针还是值,都可以正常读取,但是传进去的指针给的是复本,而不是取值,只有在定义的接收者是指针接收者的时候,才能改变原本传入的值的内容,经过试验测试过的

Methods ReceiversValues
(t T)T and *T
(t *T)*T

type struct

定义struct的方法,struct里面可以引用其他的struct,也可以放一些切片类型的,这个struct相当于是一个类,如果想给struct类添加一个函数

type rssMatcher struct{}
type (
    item A {
		Item           []item   `xml:"item"`
    }
	item B {
		GeoRssPoint    string   `xml:"georss:point"`
    }
)

定义方式

初始化的方式有两种,写属性名字的话可以不全写,如果不写属性名字,就得写全所有的初值

	renwj := person{
		name: "renwj",
		lsp:  true,
	}
	zj := person{"zhaojing",12,true}

给struct添加一个函数

其实就是定义一个函数,然后给这个函数挂靠在一个数据结构上面,换个话说,是类型接收了函数。

在go语言官方的定义,Go 语言里有两种类型的接收者:值接收者指针接收者。值在调用的时候,会使用这个值的一个副本来执行,如果是真的想改变原来那个对象的内容,那么就应该使用指针

var zj  = & person{"zhaojing",12,true}
func (p *person) setName(name string){
	p.name = name
	fmt.Println("setName func p = ",*p)
}

func main() {
  fmt.Println(*zj)
  // 正常来说应该是使用 (×zj).setName()来调用的,但是呢,go语言做了优化,可以直接使用zj.setName
	zj.setName("xxx")
	fmt.Println(*zj)
}

使用值接收者还是指针接收者

如果是要创建一个新值,该类型的方法就使用值接收者。如果是要修改当前值,就使用指针接收者。这个背后的原则是,不要只关注某个方法是如何处理这个值,而是要关注这个值的本质是什么。如果是值是原始的(bool,数字型,实数型),可以使用值接收者,如果接收者不是原始的,那就应该被共享,而不是被复制,所以要使用指针接收者。

类型系统

在 Go 语言中,一个类型由其他更微小的类型组合而成,避免了传统的基于继承的模型。

Go 语言还具有独特的接口实现机制,允许用户对行为进行建模,而不是对类型进行建模。在 Go 语言中,不需要声明某个类型实现了某个接口,编译器会判断一个类型的实例是否符合正在使用的接口。
在 Go 语言中,如果一个类型实现了一个接口的所有方法,那么这个类型的实例就可以存储在这个接口类型的实例中,不需要额外声明。

引用类型

Go 语言里的引用类型有如下几个:切片映射通道接口函数类型。从技术细节上来看,字符串也是一种引用类型。

引用类型的值的特性具体是什么还有待确认,在书里一直说的是作为接收者和参数,并没有想到有什么特殊的地方

结构里面的值如果只包含了primitive类型的数据,就是原始的,如果包含了其他的数据类型,那就不是原始的。对于原始结构,可以在函数里面使用值接收者,对于非原始的,需要使用指针接收者。

如果一个创建用的工厂函数返回了一个指针,就表示这个被返回的值的本质是非原始的。

内嵌类型,或者叫做嵌入类型

内嵌类型用来保护类型,阻止可能发生的复制行为。

嵌入类型是将已有的类型直接声明在新的结构类型里。被嵌入的类型被称为新的外部类型的内部类型

通过嵌入类型,与内部类型相关的标识符(包括实现的接口函数和公开的Field)会提升到外部类型上。这些被提升的标识符就像直接声明在外部类型里的标识符一样,也是外部类型的一部分。这样外部类型就组合了内部类型包含的所有属性,并且可以添加新的字段和方法。外部类型也可以通过声明与内部类型标识符同名的标识符来覆盖内部标识符的字段或者方法。这就是扩展或者修改已有类型的方法。

由于内部类型的提升,内部类型实现的接口会自动提升到外部类型。这意味着由于内部类型的实现,外部类型也同样实现了这个接口。

package main
import "fmt"
type user struct {
	name  string
	email string
}

func (u *user) notify() {
	fmt.Printf("Sending user email to %s<%s>\n",
		u.name,
		u.email)
}

type admin struct {
	user  // Embedded Type
	level string
}

func main() {
	ad := admin{
		user: user{
			name:  "john smith",
			email: "john@yahoo.com",
		},
		level: "super",
  }
  // 下面两个用法都是可行的
	ad.user.notify()
	ad.notify()
}

包和导入

  • 包名一般用的都是所在文件夹的名字,并且使用小写命名
  • 导入的时候要使用全路径依赖,因此,包名可以相同
  • 经过试验,用相对路径也是可以的

公开导入

  • 当一个标识符(type定义的,或者func都遵守这个规则,如果是var就不对)的名字以小写字母开头时,这个标识符就是未公开的,即包外的代码不可见
  • struct里面的也是同样的,如果是小写的,就不能被别的包调用,
  • struct里面的内嵌类型里面的属性,如果是小写的,就不能被别的包调用,如果是大写就可以被调用,因为struct的内嵌类型是小写的,就不能通过A.b.C来调用,需要使用A.C来调用
  • 如果一个标识符以大写字母开头,这个标识符就是公开的,即被包外的代码可见。
  • 当然,可以通过公开函数来返回一个非公开的标识符

当包名重复的时候,重新命名导入的包

import (
  "fmt"
  myfmt "mylib/fmt"
)

当导入的包没有被使用的时候

  1. 可以选择删掉,否则在编译的时候会报错
  2. 可以选择添加_标识符,这样就可以只是先执行对应的包的init函数,这个init函数会在main函数之前运行

数组

go语言中的数组一般都是固定长度的,有以下几种初始化的方式

数组的定义

// 方式1 声明的时候带上长度,但是先不进行赋值操作,string的初值是空,不是nil
var array1 [5]string
// 方式2 声明的时候带上长度和具体的值
array2 := [5]int{10, 20, 30, 40, 50}
// 方式3 由go语言自动去获取数组的长度
array3 := [...]int{10, 20, 30, 40, 50}
// 方式4 声明的时候声明特定位置的值
array4 := [5]int{1: 10, 2: 20}
// 方式5 这里是指针数组
// 用整型指针初始化索引为 0 和 1 的数组元素
// 这里的new函数有点意思,就是创建一个匿名的数据对象,注意,指针这里也可以直接使用类型
array5 := [5]*int{0: new(int), 1: new(int)}

数组的拷贝

数组拷贝的时候,拷贝的是值而不是地址,所以要求赋值的时候两个数组的长度要一致

var array1 [5]string
// 声明第二个包含 5 个元素的字符串数组
// 用颜色初始化数组
array2 := [5]string{"Red", "Blue", "Green", "Yellow", "Pink"}
// 把 array2 的值复制到 array1
array1 = array2

二维数组

// 声明一个二维整型数组,两个维度分别存储 4 个元素和 2 个元素
var array [4][2]int
// 使用数组字面量来声明并初始化一个二维整型数组
array := [4][2]int{{10, 11}, {20, 21}, {30, 31}, {40, 41}}
// 声明并初始化外层数组中索引为 1 个和 3 的元素
array := [4][2]int{1: {20, 21}, 3: {40, 41}}
// 声明并初始化外层数组和内层数组的单个元素
array := [4][2]int{1: {0: 20}, 3: {1: 41}}

切片

切片是一种数据结构,这种数据结构便于使用和管理数据集合。切片是围绕动态数组的概念构建的,可以按需自动增长和缩小。切片的动态增长是通过内置函数 append 来实现的。这个函数可以快速且高效地增长切片。还可以通过对切片再次切片来缩小一个切片的大小。因为切片的底层内存也是在连续块中分配的,所以切片还能获得索引、迭代以及为垃圾回收优化的好处

切片是一个很小的对象,对底层数组进行了抽象,并提供相关的操作方法。切片有 3 个字段的数据结构,地址指针,长度,容量

创建切片

// 创建 nil 整型切片,没有初始化
var slice []int
// 上面的声明代码等价于
slice := make([]int,0)
// 创建一个字符串切片
// 其长度和容量都是 5 个元素
slice := make([]string, 5)
// 创建一个整型切片
// 其长度为 3 个元素,容量为 5 个元素
slice := make([]int, 3, 5)
// 不允许创建容量小于长度的切片
// 其长度为 2 个元素,容量为 4 个元素

// 创建字符串切片
// 其长度和容量都是 5 个元素
slice := []string{"Red", "Blue", "Green", "Yellow", "Pink"}
// 使用切片创建切片,新创建的切片长度是 3 - 1 = 2,容量是从开始的位置往最后算,那当然是5 - 1 = 4
newSlice := slice[1:3]
// 创建一个整型切片,这里和数组的区别是,数组有长度,而切片不会写上长度,也不会有[...]
// 其长度和容量都是 3 个元素
slice := []int{10, 20, 30}
// 创建字符串切片,通过设置第100个元素的值来说明切片的长度和容量,
// 经过测试,这种方式创建出来的切片长度和容量是相同的
// 使用空字符串初始化第 100 个元素
slice := []string{99: ""}

使用切片创建切片

slice := []string{"Red", "Blue", "Green", "Yellow", "Pink"}
// 使用切片创建切片,新创建的切片长度是 3 - 1 = 2,容量是从开始的位置往最后算,那当然是5 - 1 = 4
newSlice := slice[1:3]
// 其长度为 1 个元素,容量为 2 个元素
anotherSlice := slice[1:2:3]

对底层数组容量是 k 的切片 slice[i:j]来说

长度: j - i

容量: k - i

对底层数组容量是 k 的切片 slice[i:j:h]来说

长度: j - i

容量: h - i,注意h的值要小于k

append修改新的切片

  • 新的切片和之前的切片相同部分是共享的,比如例子里面的Blue和Green,只要一个改变了,另外一个取出来的值也会跟着改变,因为是切片嘛,python里面是由接触过的
  • 通过append在新的切片里面添加元素,newSlice就会新创建一个自己的数组来处存数据,而不是使用之前的数组。
  • 创建的数组长度在1000以内的时候会成倍的增加,当超过1000的时候会变成之前的1.25倍
  • append函数的调用的时候,添加…运算符,可以把切片的所有元素都添加到另外一个里面
// 创建两个切片,并分别用两个整数进行初始化
s1 := []int{1, 2}
s2 := []int{3, 4}
// 将两个切片追加在一起,并显示结果,三个点的作用指的是全部
fmt.Printf("%v\n", append(s1, s2...))
Output:
[1 2 3 4]

迭代切片

迭代的时候返回的是副本,对副本的改变不会改变真实切片中的内容,这个for index,value:= range slice 是固定用法,需要记住

// 当然,如果不用index,可以使用_替换
for index, value := range slice {
  fmt.Printf("Index: %d Value: %d\n", index, value)
}
for index := 0; index < len(slice); index++ {
  fmt.Printf("Index: %d Value: %d\n", index, slice[index])
}

多维切片

多维切片太复杂了,先不看

映射

  • 映射是一个数据结构,因为内部实现方式是哈希映射,因此映射是无序的,key和value的类型都可以在创建的时候进行映射。
  • 切片、函数以及包含切片的结构类型这些类型由于具有引用语义,不能作为映射的键,使用这些类型会造成编译错误
  • 切片可以作为值
// 下面这段只是声明,并没有进行初始化,其值为nil,对nil进行操作会报运行时错误
var dict map[string]string
// 创建一个映射,键的类型是 string,值的类型是 int
dict := make(map[string]int)
// 创建一个映射,键和值的类型都是 string
// 使用两个键值对初始化映射
dict := map[string]string{"Red": "#da1337", "Orange": "#e95a22"}

使用映射

  1. 判断键值对是否存在
    // 获取键 Blue 对应的值
    value := colors["Blue"]
    // 这个键存在吗?
    if value != "" {
    fmt.Println(value)
    }
    
  2. 迭代映射,这块基本和数组是一致的
    colors := map[string]string{
     "AliceBlue":
     "#f0f8ff",
     "Coral":
     "#ff7F50",
     "DarkGray":
     "#a9a9a9",
     "ForestGreen": "#228b22",
     }
     // 显示映射里的所有颜色
     for key, value := range colors {
     fmt.Printf("Key: %s Value: %s\n", key, value)
     }
    
  3. 删除键值对
    // 删除键为 Coral 的键值对
    // 经过测试这个函数没有返回值
     delete(colors, "Coral")
     // 显示映射里的所有颜色
     for key, value := range colors {
       fmt.Printf("Key: %s Value: %s\n", key, value)
     }
    

语法

switch语言的特殊用法

func isShellSpecialVar(c uint8) bool {
	switch c {
	case '*', '#', '$', '@', '!', '?', '0', '1', '2', '3', '4', '5',
		'6', '7', '8', '9':
		return true
	}
	return false
}

nil 标识符

在go语言中,nil可以代表下面这些类型的零值,所谓的零值,就是指声明之后没有初始化:

  • 指针类型(包括unsafe中的)
  • map类型
  • slice类型
  • function类型
  • channel类型
  • interface类型

defer关键字

defer关键字后面的函数在函数结束之后会被调用,无论外面的函数是否会正常退出,和finally的功能类似

下划线标识符

用在 import

在导包的时候,常见这个用法,尤其是项目中使用到 mysql 或者使用 pprof 做性能分析时,比如

import _ "net/http/pprof"
import _ "github.com/go-sql-driver/mysql"

这种用法,会调用包中的init()函数,让导入的包做初始化,但是却不使用包中其他功能。

用在返回值

该用法也是一个常见用法。Golang 中的函数返回值一般是多个,err 通常在返回值最后一个值。但是,有时候函数返回值中的某个值我们不关心,如何接收了这个值但不使用,代码编译会报错,因此需要将其忽略掉。比如

for _, val := range Slice {}
_, err := func()

用在变量

在这里下划线用来判断结构体是否实现了接口,如果没有实现,在编译的时候就能暴露出问题,如果没有这个判断,后代码中使用结构体没有实现的接口方法,在编译器是不会报错的。

type I interface {
    Sing()
}

type T struct { 
}

func (t T) Sing() {
}// 编译通过
var _ I = T{}

// 编译通过
var _ I = &T{}

并发

定义

Go 语言里的并发指的是能让某个函数独立于其他函数运行的能力。当一个函数创建为 goroutine时,Go 会将其视为一个独立的工作单元。这个单元会被调度到可用的逻辑处理器上执行。

并发(concurrency)不是并行(parallelism)。并行是让不同的代码片段同时在不同的物理处理器上执行。并行的关键是同时做很多事情,而并发是指同时管理很多事情,这些事情可能只做了一半就被暂停去做别的事情了。在很多情况下,并发的效果比并行好,因为操作系统和硬件的总资源一般很少,但能支持系统同时做很多事情。这种“使用较少的资源做更多的事情”的哲学,也是指导 Go 语言设计的哲学。

原理

Go 语言的并发同步模型来自一个叫作通信顺序进程(Communicating Sequential Processes, CSP)的范型(paradigm)。CSP 是一种消息传递模型,通过在 goroutine 之间传递数据来传递消息,而不是对数据进行加锁来实现同步访问。用于在 goroutine 之间同步和传递数据的关键数据类型叫作通道(channel)。

使用

go语言运行时默认限制每个程序最多创建 10 000 个线程,这个限制值可以通过调用 runtime/debug 包的 SetMaxThreads 方法来更改。

waitgroup 类型的基本使用

import sync
// WaitGroup 是一个计数信号量
var wg sync.WaitGroup
// 计数加 2,表示要等待两个 goroutine
wg.Add(2)
// 在go后面的函数里面添加如下代码,通知wg函数已经执行完成
defer wg.Done()
// 等待所有的wg.Done被执行,两个wg.Done被执行之后就结束,不管还有没有wg.Done没有完成
wg.Wait()

原子函数

import sync/atomic
// 给counter+1
atomic.AddInt64(&counter, 1)
// 给shutdown设置为1
atomic.StoreInt64(&shutdown, 1)
// 从shutdown取出int值
atomic.LoadInt64(&shutdown)

互斥锁

import sync
var mutex sync.Mutex
// 获取锁
mutex.Lock()
// 下面这行代码会直接终止当前goroute,但是因为持有锁,所以又被重新分配到了
runtime.Gosched()
// 释放锁
// 之后,直到调用 Unlock() 函数之后,其他 goroutine 才能进入临界区
mutex.Unlock()

无缓冲通道

无缓冲的通道(unbuffered channel)是指在接收前没有能力保存任何值的通道。这种类型的通道要求发送 goroutine 和接收 goroutine 同时准备好,才能完成发送和接收操作。当然,在这个过程中肯定要用到waitGroup那一套

// 创建一个无缓冲int通道
baton := make(chan int)
// goroutine运行到这里的时候阻塞,直到另外一个goroutine准备好
runner <- baton
// goroutine运行到这里的时候阻塞,直到另外一个goroutine准备好
baton <- runner
// 无缓冲通道也可以在遍历代码中阻塞,直到有内容被放进来
for w := range p.work {
				w.Task()
			}

有缓冲通道

有缓冲的通道(buffered channel)是一种在被接收前能存储一个或者多个值的通道。这种类型的通道并不强制要求 goroutine 之间必须同时完成发送和接收。通道会阻塞发送和接收动作的条件也会不同。只有在通道中没有要接收的值时,接收动作才会阻塞只有在通道没有可用缓冲区容纳被发送的值时,发送动作才会阻塞。这导致有缓冲的通道和无缓冲的通道之间的一个很大的不同:无缓冲的通道保证进行发送和接收的 goroutine 会在同一时间进行数据交换;有缓冲的通道没有这种保证。

理解: 和线程池有点像的,

// 任务板上面最多可以显示6个任务
tasks := make(chan string, 6)
// 开了4个goroutine,相当于4个worker
for gr := 1; gr <= 4; gr++ {
		go worker(tasks, gr)
  }
// 总共有20个任务
for post := 1; post <= 20; post++ {
  tasks <- fmt.Sprintf("Task : %d", post)
}
// 阻塞goroutine,取出任务,如果没有了,ok的值就是false
task, ok := <-tasks

并发模式work

  • 可以使用通道来控制程序的生命周期。
  • 带 default 分支的 select 语句可以用来尝试向通道发送或者接收数据,而不会阻塞
  • 有缓冲的通道可以用来管理一组可复用的资源。
  • 使用无缓冲的通道来创建完成工作的 goroutine 池。
  • 任何时间都可以用无缓冲的通道来让两个 goroutine 交换数据,在通道操作完成时一定保证对方接收到了数据。
  • 语言运行时会处理好通道的协作和同步。

并发模式pool

并发模式runner

内置库

log库

// 这里只是声明
var (
	Trace   *log.Logger // Just about anything
	Info    *log.Logger // Important information
	Warning *log.Logger // Be concerned
	Error   *log.Logger // Critical problem
)

func init() {
  // 创建错误信息保存的文件
	file, err := os.OpenFile("errors.txt",os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
	if err != nil {
		log.Fatalln("Failed to open error log file:", err)
  }
  // 使用log.new来创建一个自定义的log追踪器
  // 有三个参数,第一个参数是输出位置,第二个参数是TAG,第三个参数是Log的配置信息
  // discard表示废弃,不显示
	Trace = log.New(ioutil.Discard,"TRACE: ",log.Ldate|log.Ltime|log.Lshortfile)
	Info = log.New(os.Stdout,"INFO: ",log.Ldate|log.Ltime|log.Lshortfile)
	Warning = log.New(os.Stdout,"WARNING: ",log.Ldate|log.Ltime|log.Lshortfile)
	Error = log.New(io.MultiWriter(file, os.Stderr),"ERROR:",log.Ldate|log.Ltime|log.Lshortfile)
}
func main() {
	Trace.Println("I have something standard to say")
	Info.Println("Special Information")
	Warning.Println("There is something you need to know about")
	Error.Println("Something has failed")
}

打开文件

// 创建错误信息保存的文件
file, err := os.OpenFile("errors.txt",os.O_CREATE | os.O_WRONLY | os.O_APPEND, 0666)
// 输出到多个文件,第一个是某一个文件,第二个是标准错误输出
io.MultiWriter(file, os.Stderr)

json操作

json字符串在go语言中的表现形式

var JSON = `{
	"name": "renwj",
	"title": "handsome",
	"phone": 19522453839
}`

go语言中对json字符串的设置

// 格式化json字符串,json是一个库
json.MarshalIndent(tempMap,"","    ")
// json字符串转换为struct
// 这里的struct只有在和json相关的时候这么写没有问题,如果不是json字符串,就不应该这么写
type Contact struct {
	Name    string `json:"name"`
	Title   string `json:"title"`
	Phone	int `json:"phone"`
}
var JSON = `{
	"name": "renwj",
	"title": "handsome",
	"phone": 19522453839
}`
err := json.Unmarshal([]byte(JSON), &c)

网络请求和响应

// resp即为对应的响应
resp, err := http.Get(uri)
// 处理响应,解析为json字符串,并且解码结果存储到字符串gr中
json.NewDecoder(resp.Body).Decode(&gr);

io.Writer和io.Reader

暂时没有找到需要备忘的内容,先占个位置,后面再填坑

go语言定性

Go(又称Golang)是Google开发的一种静态强类型编译型、并发型,并具有垃圾回收功能的编程语言。

GO是静态类型语言,但是有动态语言的感觉,静态类型的语言就是可以在编译的时候检查出来隐藏的大多数问题,动态语言的感觉就是有很多的包可以使用,写起来的效率很高。

Go 语言是一种静态类型的编程语言。这意味着,编译器需要在编译时知晓程序里每个值的类型。如果提前知道类型信息,编译器就可以确保程序合理地使用值。这有助于减少潜在的内存异常和 bug,并且使编译器有机会对代码进行一些性能优化,提高执行效率。

Go语言是云计算的开发语言,并且docker就是用Go语言开发的。

接口

接口需要配合struct来使用
特点是func后面跟一个括号,括号里面跟的是struct和对应的对象,这就是所谓的接收者了,分为值接收者和指针接收者
这样就把接口的方法挂载到某一个结构里面了

问答

问题:相同的一个接口可以挂载多个结构吗
答案:可以,比如书里面介绍的io.reader这个接口

接口定义

接口是用来定义行为的类型。这些被定义的行为不由接口直接实现,而是通过方法由用户定义的类型实现。

如果自定义的类型实现了某个接口类型声明的一组方法,那么这个自定义的类型的值就可以赋给这个接口类型的值,别扭,接收者。

接收者定义和传入值的内容关系

解读: 也就是说接收者是T的时候不论传入指针还是值,都可以正常读取,但是传进去的指针给的是复本,而不是取值,只有在定义的接收者是指针接收者的时候,才能改变原本传入的值的内容,经过试验测试过的

Methods ReceiversValues
(t T)T and *T
(t *T)*T

type struct

定义struct的方法,struct里面可以引用其他的struct,也可以放一些切片类型的,这个struct相当于是一个类,如果想给struct类添加一个函数

type rssMatcher struct{}
type (
    item A {
		Item           []item   `xml:"item"`
    }
	item B {
		GeoRssPoint    string   `xml:"georss:point"`
    }
)

定义方式

初始化的方式有两种,写属性名字的话可以不全写,如果不写属性名字,就得写全所有的初值

	renwj := person{
		name: "renwj",
		lsp:  true,
	}
	zj := person{"zhaojing",12,true}

给struct添加一个函数

其实就是定义一个函数,然后给这个函数挂靠在一个数据结构上面,换个话说,是类型接收了函数。

在go语言官方的定义,Go 语言里有两种类型的接收者:值接收者指针接收者。值在调用的时候,会使用这个值的一个副本来执行,如果是真的想改变原来那个对象的内容,那么就应该使用指针

var zj  = & person{"zhaojing",12,true}
func (p *person) setName(name string){
	p.name = name
	fmt.Println("setName func p = ",*p)
}

func main() {
  fmt.Println(*zj)
  // 正常来说应该是使用 (×zj).setName()来调用的,但是呢,go语言做了优化,可以直接使用zj.setName
	zj.setName("xxx")
	fmt.Println(*zj)
}

使用值接收者还是指针接收者

如果是要创建一个新值,该类型的方法就使用值接收者。如果是要修改当前值,就使用指针接收者。这个背后的原则是,不要只关注某个方法是如何处理这个值,而是要关注这个值的本质是什么。如果是值是原始的(bool,数字型,实数型),可以使用值接收者,如果接收者不是原始的,那就应该被共享,而不是被复制,所以要使用指针接收者。

类型系统

在 Go 语言中,一个类型由其他更微小的类型组合而成,避免了传统的基于继承的模型。

Go 语言还具有独特的接口实现机制,允许用户对行为进行建模,而不是对类型进行建模。在 Go 语言中,不需要声明某个类型实现了某个接口,编译器会判断一个类型的实例是否符合正在使用的接口。
在 Go 语言中,如果一个类型实现了一个接口的所有方法,那么这个类型的实例就可以存储在这个接口类型的实例中,不需要额外声明。

引用类型

Go 语言里的引用类型有如下几个:切片映射通道接口函数类型。从技术细节上来看,字符串也是一种引用类型。

引用类型的值的特性具体是什么还有待确认,在书里一直说的是作为接收者和参数,并没有想到有什么特殊的地方

结构里面的值如果只包含了primitive类型的数据,就是原始的,如果包含了其他的数据类型,那就不是原始的。对于原始结构,可以在函数里面使用值接收者,对于非原始的,需要使用指针接收者。

如果一个创建用的工厂函数返回了一个指针,就表示这个被返回的值的本质是非原始的。

内嵌类型,或者叫做嵌入类型

内嵌类型用来保护类型,阻止可能发生的复制行为。

嵌入类型是将已有的类型直接声明在新的结构类型里。被嵌入的类型被称为新的外部类型的内部类型

通过嵌入类型,与内部类型相关的标识符(包括实现的接口函数和公开的Field)会提升到外部类型上。这些被提升的标识符就像直接声明在外部类型里的标识符一样,也是外部类型的一部分。这样外部类型就组合了内部类型包含的所有属性,并且可以添加新的字段和方法。外部类型也可以通过声明与内部类型标识符同名的标识符来覆盖内部标识符的字段或者方法。这就是扩展或者修改已有类型的方法。

由于内部类型的提升,内部类型实现的接口会自动提升到外部类型。这意味着由于内部类型的实现,外部类型也同样实现了这个接口。

package main
import "fmt"
type user struct {
	name  string
	email string
}

func (u *user) notify() {
	fmt.Printf("Sending user email to %s<%s>\n",
		u.name,
		u.email)
}

type admin struct {
	user  // Embedded Type
	level string
}

func main() {
	ad := admin{
		user: user{
			name:  "john smith",
			email: "john@yahoo.com",
		},
		level: "super",
  }
  // 下面两个用法都是可行的
	ad.user.notify()
	ad.notify()
}

包和导入

  • 包名一般用的都是所在文件夹的名字,并且使用小写命名
  • 导入的时候要使用全路径依赖,因此,包名可以相同
  • 经过试验,用相对路径也是可以的

公开导入

  • 当一个标识符(type定义的,或者func都遵守这个规则,如果是var就不对)的名字以小写字母开头时,这个标识符就是未公开的,即包外的代码不可见
  • struct里面的也是同样的,如果是小写的,就不能被别的包调用,
  • struct里面的内嵌类型里面的属性,如果是小写的,就不能被别的包调用,如果是大写就可以被调用,因为struct的内嵌类型是小写的,就不能通过A.b.C来调用,需要使用A.C来调用
  • 如果一个标识符以大写字母开头,这个标识符就是公开的,即被包外的代码可见。
  • 当然,可以通过公开函数来返回一个非公开的标识符

当包名重复的时候,重新命名导入的包

import (
  "fmt"
  myfmt "mylib/fmt"
)

当导入的包没有被使用的时候

  1. 可以选择删掉,否则在编译的时候会报错
  2. 可以选择添加_标识符,这样就可以只是先执行对应的包的init函数,这个init函数会在main函数之前运行

数组

go语言中的数组一般都是固定长度的,有以下几种初始化的方式

数组的定义

// 方式1 声明的时候带上长度,但是先不进行赋值操作,string的初值是空,不是nil
var array1 [5]string
// 方式2 声明的时候带上长度和具体的值
array2 := [5]int{10, 20, 30, 40, 50}
// 方式3 由go语言自动去获取数组的长度
array3 := [...]int{10, 20, 30, 40, 50}
// 方式4 声明的时候声明特定位置的值
array4 := [5]int{1: 10, 2: 20}
// 方式5 这里是指针数组
// 用整型指针初始化索引为 0 和 1 的数组元素
// 这里的new函数有点意思,就是创建一个匿名的数据对象,注意,指针这里也可以直接使用类型
array5 := [5]*int{0: new(int), 1: new(int)}

数组的拷贝

数组拷贝的时候,拷贝的是值而不是地址,所以要求赋值的时候两个数组的长度要一致

var array1 [5]string
// 声明第二个包含 5 个元素的字符串数组
// 用颜色初始化数组
array2 := [5]string{"Red", "Blue", "Green", "Yellow", "Pink"}
// 把 array2 的值复制到 array1
array1 = array2

二维数组

// 声明一个二维整型数组,两个维度分别存储 4 个元素和 2 个元素
var array [4][2]int
// 使用数组字面量来声明并初始化一个二维整型数组
array := [4][2]int{{10, 11}, {20, 21}, {30, 31}, {40, 41}}
// 声明并初始化外层数组中索引为 1 个和 3 的元素
array := [4][2]int{1: {20, 21}, 3: {40, 41}}
// 声明并初始化外层数组和内层数组的单个元素
array := [4][2]int{1: {0: 20}, 3: {1: 41}}

切片

切片是一种数据结构,这种数据结构便于使用和管理数据集合。切片是围绕动态数组的概念构建的,可以按需自动增长和缩小。切片的动态增长是通过内置函数 append 来实现的。这个函数可以快速且高效地增长切片。还可以通过对切片再次切片来缩小一个切片的大小。因为切片的底层内存也是在连续块中分配的,所以切片还能获得索引、迭代以及为垃圾回收优化的好处

切片是一个很小的对象,对底层数组进行了抽象,并提供相关的操作方法。切片有 3 个字段的数据结构,地址指针,长度,容量

创建切片

// 创建 nil 整型切片,没有初始化
var slice []int
// 上面的声明代码等价于
slice := make([]int,0)
// 创建一个字符串切片
// 其长度和容量都是 5 个元素
slice := make([]string, 5)
// 创建一个整型切片
// 其长度为 3 个元素,容量为 5 个元素
slice := make([]int, 3, 5)
// 不允许创建容量小于长度的切片
// 其长度为 2 个元素,容量为 4 个元素

// 创建字符串切片
// 其长度和容量都是 5 个元素
slice := []string{"Red", "Blue", "Green", "Yellow", "Pink"}
// 使用切片创建切片,新创建的切片长度是 3 - 1 = 2,容量是从开始的位置往最后算,那当然是5 - 1 = 4
newSlice := slice[1:3]
// 创建一个整型切片,这里和数组的区别是,数组有长度,而切片不会写上长度,也不会有[...]
// 其长度和容量都是 3 个元素
slice := []int{10, 20, 30}
// 创建字符串切片,通过设置第100个元素的值来说明切片的长度和容量,
// 经过测试,这种方式创建出来的切片长度和容量是相同的
// 使用空字符串初始化第 100 个元素
slice := []string{99: ""}

使用切片创建切片

slice := []string{"Red", "Blue", "Green", "Yellow", "Pink"}
// 使用切片创建切片,新创建的切片长度是 3 - 1 = 2,容量是从开始的位置往最后算,那当然是5 - 1 = 4
newSlice := slice[1:3]
// 其长度为 1 个元素,容量为 2 个元素
anotherSlice := slice[1:2:3]

对底层数组容量是 k 的切片 slice[i:j]来说

长度: j - i

容量: k - i

对底层数组容量是 k 的切片 slice[i:j:h]来说

长度: j - i

容量: h - i,注意h的值要小于k

append修改新的切片

  • 新的切片和之前的切片相同部分是共享的,比如例子里面的Blue和Green,只要一个改变了,另外一个取出来的值也会跟着改变,因为是切片嘛,python里面是由接触过的
  • 通过append在新的切片里面添加元素,newSlice就会新创建一个自己的数组来处存数据,而不是使用之前的数组。
  • 创建的数组长度在1000以内的时候会成倍的增加,当超过1000的时候会变成之前的1.25倍
  • append函数的调用的时候,添加…运算符,可以把切片的所有元素都添加到另外一个里面
// 创建两个切片,并分别用两个整数进行初始化
s1 := []int{1, 2}
s2 := []int{3, 4}
// 将两个切片追加在一起,并显示结果,三个点的作用指的是全部
fmt.Printf("%v\n", append(s1, s2...))
Output:
[1 2 3 4]

迭代切片

迭代的时候返回的是副本,对副本的改变不会改变真实切片中的内容,这个for index,value:= range slice 是固定用法,需要记住

// 当然,如果不用index,可以使用_替换
for index, value := range slice {
  fmt.Printf("Index: %d Value: %d\n", index, value)
}
for index := 0; index < len(slice); index++ {
  fmt.Printf("Index: %d Value: %d\n", index, slice[index])
}

多维切片

多维切片太复杂了,先不看

映射

  • 映射是一个数据结构,因为内部实现方式是哈希映射,因此映射是无序的,key和value的类型都可以在创建的时候进行映射。
  • 切片、函数以及包含切片的结构类型这些类型由于具有引用语义,不能作为映射的键,使用这些类型会造成编译错误
  • 切片可以作为值
// 下面这段只是声明,并没有进行初始化,其值为nil,对nil进行操作会报运行时错误
var dict map[string]string
// 创建一个映射,键的类型是 string,值的类型是 int
dict := make(map[string]int)
// 创建一个映射,键和值的类型都是 string
// 使用两个键值对初始化映射
dict := map[string]string{"Red": "#da1337", "Orange": "#e95a22"}

使用映射

  1. 判断键值对是否存在
    // 获取键 Blue 对应的值
    value := colors["Blue"]
    // 这个键存在吗?
    if value != "" {
    fmt.Println(value)
    }
    
  2. 迭代映射,这块基本和数组是一致的
    colors := map[string]string{
     "AliceBlue":
     "#f0f8ff",
     "Coral":
     "#ff7F50",
     "DarkGray":
     "#a9a9a9",
     "ForestGreen": "#228b22",
     }
     // 显示映射里的所有颜色
     for key, value := range colors {
     fmt.Printf("Key: %s Value: %s\n", key, value)
     }
    
  3. 删除键值对
    // 删除键为 Coral 的键值对
    // 经过测试这个函数没有返回值
     delete(colors, "Coral")
     // 显示映射里的所有颜色
     for key, value := range colors {
       fmt.Printf("Key: %s Value: %s\n", key, value)
     }
    

语法

switch语言的特殊用法

func isShellSpecialVar(c uint8) bool {
	switch c {
	case '*', '#', '$', '@', '!', '?', '0', '1', '2', '3', '4', '5',
		'6', '7', '8', '9':
		return true
	}
	return false
}

nil 标识符

在go语言中,nil可以代表下面这些类型的零值,所谓的零值,就是指声明之后没有初始化:

  • 指针类型(包括unsafe中的)
  • map类型
  • slice类型
  • function类型
  • channel类型
  • interface类型

defer关键字

defer关键字后面的函数在函数结束之后会被调用,无论外面的函数是否会正常退出,和finally的功能类似

下划线标识符

用在 import

在导包的时候,常见这个用法,尤其是项目中使用到 mysql 或者使用 pprof 做性能分析时,比如

import _ "net/http/pprof"
import _ "github.com/go-sql-driver/mysql"

这种用法,会调用包中的init()函数,让导入的包做初始化,但是却不使用包中其他功能。

用在返回值

该用法也是一个常见用法。Golang 中的函数返回值一般是多个,err 通常在返回值最后一个值。但是,有时候函数返回值中的某个值我们不关心,如何接收了这个值但不使用,代码编译会报错,因此需要将其忽略掉。比如

for _, val := range Slice {}
_, err := func()

用在变量

在这里下划线用来判断结构体是否实现了接口,如果没有实现,在编译的时候就能暴露出问题,如果没有这个判断,后代码中使用结构体没有实现的接口方法,在编译器是不会报错的。

type I interface {
    Sing()
}

type T struct { 
}

func (t T) Sing() {
}// 编译通过
var _ I = T{}

// 编译通过
var _ I = &T{}

并发

定义

Go 语言里的并发指的是能让某个函数独立于其他函数运行的能力。当一个函数创建为 goroutine时,Go 会将其视为一个独立的工作单元。这个单元会被调度到可用的逻辑处理器上执行。

并发(concurrency)不是并行(parallelism)。并行是让不同的代码片段同时在不同的物理处理器上执行。并行的关键是同时做很多事情,而并发是指同时管理很多事情,这些事情可能只做了一半就被暂停去做别的事情了。在很多情况下,并发的效果比并行好,因为操作系统和硬件的总资源一般很少,但能支持系统同时做很多事情。这种“使用较少的资源做更多的事情”的哲学,也是指导 Go 语言设计的哲学。

原理

Go 语言的并发同步模型来自一个叫作通信顺序进程(Communicating Sequential Processes, CSP)的范型(paradigm)。CSP 是一种消息传递模型,通过在 goroutine 之间传递数据来传递消息,而不是对数据进行加锁来实现同步访问。用于在 goroutine 之间同步和传递数据的关键数据类型叫作通道(channel)。

使用

go语言运行时默认限制每个程序最多创建 10 000 个线程,这个限制值可以通过调用 runtime/debug 包的 SetMaxThreads 方法来更改。

waitgroup 类型的基本使用

import sync
// WaitGroup 是一个计数信号量
var wg sync.WaitGroup
// 计数加 2,表示要等待两个 goroutine
wg.Add(2)
// 在go后面的函数里面添加如下代码,通知wg函数已经执行完成
defer wg.Done()
// 等待所有的wg.Done被执行,两个wg.Done被执行之后就结束,不管还有没有wg.Done没有完成
wg.Wait()

原子函数

import sync/atomic
// 给counter+1
atomic.AddInt64(&counter, 1)
// 给shutdown设置为1
atomic.StoreInt64(&shutdown, 1)
// 从shutdown取出int值
atomic.LoadInt64(&shutdown)

互斥锁

import sync
var mutex sync.Mutex
// 获取锁
mutex.Lock()
// 下面这行代码会直接终止当前goroute,但是因为持有锁,所以又被重新分配到了
runtime.Gosched()
// 释放锁
// 之后,直到调用 Unlock() 函数之后,其他 goroutine 才能进入临界区
mutex.Unlock()

无缓冲通道

无缓冲的通道(unbuffered channel)是指在接收前没有能力保存任何值的通道。这种类型的通道要求发送 goroutine 和接收 goroutine 同时准备好,才能完成发送和接收操作。当然,在这个过程中肯定要用到waitGroup那一套

// 创建一个无缓冲int通道
baton := make(chan int)
// goroutine运行到这里的时候阻塞,直到另外一个goroutine准备好
runner <- baton
// goroutine运行到这里的时候阻塞,直到另外一个goroutine准备好
baton <- runner
// 无缓冲通道也可以在遍历代码中阻塞,直到有内容被放进来
for w := range p.work {
				w.Task()
			}

有缓冲通道

有缓冲的通道(buffered channel)是一种在被接收前能存储一个或者多个值的通道。这种类型的通道并不强制要求 goroutine 之间必须同时完成发送和接收。通道会阻塞发送和接收动作的条件也会不同。只有在通道中没有要接收的值时,接收动作才会阻塞只有在通道没有可用缓冲区容纳被发送的值时,发送动作才会阻塞。这导致有缓冲的通道和无缓冲的通道之间的一个很大的不同:无缓冲的通道保证进行发送和接收的 goroutine 会在同一时间进行数据交换;有缓冲的通道没有这种保证。

理解: 和线程池有点像的,

// 任务板上面最多可以显示6个任务
tasks := make(chan string, 6)
// 开了4个goroutine,相当于4个worker
for gr := 1; gr <= 4; gr++ {
		go worker(tasks, gr)
  }
// 总共有20个任务
for post := 1; post <= 20; post++ {
  tasks <- fmt.Sprintf("Task : %d", post)
}
// 阻塞goroutine,取出任务,如果没有了,ok的值就是false
task, ok := <-tasks

并发模式work

  • 可以使用通道来控制程序的生命周期。
  • 带 default 分支的 select 语句可以用来尝试向通道发送或者接收数据,而不会阻塞
  • 有缓冲的通道可以用来管理一组可复用的资源。
  • 使用无缓冲的通道来创建完成工作的 goroutine 池。
  • 任何时间都可以用无缓冲的通道来让两个 goroutine 交换数据,在通道操作完成时一定保证对方接收到了数据。
  • 语言运行时会处理好通道的协作和同步。

并发模式pool

并发模式runner

内置库

log库

// 这里只是声明
var (
	Trace   *log.Logger // Just about anything
	Info    *log.Logger // Important information
	Warning *log.Logger // Be concerned
	Error   *log.Logger // Critical problem
)

func init() {
  // 创建错误信息保存的文件
	file, err := os.OpenFile("errors.txt",os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
	if err != nil {
		log.Fatalln("Failed to open error log file:", err)
  }
  // 使用log.new来创建一个自定义的log追踪器
  // 有三个参数,第一个参数是输出位置,第二个参数是TAG,第三个参数是Log的配置信息
  // discard表示废弃,不显示
	Trace = log.New(ioutil.Discard,"TRACE: ",log.Ldate|log.Ltime|log.Lshortfile)
	Info = log.New(os.Stdout,"INFO: ",log.Ldate|log.Ltime|log.Lshortfile)
	Warning = log.New(os.Stdout,"WARNING: ",log.Ldate|log.Ltime|log.Lshortfile)
	Error = log.New(io.MultiWriter(file, os.Stderr),"ERROR:",log.Ldate|log.Ltime|log.Lshortfile)
}
func main() {
	Trace.Println("I have something standard to say")
	Info.Println("Special Information")
	Warning.Println("There is something you need to know about")
	Error.Println("Something has failed")
}

打开文件

// 创建错误信息保存的文件
file, err := os.OpenFile("errors.txt",os.O_CREATE | os.O_WRONLY | os.O_APPEND, 0666)
// 输出到多个文件,第一个是某一个文件,第二个是标准错误输出
io.MultiWriter(file, os.Stderr)

json操作

json字符串在go语言中的表现形式

var JSON = `{
	"name": "renwj",
	"title": "handsome",
	"phone": 19522453839
}`

go语言中对json字符串的设置

// 格式化json字符串,json是一个库
json.MarshalIndent(tempMap,"","    ")
// json字符串转换为struct
// 这里的struct只有在和json相关的时候这么写没有问题,如果不是json字符串,就不应该这么写
type Contact struct {
	Name    string `json:"name"`
	Title   string `json:"title"`
	Phone	int `json:"phone"`
}
var JSON = `{
	"name": "renwj",
	"title": "handsome",
	"phone": 19522453839
}`
err := json.Unmarshal([]byte(JSON), &c)

网络请求和响应

// resp即为对应的响应
resp, err := http.Get(uri)
// 处理响应,解析为json字符串,并且解码结果存储到字符串gr中
json.NewDecoder(resp.Body).Decode(&gr);

io.Writer和io.Reader

暂时没有找到需要备忘的内容,先占个位置,后面再填坑


版权声明:本文为u013894427原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。