go 基础回顾

46 阅读12分钟

const 声明常量

var 声明变量

在作用域中可以直接 :=value声明变量

{ :=6 :="aaaa"

}

  • 定义接口一般可让外部使用的 通常定义命名为Reader形式,首字母大写,最后以er结尾 ;
  • 内部使用不让外部使用的animor首字母小写,最后以er结尾;

在实际应用中,最为常用的获取变量的内存地址 使用 "&"; 获取某个地址对应的值 使用 "*";

//exampleNumberA变量(整数型变量)声明和赋值 var exampleNumberA int = 10 //获取exampleNumberA的地址,并赋值给exampleNumberAPtr变量(exampleNumberAPtr的类型是指针类型) exampleNumberAPtr := &exampleNumberA //输出exampleNumberAPtr变量的值(将输出内存地址) fmt.Println(exampleNumberAPtr) //获取exampleNumberAPtr(指针变量)表示的实际数据值,并赋值给exampleNumberAPtrValue变量(整数型变量) exampleNumberAPtrValue := *exampleNumberAPtr //输出exampleNumberAPtrValue变量(整数型变量)的值 fmt.Println(exampleNumberAPtrValue) exampleNumberAPtrValue相当于创建了一个 指针类型变量 相当于在内存中创建了没有变量名某种类型变量 new(type)和上面一系列操作得到的exampleNumberAPtrValue,都可以创建 指针类型变量

位运算符

位运算符 含义 & 按位与(AND)操作,其结果是运算符前后的两数各对应的二进位相与后的结果。 | 按位或(OR)操作,其结果是运算符前后的两数各对应的二进位相或后的结果。 ^ 按位异或(XOR)操作,当运算符前后的两数各对应的二进位相等时,返回0;反之,返回1。 &^ 按位清空(AND NOT)操作,当运算符右侧某位为1时,运算结果中的相应位值为0;反之,则为运算符左侧相应位的值。 << 按位左移操作,该操作本质上是将某个数值乘以2的n次方,n即为左移位数。更直观地来看,其结果就是将某个数值的二进制每个位向左移了n个位置。超限的高位丢弃,低位补0。

按位右移操作,该操作本质上是将某个数值除以2的n次方,n即为左移位数。更直观地来看,其结果就是将某个数值的二进制每个位向右移了n个位置。超限的低位丢弃,高位补0。

数组和切片 var resultSlice [3]int var resultSlice []int 不限制值的类型,但要求所有元素均为相同的类型

      resultArray[arrayIndex] = i        slice_name = append(slice_name, value)
     数组要指定对应的下标                      切片可使用append将某值追加到slice_name中,以达到扩充的目的,修改值和数组一样

映射 var m map[string]int

for index , value := range variable{

//循环体

} index表示下标或者键value表示实际的variable则是要循环的数组/切片/集合

函数

func you_hanshu ( [canshu] )( [result] ){

}

func main() { var resultSlice []int findPrimeNumber(resultSlice, 10) fmt.Println(resultSlice) }

func findPrimeNumber(result []int) { result = append(result) fmt.Println(result) }

其中fmt.Println(resultSlice)fmt.Println(result)打印出来的不相同, 因为在函数值传递的过程中,函数会将值复制一份,且地址不一样, 也就是操作的是函数复制的那个值,原值不会改变;

func main() { var resultSlice []int findPrimeNumber(&resultSlice, 10)//这块要传递地址 fmt.Println(resultSlice) }

func findPrimeNumber(result *[]int) {//这块要接受地址的值 *result = *append(result) fmt.Println(result) }

func hanshudefer(result *[]int) {//这块要接受地址的值 defer fmt.Print("素数") defer fmt.Print("查找") } 在函数中defer的执行顺序是反的,且是在函数最后执行完结果后再 执行的

结构体

type Dog struct { Breed string Age int Weight float64 Gender string }

  • 特别注意:这里有个方法引入其中,这里有个绑定对像;这个绑定可以是结构体/函数/基础类型

func (m MyInt) Double() MyInt { return m * 2 }

一句话总结 函数:谁都能用的工具,比如一把锤子,哪里需要敲哪里。 方法:某个“东西”专属的技能,比如“手机”能打电话,“汽车”能加速,得先有手机/汽车才能用这个技能。

  • 结构体嵌套

type Animal struct { Name int Age int Gender string } type Bird struct { WingColor string CommonAnimal Animal }

  • 匿名结构体

  • 在结构中的属性,和绑定了结构体的方法都可以使用 Dog.Name /Dog.Double() 直接使用

  • 在结构体确定数据结构后,里面的结构不能被动态的修改,也就是里面的字段结构和类型不能改变,字段的值可以被随意修改。

接口 指定行为规范

type interface_name interface{ function_name( [params] ) [return_values] ... } 实现 type fileCache struct { }

//FetchImage接口实现

func (f *fileCache) FetchImage(url string) string { return "从本地缓存中获取图片:" + url }

//从本地缓存中获取数据 var imageLoader ImageDownloader //这块就是声明了这个接口类型 imageLoader = new(fileCache) //这块new的这个fileCache要有接口中定义的方法 data := imageLoader.FetchImage("www.example.com/a.png") fmt.Println(data)

范型 =》 空接口 interface{} 可接受任意类型

func main() { dataOutput("Hello") dataOutput(123) dataOutput(true) }

func dataOutput(data interface{}) { fmt.Println(data) } 输出的 > Hello > > 123 > > true

*** 在Go语言中,用来判断某个数据是否属于某种类型的方法被称为“类型断言”。

类型断言的使用格式为:

value, ok := x.(T) 这是安全断言 value:= x.(T) 这是直接断言,失败会 (panic) 其中,x是指某个变量,T表示类型,value是将x变量转换为T类型之后的值,ok是布尔类型,表示x是否属于T类型。 ***

//计算某个物体的体积 // 正方体 type cube struct { // 边长 length float64 }

// 正方体的体积计算 func (c *cube) cubeVolume() float64 { return c.length * c.length * c.length }

func calcSize(material interface{}) float64 { cubeMaterial, cubeOk := material.(cube) //意思如果material是正方体 if cubeOk{ return cubeMaterial.cubeVolume() } else { return 0 } }

接口的嵌套

type a interface {} type b interface {} type c interface { a b } // DownloadAndSave 下载和保存 type DownloadAndSave interface { Download Save } func main() { //声明一个file类型的变量,命名为downloadFileExample downloadFileExample := new(file)

//使用DownloadAndSave接口 var downloadAndSave DownloadAndSave downloadAndSave = downloadFileExample downloadAndSave.download() downloadAndSave.save() }

下面这些代码最后的getSayHello

type Person struct { name string age int gender int }

type SayHello interface { sayHello() }

func (p *Person) sayHello() { fmt.Println("Hello!") }

func getSayHello() SayHello { var p *Person = nil return p }// 返回的是 动态类型是*Person,动态值是nil 且结构要满足SayHello

func main() { var person = new(Person) person.name = "David" person.age = 18 person.gender = 0 var sayHello SayHello sayHello = person fmt.Println(reflect.TypeOf(sayHello)) fmt.Println(sayHello == nil) fmt.Println(getSayHello()) fmt.Println(getSayHello() == nil) }

并发 在程序前面加go 可提高运行效率,其中为了保证运行的代码在mian主任务中有完整性 引入sync 包

var goRoutineWait sync.WaitGroup func main() { goRoutineWait.Add(1) // 并发调用testFunc() go testFunc() goRoutineWait.Wait() fmt.Println("程序运行结束") } // 并发测试函数 func testFunc() { defer goRoutineWait.Done() for i := 1; i <= 3; i++ { fmt.Printf("第%d次运行\n", i) time.Sleep(time.Second) } }

  • // goRoutineWait.Add() 用来创建协程

  • //goRoutineWait.Wait() 用来等待某个协程完整性

  • //goRoutineWait.Done() 方法,表示协程任务执行完成

  • 对于一颗多核心的 CPU,若设置程序只能使用一半数量的核心,代码为:

if runtime.NumCPU() > 2 { runtime.GOMAXPROCS(runtime.NumCPU() / 2) }

  • // 获取当前程序可用的CPU核心数

fmt.Println(runtime.GOMAXPROCS(0))

  • //让当前主线程让出资源,先执行协程的任务,则在该协程下面输出

runtime.Gosched()

  • 还希望立即停止协程任务的执行。方法便是使用调用 runtime.Goexit()

协程中的数据共享。

随着对 Go 并发领会的逐渐深入,使用得越来越频繁,便会遇到使用 Goroutine 的三个“陷阱”

  • Goroutine Leaks(协程任务泄露)
  • Data Race(数据竞争)
  • Incomplete Work(未完成的任务)

针对上述 1 和 3,规避的方式就是确保每一个协程任务可以正常结束。

针对上述 2,规避的方式便是通过传递数据的方式共享数据,而非直接操作某个公共变量,从而规避数据竞争。

通道(Channel)类型可以实现数据共享

  • 同步通道,
  • 缓冲通道
var intChan = make(chan int)
    //生产鸡蛋
func layEggs() {
        intChan <- 1
        close(intChan)
    }
    //消耗鸡蛋
func eatEggs(intChan chan int) {
    eggCounts := <-intChan
    fmt.Printf("吃%d个荷包蛋", eggCounts)
    }

    func main() {
// 执行2个协程任务
syncWait.Add(2)
// 开启下蛋任务
go layEggs()
// 开启吃荷包蛋任务
go eatEggs(intChan)
// 等待协程任务完成
syncWait.Wait()
}
//使用同步通道时,要确保传出数据和获取数据必须成对出现。另外,一旦通道被关闭,便不能再向其中传出数据了

缓存通道

同步通道和缓冲通道在声明时的区别,只在于是否定义缓冲区的大小。当缓冲区大小的值为 0 时,通道的类型将为同步通道。DaysOfWeek有无

const DaysOfWeek = 7
var intChan = make(chan int, DaysOfWeek)

如果有多个发送者 :只让某个通道的唯一发送者关闭该通道,

判断通道是否关闭 1.尝试从通道中接收值来判断通道是否关闭;

func collectEggs(intChan chan int) {
   defer syncWait.Done()
   var eggCounts int
   for i := 0; i < DaysOfWeek; i++ {
      eggCounts += <-intChan
      fmt.Println("鸡蛋被收集了")
   }
   _, isOpen := <-intChan
   if !isOpen {
      fmt.Printf("本周共产%d个鸡蛋\n", eggCounts)
   }
}

2.当通道关闭后,for-range 循环会自动跳出

func collectEggs(intChan chan int) {
   defer syncWait.Done()
   var eggCounts int
   for intValue := range intChan {
      eggCounts += intValue
      fmt.Println("鸡蛋被收集了")
   }
   fmt.Printf("本周共产%d个鸡蛋\n", eggCounts)
}

单向通道 readOnlyIntChan 的只接收通道 sendOnlyIntChan 的只发送通道

var readOnlyIntChan <-chan int = intChan    //<-chan 表示只接收通道。与其相反

var sendOnlyIntChan chan<- int = intChan   //chan<- 则表示只发送通道

锁和原子操作

一个是 sync.Mutex,另一个是 sync.Atomic,它们分别用于锁和原子操作
互斥锁
var testInt = 0
var syncWait sync.WaitGroup
var locker sync.Mutex
func main() {
   syncWait.Add(2)
   go testFunc()
   go testFunc()
   syncWait.Wait()
   fmt.Println(testInt)
}
//locker.Lock() 是加锁,locker.Unlock() 是解锁
func testFunc() {
    defer syncWait.Done()
    defer locker.Unlock()
    locker.Lock()
    for i := 0; i < 1000; i++ {
       testInt += 1
    }
}
读写互斥锁

我们可以用 “读写互斥锁”充分发挥并发优势,只在写操作上串行,保证数据安全。

读写互斥锁的运行机制是这样的:

  • 当协程任务获得读操作锁后,读操作并发运行,写操作等待;
  • 当协程任务获得写操作锁后,考虑到数据可能发生变化,所以无论是读还是写操作都要等待。

写操作的方法依然 locker.Lock() 是加锁,locker.Unlock() 是解锁。 读操作的方法则是 locker.RLock() 和 locker.RUnlock()

原子操作

所谓“原子操作”,简单理解就是指那些进行过程中不能被打断的操作 a在执行,必须执行完,b才可以操作 Go 语言中的原子操作通过 sync/atomic 包实现,具体特性如下:

  • 原子操作都是非入侵式的;

  • 原子操作共有五种:增减、比较并交换、载入、存储、交换;

  • 原子操作支持的类型类型包括 int32、int64、uint32、uint64、uintptr、unsafe.Pointer

    原子操作可以理解为“最小的不可分割的操作单元”。例如,对变量i进行i++操作,实际包含三个步骤:

    • 从内存读取i的值到寄存器
    • 寄存器中的值加1
    • 将结果写回内存 如果在多线程/协程环境下,这三个步骤可能被其他操作打断,导致数据不一致。而原子操作通过CPU指令保证这三个步骤作为一个整体执行, 中间不会被其他协程干扰,从而避免数据竞争(Race Condition)

反射

在程序编译过程中,代码中的变量会被转换为内存地址,变量名称不会被编译器写入可执行的部分。 所以在程序运行期间,通常是无法获取变量名、变量类型以及结构体内部构造等等信息的。 使用反射则可以让编译器编译代码时将某些需要访问自身信息的变量``写入可执行文件中,并对外开放访问它们的途径。 简单地说,借助反射,可以使程序运行期间可以对其本身进行访问修改

反射的基本使用

  • 通过反射获取变量的类型与值;
  • 通过反射修改变量的值。

反射的三大定律

  • 反射可以将“接口类型变量”转换为“反射类型对象”;
  • 反射可以将“反射类型对象”转换为“接口类型变量”;
  • 如果要修改“反射类型对象”,其值必须是“可写的”(settable)

通过反射获取变量的类型与值 在前面课程的示例中,我们其实已经或多或少地使用过反射了。比如下面这段代码:

func main() {
var testNum int = 10
fmt.Println(reflect.TypeOf(testNum))
fmt.Println(reflect.ValueOf(testNum))
}

在这段代码中,通过调用 reflect.TypeOf()reflect.ValueOf() 函数,并向其中传入 testNum 变量, 可以轻松地获取 testNum 的类型。本例中,testNum 是 int 类型,值为 10。运行这段代码,可以观察到控制台输出即为:

int > > 10

使用反射进行值属性获取或修改值前,一定要确保操作对象是可寻址的 ,对于修改值得操作,还要确保操作对象是可被修改的
func main() {
   var testNum int = 10
   ptrOfTestNum := &testNum
   valueOfPtrTestNum := reflect.ValueOf(ptrOfTestNum)
   valueOfPtrTestNum.Elem().SetInt(20)
   fmt.Println(testNum)
}

反射的三大定律

  • 反射可以将“接口类型变量”转换为“反射类型对象”

Go 语言的反射是通过接口来实现的。 运行时,首先会将任意类型的变量转换为接口类型,再从接口类型转换为反射类型(即 reflect.Type 和 reflect.Value)

  • 反射可以将“反射类型对象”转换为“接口类型变量” 上述转换仅发生于 reflect.Value.interface(),不适用于 reflect.Type。

  • 如果要修改“反射类型对象”,其值必须是“可写的”(settable) Go 语言在函数间传递参数时,都是值传递。若要修改原值,就一定要确保操作对象是可写(可通过值的 CanSet() 方法判定)的。

使用反射访问结构体

Go 语言提供了获取成员个数和属性信息的方法,它们通过 reflect.Type 类型调用。较为常用的有以下两个方法:

// 获取成员个数
  NumField()
// 获取成员属性信息
   Field()
func main() {
   personExample := Person{Name: &quot;小明&quot;, age: 18, gender: 1}
   typeOfPersonExample := reflect.TypeOf(personExample)
   for i := 0; i< typeOfPersonExample.NumField(); i++ {//小于它的个数
      fmt.Println(typeOfPersonExample.Field(i).Name,
         typeOfPersonExample.Field(i).Type,
      )
}
   //可通过
fmt.Println(typeOfPersonExample.Field(i).Name, //属性
typeOfPersonExample.Field(i).Type, //类型
typeOfPersonExample.Field(i).Tag, //值
)
> Name string display:"名字" > > age int display:"年龄" > > gender int display:"性别"

使用反射调用函数

使用反射创建变量

控制反转与依赖注入

依赖注入可以让控制反转变成现实。

func test() {
fmt.Println("我被调用了!")
}
func main() {
   injector := inject.New()
   injector.Invoke(test)
}

接下来就是调用时的传参和接收返回值了。传参的过程又被称为注入参数的过程