Go学习笔记

Version

$ go version
go version go1.14.2 darwin/amd64

编写规则:

  1. 文件后缀为.go

  2. 程序区分大小写

  3. 语句后,不需要带分号

  4. 定义的变量或import包,必须使用否则会报错

  5. 每一个语句一行,否则会报错

  6. {}成对出现,而且风格为

    func main(){
    	//code
    }
    

变量

声名语法:var 变量名 数据类型

备注:没有public、private等关键字来修饰,但可以简单的理解为首字母大写是公开的,首字母小写是私有的

  1. 指定变量类型,声明后若不赋值,使用默认值

    package main
    import (
    	"fmt"
    )
    func main(){
    	var i int
    	fmt.Println("i的默认值为:",i)
    }
    
  2. 类型推导:根据值自行判定变量的类型

    package main
    import (
    	"fmt"
    )
    import (
    	"fmt"
    )
    func main(){
    	var num =8.8
    	fmt.Println("num的值为:",num)
    }
    
  3. 省略var,注意新的变量名不能为已经声明过的变量,否则会编译错误。

    package main
    import (
    	"fmt"
    )
    func main(){
    	name := "wdm.life"
    	fmt.Println("name的值为:",name)
    }
    
  4. 多变量声明

    package main
    import "fmt"
    func main(){
    	name,sex,old := "wdm.life", "男", 25
    	fmt.Println("姓名:",name,"性别:",sex,"年龄:",old)
    }
    
  5. 全局变量声明则放在函数外面即可

  6. Golang的变量如果没有赋初值,编译器会使用默认值,比如int 默认值0 string 默认值为空串, 小数默认为0

类型转换

#string到int
int,err:=strconv.Atoi(string)
#string到int64
int64, err := strconv.ParseInt(string, 10, 64)
#int到string
string:=strconv.Itoa(int)
#int64到string
string:=strconv.FormatInt(int64,10)

string

  1. 双引号,会识别转义字符

  2. 反引号,以字符串原生形式输出,包括换行和特殊字符,可以实现防止攻击,输出源代码等效果

    str:=`
    package main
    import "fmt"
    func main(){
    	//code
    }
    `
    
  3. 多行字符串

    str:="name"+
    "sex"
    

    类型转换

    基本语法:T(v) 将值v转换为类型T

    var i int32 =182
    var n2 float32=float32(i)
    

    基本类型转string类型

    1. fmt.Sprintf(“%参数”, 表达式)

    string转基本类型

    使用strconv包函数

    var str string="true"
    var b bool
    
    b,_=strconv.ParseBool(str)
    

指针

  1. 基本数据类型,变量存的就是值,也叫值类型
  2. 获取变量的地址,用&,比如: var num int,获取num的地址: &num分析一下基本数据类型在内存的布局.
  3. 指针类型,指针变量存的是一个地址,这个地址指向的空间存的才是值比如: var ptr *int = &num
  4. 获取指针类型所指向的值,使用: *,比如: var ptr int, 使用ptr获取ptr指向的值

值类型和引用类型

  1. 值类型:基本数据类型int 系列,float系列,bool, string、数组和结构体struct
  2. 引用类型:指针、slice 切片、map、 管道chan、 interface 等都是引用类型

特点

​ 值类型:变量直接存储值,内存通常在栈中分配

​ 引用类型:变量存储的是一个地址,这个地址对应的空间才真正存储数据(值),内存通常在堆上分配,当没有任何变量引用这个地址时,该地址对应的数据空间就成为一个垃圾,由GC来回收

包的使用

  1. 在给一个文件打包时,该包对应的一个文件夹,通常文件的包名跟文件所在的文件夹名一致,一般为小写字母

  2. 当一个文件要使用其它包函数或变量时,需要先引入对应的包

    1. import "包名"
      
    2. import (
      	"包名1"
      	"包名2"
      )
      
    3. package 指令在go文件的首行,然后是import指令

    4. import包时,路径从$GOPATH的src下开始,不需要带src,编译器会自动从src下开始引入

  3. 为了让其它包的文件,可以访问到本包的函数,则该函数名的首字母要大写,类似其它语言的pubilc

  4. 在访问其它包函数或变量时,语法是:包名.函数名

  5. 如果包名较长,可以给包取别名。**注意:**取别名后,原来的包名就不能使用了。要使用别名来访问函数或变量

  6. 在同一个包下不能有相同的函数名、变量名,否则报重复定义

  7. 编译为可执行程序文件时,就需要将这个包声明为main,即package main。这个一个语法规范,如果你写一个库,包名可以自定义

函数使用的细节

  1. 函数的形参列表可以是多个,返回值列表也可以是多个。

  2. 形参列表和返回值列表的数据类型可以是值类型和引用类型。

  3. 函数的命名遵循标识符命名规范,首字母不能是数字,首字母大写该函数可以被本包文件和其它包文件使用,类似public,首字母小写,只能被本包文件使用,其它包文件不能使用,类似privat

  4. 函数中的变量是局部的,函数外不生效

  5. 基本数据类型和数组默认都是值传递,即进行值拷贝。在函数内修改,不会影响到原来的值

  6. 如果希望函数内的变量能修改函数外的亦是,可以通过传入变量的地址&,函数内以指针的方式操作变量

  7. Go不支持函数重载

  8. 在Go中,函数也是一种数据类型,可能赋值给一个变量,则该变量就是一个函数的变量了。通过该变量可以对函数调用

    package main
    
    import "fmt"
    
    func getSum(num1 int, num2 int) int {
    	return num1 + num2
    }
    func main() {
    	biov := getSum
    	fmt.Printf("biov的类型%T,getSum类型是%T\n", biov, getSum)
    	result := biov(3, 7)
    	fmt.Println("result=", result)
    }
    
  9. 函数既然是一种数据类型,因此在Go中,函数可能作为形参且调用

    package main
    
    import "fmt"
    
    func main() {
    	result := myfun(getSum, 3, 7)
    	fmt.Println("result=", result)
    }
    
    func myfun(funvar func(int, int) int, num1 int, num2 int) int {
    	return funvar(num1, num2)
    }
    
    func getSum(num1 int, num2 int) int {
    	return num1 + num2
    }
    
    
  10. 为了简化数据类型定义,Go支持自定义数据类型

    基本语法为:type 自定义数据类型名 数据类型 //相当于一个别名

    type myInt int
    
  11. 支持函数返回值命名

    func getSumAndSub(n1 int,n2 int) (sum int,sub int){
    	sum = n1 + n2
    	sub = n1 - n2
    	return
    }
    
  12. 使用 _ 标识符,忽略返回值

  13. 支持可变参数

    // 0~n个参数
    func sum(args... int)sum int{
    }
    // 1~n个参数
    func sum(n1 int,args... int)sum int{
    }
    

    说明:

    1. args是slice切片,通过args[index]可以访问到各个值

    2. 如果一个函数的形参中有可变参数,则可变参数需要放在在形参列表最后

      package main
      
      import "fmt"
      
      func main() {
      	result := sum(1, 2, 3, 4, 5, 6, 7, 8, 9)
      	fmt.Println("result=", result)
      }
      
      func sum(n1 int, args ...int) int {
      	sum := n1
      	for i := 0; i < len(args); i++ {
      		sum += args[i]
      	}
      	return sum
      }
      

init函数

介绍

每一个源文件都可以包含一个init函数,该函数会在main函数执行前,被Go运行框架调用,换一个说法就是init会在main函数前先被执行

细节

1. 如果一个文件同时包含全局变量定义,init函数各main函数,则执行的流程:**全局变量定义->init函数->main函数**

2. init函数最主要的作用是完成一些初始化的工作

匿名函数

介绍

没有名字的函数就是匿名函数,某个函数只使用一次时可以考虑使用匿名函数,它也可以实现多次调用。

使用方式1

func main(){
	result :=func (n1 int, n2 int)int{
		result n1 + n2
	}(3,7)
	
	fmt.Println("result=",result)
}

使用方式2

func main(){
  a := func (n1 int, n2 int)int {
    return n1 - n2
  }
  result :=a(3, 7)
  fmt.Println("result=",result)
  result2 :=a(3, 7)
  fmt.Println("result=",result2)
}

全局匿名函数

如果将匿名函数赋给一个全局变量,那么这个匿名函数,就成为一个全局匿名函数

闭包

介绍

一个函数和与其相关的引用环境组合的一个整体(实体)

案例

func AddUpper() func (int)int{
	var n int = 10 
	return func (x int) int{
		n = n + x
		return n
	}
}

func main(){
	f := AddUpper()
	fmt.Println(f(1))
}
  1. AddUpper 是一个函数,返回的数据类型是fun(int) int

  2. 闭包说明

    var n int = 10 
    	return func (x int) int{
    		n = n + x
    		return n
    	}
    

    返回的是一个匿名函数,但是这个匿名函数引用到函数外的n,因此这个艾薇儿函数就和n形成一个整体,构成闭包

  3. 简单理解为:闭包是类,函数是操作,n是字段。函数和它使用到n构成闭包

  4. 当我们反复的调用f函数时,因为n是初始化一次,因此每骼一次不进行累计

  5. 我们要搞清楚闭包的关键,就是要分析出返回的函数它使用(引用)到哪些变量,因为函数和它引用到的变量共同构成闭包

闭包最佳实践

要求如下:

  1. 编写一个函数 makeSuffix(suffix string) 可以接收一个文件后缀名(比如.jpg),并返回一个闭包

  2. 调用闭包,可以传入一个文件名,如果该文件名没有指定的后缀(如.jpg),则返回 文件名.jpg,如果已经有.jpg后缀,则返回原文件名

  3. 要求使用闭包的方式完成

  4. strings.HasSuffix,该函数可以判断某个字符串是否有指定的后缀

    package main
    
    import (
    	"fmt"
    	"strings"
    )
    
    func main() {
    	f2 := makeSuffix(".jpg")
    	fmt.Println("文件名处理后=", f2("eeedsss"))
    	fmt.Println("文件名处理后=", f2("wdm.life"))
    	fmt.Println("文件名处理后=", f2("wdm.jpg"))
    }
    
    func makeSuffix(suffix string) func(string) string {
    	return func(xx string) string {
    		if !strings.HasSuffix(xx, suffix) {
    			return xx + suffix
    		}
    		return xx
    	}
    }
    

    上面代码的总结与说明:

    1. 返回的匿名函数和makeSuffix(suffix string) 的 suffix 变量 组合成一个闭包,因为返回的函数引用到suffix这个变量
    2. 我们体会一下闭包的好处,如果使用传统的方法,也可以轻松实现这个功能,但是传统方法需要每次都传入后缀名,比如.jpg,而闭包因为可以保留上次引用的某个值,所以我们传入一次就可以反复使用

defer函数

干啥用呢

在函数中,程序员经常需要创建资源(比如:数据库连接。文件句柄、锁等),为了在函数执行完毕后,及时的释放资源,Go的设计者提供defer(延时机制)

细节

  1. 当Go执行到一个defer时,不会立即执行defer后的语句,而是将defer后的语句压入到一个栈中

  2. 当函数执行完毕后,再从defer栈中,依次从栈顶取出语句执行(注:遵守栈的先入后出的机制)

  3. 在defer将语句放入到栈时,也会将相关的值拷贝同时入栈。

    package main
    
    import (
    	"fmt"
    )
    
    func main() {
    	result := sum(3, 7)
    	fmt.Println("result=", result)
    }
    
    func sum(n1 int, n2 int) int {
    	//当执行到defer时,暂时不执行,会将defer后面的语句压入到独立的栈(defer栈)
    	//当函数执行完毕后,再从defer栈, 按照先入后出的方式出栈,执行
    	defer fmt.Println("ok1 n1=", n1) //defer 3. ok1 n1 = 10
    	defer fmt.Println("ok2 n2=", n2) //defer 2. ok2 n2= 20
    	//增加一句话
    	n1++
    	//n1=11
    	n2++
    	res := n1 + n2
    	fmt.Println("ok3 res=", res) // 1. ok3 res= 32
    	return res
    }
    

其它说明

  1. 创建资源后,比如打开了文件,获取了数据库的链接,或者是锁资源,可以执行defer fiel.close()defer connect.Close()
  2. 在defer后,可以继续使用创建资源
  3. 当函数执行完毕后,系统会依次从defer栈中,取出语句关闭资源
  4. 这种机制非常简洁,程序员不用再为在什么时机关闭资源而烦恼

函数参数传递方式

两种传递方式

  1. 值传递
  2. 引用传递

其实,不管是值传递还是引用传递,传递给函数的都是变量的副本,不同的是,值传递的是值的拷贝,引用传递的是地址的拷贝,一般来说,地址拷贝效率高,因为数据量小,而值拷贝决定拷贝的数据大小,数据越大,效率越低。

时间

方式1 格式化输出的时间是固定的**”2006-01-02 15:04:05”**

fmt.Printf(now.Format("2006-01-02 15:04:05"))
fmt.Printf(now.Format("2006-01-02"))
fmt.Printf(now.Format("15:04:05"))

方式2

fmt.Printf("当前时间是 %d-%d-%d- %d:%d:%d \n",now.Year(),now.Month(),now.Day(),now.Hour(),now.Minute(),now.Second())

时间的常量

const(
	Nanosecond Duration = 1//纳秒
	Microsecond 				= 1000 * Microsecond //微秒
	Millisecond					=1000 * Microsecond //毫秒
	Second							=1000 * Millisecond //秒
	Minute							=60 * Second //分钟
	Hour								=60 * Minute //小时
)

内置函数

  1. len 用来求长度,如string、array、slice、map、channel
  2. new 用来分配内存,主要用来分配值类型,比如int、float32、struct……返回的是指针
  3. make 用来分配内存,主要用来分配引用类型,比如chnnel、map、slice、

错误处理

  1. 在默认的情况下,当发生错误后(panic),程序就会退出(崩溃)
  2. 我们希望当发生错误后,可以捕获到错误,并进行处理,保证程序可以继续执行。还可以在捕获到错误后给我们一个提示

自定义错误

  1. errors.New(“错误说明”),会返回一个error类型的值,表示一个错误
  2. panic内置函数,接收一个interface{}类型的值(任何值)作为参数。可以接收error类型的变量,输出错误信息,并退出程序

数组与切片

数组的定义

var 数组名 [数组大小]数据类型;如var a [5]int

赋值 a[0] = 1 a[2]=3……

数组初始化的方式

var numArr01 [3]int =[3]int{1,2,3}

var numArr02 =[3]int{4,5,6}

var numArr03 =[...]int{1:11,2:222,3:333}

numArr04 :=[...]string{1:"11",2:"222",3:"333"}

数组的遍历

for k,v := range numArr01{
	fmt.Println(k,":"v)
}

for _,v := range numArr01{
	fmt.Println(v)
}

数组使用的注意事项和细节

  1. 数组是多个相同类型数据的组合,一个数组一旦声明/定义了,其长度是固定的,不能动态变化

  2. var arr []int 这个arr 就是一个slice切片

  3. 数组中的元素可以是任何数据类型,包括类型和引用类型,但是不能混用

  4. 数组创建后,如果没有赋值,有默认值(零值)

    数值类型数组:0

    字符串数组:“”

    bool数组:false

  5. 使用数组的步骤:1. 声明数组并开辟空间 2. 给数组各个元素赋值 3. 使用数组

  6. 数组的下标是从0开始的

  7. 数组下标必须在指定范围内使用,否则报panic:数组越界,如:var arr[5]int 则有效下标为0-4

  8. 数组属值类型,在默认情况下是值传递,因此会进行值拷贝。数组间不会影响

  9. 如想在其它函数中,去修改原来的数组,可以使用引用传递(指针方式)

    func test01(arr *[3]int){
    	(*arr)[0] = 88
    }
    
  10. 长度是数组类型的一部分,在传递函数参数时需要考虑数组的长度

切片

基本介绍

  1. 切片的英文是slice

  2. 切片是数组的一个引用,因此切片是引用类型,在进行传递时,遵守引用传递的机制

  3. 切片的使用和数组类似,遍历切片、访问切片的元素和求切片长度len(slice)都一样

  4. 切片的长度是可以变化的,因此切片是一个可以动态变化数组

  5. 切片定义的基本语法:

    var 切片名 []类型 如:var a[]int

    func main() {
    	var intArr [6]int = [...]int{1, 22, 33, 66, 99, 88}
    	slice := intArr[1:3]
    	fmt.Println("intArr=", intArr)
    	fmt.Println("slice 的元素是 =", slice)
    	fmt.Println("slice 的元素个数 =", len(slice))
    	fmt.Println("slice 的容量 =", cap(slice))
    }
    
  6. 使用

    1. 定义一个切片,然后让切片去引用一个已经创建好的数组,比如:

      func main() {
      	var intArr [6]int = [...]int{1, 22, 33, 66, 99, 88}
      	slice := intArr[1:3]
      	fmt.Println("intArr=", intArr)
      	fmt.Println("slice 的元素是 =", slice)
      	fmt.Println("slice 的元素个数 =", len(slice))
      	fmt.Println("slice 的容量 =", cap(slice))
      }
      
    2. 通过make来创建切片

      基本语法:var 切片名 []type = make([]type,len,[cap])

      参数说明:type:数据类型 len:大小 cap:指定切片容量,可选,如果你分配了cap,则要求vap>=len

      1. 通过make方式创建切片可以指定切片的大小和容量
      2. 如果没有给切片的各个元素赋值,那么就会使用默认值[int, float=>0 string=>”” bool=>false]
      3. 通过make方式创建的切片对应的数组是由make底层维护,对外不可见即只能通过slice去访问各个元素
    3. 定义一个切片,直接就指定具体数据,使用原理类似make的方式

    1和2的区别:1是直接引用数组,这个数组是事先存在的程序员是可见的;2是通过make来创建切片,make也会创建一个数组,是由切片在底层进行维护,程序员是不见的

    遍历

    1. for循环是常规方式遍历

    2. for-range结构遍历切片

      func main(){
      	var arr [5]int = [...]int{1,2,3,4,5}
      	slice := arr[1:4]
      	for i := 0; i < len(slice); i++{
      		fmt.Printlf("slice[%v]=%v ",i,slice[i])
      	}
      	fmt.Println()
      	//推荐使用这个方式
      	for i, v := range slice{
      		fmt.Printlf("slice[%v]=%v \n",i,v)
      	}
      }
      

    切片使用的注意事项和细节讨论

    1. 切片初始化时 var slice = arr[startIndex:endIndex];说明:从arr数组下标为startIndex,取下下标为endIndex的元素(不含arr[endIdex])

    2. 切片初始化时,仍然不能越界。范围在[0~len(arr)]之间,但是可以动态增长

      var slice = arr[0:end] 可以简写 var slice =arr[:end]

      var slice = arr[start:len(arr)] 可以简写 var slice = arr[start:]

      var slice = arr[0:len(arr)] 可以简写 var slice = arr[:]

    3. cap是一个内置函数,用于统计切片的容量,即最大可以存放多少个元素

    4. 切片定义后,还不能使用,因为本身是个空的,需要让其引用到一个数组,或者make一个空间供切片来使用

    5. 切片可以继续切片

      package main
      
      import (
      	"fmt"
      )
      
      func main() {
      	var arr [5]int = [...]int{1, 2, 3, 4, 5}
      	slice := arr[1:4]
      	//使用常规的for遍历切片
      	for i := 0; i < len(slice); i++ {
      		fmt.Printf("slice[%v]=%v ", i, slice[i])
      	}
      	fmt.Println()
      	//使用for-range 遍历切片
      	for i, v := range slice {
      		fmt.Printf("i=%v v=%v \n", i, v)
      	}
      
      	slice2 := slice[1:2]
      	//因为arr, slice 和slice2 指向的数据空间是同一个,因此slice[0]=100
      	slice2[0] = 100
      
      	fmt.Println("slice2= ", slice2)
      	fmt.Println("slice= ", slice)
      	fmt.Println("arr= ", arr)
      }
      
    6. 用append内置函数,可以对切片进行动态追加

      var slice3 []int= []int{1, 2, 3}
      slice3 = append(slice3, 4, 5, 6)
      fmt.Println("slice3", slice3)
      slice3 = append(slice3,slice3...)
      fmt.Println("slice3", slice3)
      

      分析:

      1. 切片append操作的本质就是对数组扩容
      2. go底层会创建一个新的数组newArr(安装扩容后大小)
      3. 将slice原来包含的元素拷贝到新的数组newArr
      4. slice重要引用到newArr
    7. 切片的拷贝操作

      var slice4 []int = []int{1,2,4,5}
      var slice5 = make([]int,10)
      copy(slice5,slice4)
      fmt.Println("slice4=",slice4)
      fmt.Println("slice5=",slice5)
      
      1. copy(para1,para2)参数的数据类型是切片
      2. 按照上面的代码来看,slice4和slice5的数据空间是独立,相互不影响,就是说slice4[0]=999,slice5[0]仍然是 1
    8. 拷贝注意事项

      var a []int = []int{1,2,3,4,5}
      var slice = make([]int, 1)
      fmt.Println(slice) //[0]
      copy(clise,a)
      fmt.Println(slice)//[1]
      
    9. 切片是引用类型,所以在传递时,遵守引用传递机制。

      案例:斐波那契的数列

      package main
      
      import "fmt"
      
      func fbn(n int) []uint64 {
        //声明一个切片,大小为n
      	fbnSlice := make([]uint64, n)
      	fbnSlice[0] = 1
      	fbnSlice[1] = 1
      	for i := 2; i < n; i++ {
      		fbnSlice[i] = fbnSlice[i-1] + fbnSlice[i-2]
      	}
      	return fbnSlice
      }
      func main() {
      	fbnSlice := fbn(20)
      	fmt.Println("fnbSlice=", fbnSlice)
      }
      

排序与查找

基本介绍

排序是将一组数据,依指定的顺序进行排列的过程

分类

  1. 内部排序:指将要处理的所有数据都加载到内部存储器中进行排序,包括(交换式排序法、选择式排序法和插入式排序法
  2. 外部排序法:数据量过大,无法全部加载到内存中,需要借助外部存储进行排序。包括(合并排序法和直接合并排序法

冒泡排序

package main

import (
	"fmt"
)

func main() {
	arr := [5]int{22, 21, 67, 34, 2}
	BubbleSort(&arr)

}

// 冒泡排序
func BubbleSort(arr *[5]int) {
	fmt.Println("排序前arr=", arr)
	tem := 0
	for i := 0; i < len(*arr)-1; i++ {

		for j := 0; j < len(*arr)-1-i; j++ {
			if (*arr)[j] > (*arr)[j+1] {
				// 交换
				tem = (*arr)[j]
				(*arr)[j] = (*arr)[j+1]
				(*arr)[j+1] = tem
			}
		}
	}
	fmt.Println("排序后arr=", arr)
}

查找

  1. 顺序查找

    package main
    
    import (
    	"fmt"
    )
    
    func main() {
    	names := [4]string{"小明", "小东", "小李", "李华"}
    
    	var heroName = ""
    	fmt.Println("请输入你要查找的人名……")
    	fmt.Scanln(&heroName)
    	// 顺序查找
    	for i := 0; i < len(names); i++ {
    		if heroName == names[i] {
    			fmt.Print("找到%v , 下标%v \n", heroName, i)
    			break
    		} else if i == (len(names) - 1) {
    			fmt.Println("没有找到%v \n", heroName)
    		}
    	}
    }
    
  2. 二分查找(该数组是有序)

    /* 思路
    查找的数是 findVal
    1. arr是一个有序的数组,并且是从小到大排序
    2. 先找到中间的下标 middle = (leftIndex + rightIndex)/2,然后让中间下标的值和findVal进行比较
    2.1 如果arr[middle] > findVal, 就应该向 leftIndex --- (middle -1)
    2.2 如果arr[middle] < findVal, 就应该向 middl+1 --- rightInde
    2.3 arr[middle] == findVal, 就找到了
    3. 当leftIndex > rightIndex 的情况下就说明找不到了。退出循环
    */
    
    package main
    
    import "fmt"
    
    func main() {
    	arr := [6]int{1, 2, 3, 4, 5, 6}
    	BinarFind(&arr, 0, len(arr)-1, 6)
    
    }
    func BinarFind(arr *[6]int, leftIndex int, rightIndex int, findVal int) {
    	// 判断leftIndex 是否大于 rightIndex
    	if leftIndex > rightIndex {
    		fmt.Println("找不到")
    		return
    	}
    	// 先找到 中间的下标
    	middle := (leftIndex + rightIndex) / 2
    
    	if (*arr)[middle] > findVal {
    		// 说明我们要查找的数,应该在 leftIndex --middle-1
    		BinarFind(arr, leftIndex, middle-1, findVal)
    	} else if (*arr)[middle] < findVal {
    		// 说明我们要查找的数,应该在 middle+1 --rightIndex
    		BinarFind(arr, middle+1, rightIndex, findVal)
    	} else {
    		fmt.Printf("找到了,下标为%v \n", middle)
    	}
    }
    

二维数组

语法

var 数组名 [大小] [大小]类型;

var arr [2][3]int 

初始化

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

遍历

for

package main

import "fmt"

func main() {
	var arr = [2][3]int{{1, 2, 3}, {4, 5, 6}}
	for i := 0; i < len(arr); i++ {
		for j := 0; j < len(arr[i]); j++ {
			fmt.Printf("%v\t", arr[i][j])
		}
		fmt.Println()
	}
}

for-range

package main

import "fmt"

func main() {
	var arr = [2][3]int{{1, 2, 3}, {4, 5, 6}}
	for k, v := range arr {
		for j, v2 := range v {
			fmt.Printf("arr[%v][%v]=%v\t", k, j, v2)
		}
	}
	fmt.Println()
}

map

介绍

map 是 key-value 数据结构,又称为字段或者关联数组。类似其它编程语言的集合,在编程中经常使用到

声明

语法

var map 变量名 make [keytpe]valuetype;

go中的map的key可以是很多种类型,比如bol,数字,string,指针,channel,还可以是只包含前面几个类型的接口结构体,数组

通常key为int、string

**注意:**slice,map 还有function不可以,因为几个没有用 == 来判断

valuetype 的类型和key基本一样,通常为:数字(整数,浮点数),string,map,struct

声明是不会分配内存的,初始化需要make,分配内存才能赋值和使用

案例

a = make(map[string][string],10)
a["no1"]="梨花"
a["no2"]="桃子"
a["no3"]="武松"

说明:

  1. map 在使用前一定要make
  2. map 的 key 是不能重复,如果重复了,则以最后这个 key-value 为准
  3. map 的value 是可以相同的
  4. map 的key-value 是无序
  5. make 内置函数数目

使用

package main

import (
	"fmt"
)

func main() {
	//方式1
	var a map[string]string
	a = make(map[string]string, 10)
	a["no1"] = "梨花"
	a["no2"] = "桃子"
	a["no3"] = "武松"

	//方式2
	cities := make(map[string]string)
	cities["no1"] = "广西"
	cities["no2"] = "广州"
	cities["no3"] = "上海"

	//方式3
	heroes := map[string]string{
		"no1": "广西",
		"no2": "广州",
		"no3": "上海",
	}
	fmt.Println(heroes)
}

增删改查

map[“key”] = value ;如果key不存在就是增加,已存在就是修改

**删除:**delete(mapName,”key”),delete是一个内置函数,如果存在就删除该key-value,如果key不存在不操作,但 是也不会报错。

查找

val, ok := cities["no2"]
if ok {
	fmt.Println(“存在”)
}else {
	fmt.Println("不存在")
}

遍历

说明:map 的遍历使用for-range的结构遍历

package main

import (
	"fmt"
)

func main() {
	cities := make(map[string]string)
	cities["no1"] = "广西"
	cities["no2"] = "广州"
	cities["no3"] = "上海"

	for k, v := range cities {
		fmt.Printf("k=%v v=%v \n", k, v)
	}
}

map切片

切片的数据类型是map,则我们称为 slice of map,map切片,这样使用则map个数就可以动态变化了

package main

import (
	"fmt"
)

func main() {
	var mosters []map[string]string
	mosters = make([]map[string]string, 2)
	if mosters[0] == nil {
		mosters[0] = make(map[string]string, 2)
		mosters[0]["name"] = "老牛"
		mosters[0]["age"] = "500"
	}
	if mosters[1] == nil {
		mosters[1] = make(map[string]string, 2)
		mosters[1]["name"] = "玉兔精"
		mosters[1]["age"] = "400"
	}
	fmt.Println(mosters)
}

map排序

  1. go 中没有一个专门的方法针对map的key进行排序

  2. go 中的map默认是无序的,注意也不是按照添加的顺序存放的,每次遍历输出的顺序可能不一样

  3. go 中map的排序是先将key进行排序,然后根据key值遍历输出即可

    package main
    
    import (
    	"fmt"
    	"sort"
    )
    
    func main() {
    	map1 := make(map[int]int, 10)
    	map1[10] = 100
    	map1[1] = 11
    	map1[2] = 12
    	map1[4] = 44
    	map1[9] = 66
    	fmt.Println(map1)
    	var keys []int
    	for k, _ := range map1 {
    		keys = append(keys, k)
    	}
    	sort.Ints(keys)
    	fmt.Println(keys)
    	for _, k := range keys {
    		fmt.Printf("map[%v]=%v \n", k, map1[k])
    	}
    }
    

细节:

  1. map 是引用类型,遵守引用类型传递的机制,在一个函数接收map修改后会直接修改原来的map
package main

import "fmt"

func modify(map1 map[int]int) {
	map1[10] = 800
}
func main() {
	map1 := make(map[int]int)
	map1[1] = 90
	map1[2] = 88
	map1[3] = 33
	map1[10] = 10
	modify(map1)

	fmt.Println(map1)
}
  1. map 的容量达到后,再想增加元素会自动扩容,并不会发生panic,也就说明map能动态的增长 键值对(key-value)
  2. mao 的 value 也经常使用struct类型,更适合管理复杂的数据(比前面value是一个map更好),比如 value 为Student的结构体

结构体

基本语法

type 结构体名称 struct{
	field1 type
	field2 type
}

案例

type Student struct{
	Name string
	Age int
	Score float32
}

说明:结构体是值类型

创建与访问

  1. 直接声明

var persion Psersion

  1. {}

    var persion Persion = Persion{}

  2. -&

    var persion *Persion =new (Persion)

  3. -{}

    var persion *Persion = &Persion{}

说明:

  1. 3和4返回的 结构体
  2. 结构体指针访问字段的标准方式应该是:(*结构体指针).字段名,比如(*persion).Name = “tom”
  3. 但go做了一个简化,也支持 结构体.字段名,比如 persion.Name = “tom”。更加符合程序员使用的习惯,go编译器底层 对 persion.Name 做了转化 (*persion).Name

接口

结节:

  1. 接口本身不能创建实例,但是可以指向一个实现了该接口的自定义类型的变量
package main

import (
	"fmt"
)

type AInterface interface {
	Say()
}

type Stu struct {
	Name string
}

func (str Stu) Say() {
	fmt.Println("Stu Say")
}
func main() {
	var stu Stu
	var a AInterface = stu
	a.Say()
}
  1. 接口中所有的方法都没有就去体即都没有实现的方法

  2. 在Go中,一个自定义类型需要将某个接口的所有方法都实现,我们说这个自定义类型实现了该接口

  3. 一个自定义类型只有实现了某个接口,才能将该自定义类型的实例(变量)赋给接口类型

  4. 只要是自定义数据类型,就可以实现接口,不仅仅是结构体类型

    type integer int
    
    func (i integer) Say(){
    	fmt.Println("wdm.life \n i = ",i)
    }
    
    var i ineger = 10
    var b AInterface = i
    b.Sa()
    
  5. 一个自定义类型可以实现多个接口

    type AInterface interface {
    	Say
    }
    
    type BInterface interface {
    	Hello()
    }
    
    type Monster struct {
    
    }
    
    func (m Monster) Say() {
    	fmt.Println("Sat~~wdm.life")
    }
    
    func (m Monster) Hello() {
    	fmt.Println("Hello~~wdm.life")
    }
    
    var monster Monster
    var a2 AInterface = monster
    var b2 Ainterface = monster
    a2.Say()
    b2.Hello()
    
  6. Go接口中不能有任何变量

    type AInterface interface {
    	Name string //这个是不正确的,不允许这个
    	Test01()
    	Test02()
    }
    
  7. 一个接口(比如A接口)可以继承多个别的接口(比如B、C接口),这时如果要实现A接口,也必须B,C接口的方法也全部实现

    package main
    
    type BInterface interface {
    	test01()
    }
    
    type CInterface interface {
    	test02()
    }
    
    type AInterface interface {
    	BInterface()
    	CInterface()
    	test03()
    }
    type Stu struct {
    }
    
    func (stu Stu) test01() {
    
    }
    func (stu Stu) test02() {
    
    }
    func (stu Stu) test03() {
    
    }
    
    func main() {
    	var stu Stu
    	var a AInterface = stu
    	a.test02()
    }
    
  8. interface odfa默认是一个指针(引用类型),如果没有对interface初始化就使用,那么会输出nil

  9. 空接口interface{}没有任何方法,所以擾类型都实现了空接口,即我们可以把任何一个变量赋给空接口。

    type T interface{
    }
    

实践

package main

import (
	"fmt"
	"math/rand"
	"sort"
)

// 1 声明Hero结构体
type Hero struct {
	Name string
	Age  int
}

// 2 声明一个Hero结构体切片类型
type HeroSlice []Hero

// 3 实现Interface 接口
func (hs HeroSlice) Len() int {
	return len(hs)
}

// Less 方法就是决定你使用什么标准进行排序
// 1 按Hero 的年龄从小到大排序
func (hs HeroSlice) Less(i, j int) bool {
	return hs[i].Age < hs[j].Age
	// 修改成对Name排序
	// return hs[i].Name < hs[j].Name
}

func (hs HeroSlice) Swap(i, j int) {
	// 交换
	// temp := hs[i]
	// hs[i] = hs[j]
	// hs[j] = temp
	// 下面的一句话等价于上面三句话
	hs[i], hs[j] = hs[j], hs[i]
}

// 1 声明 Student 结构体
type Student struct {
	Name  string
	Age   int
	Socre float64
}

// 将 Student 的切片,按 Score 从大到小排序
func main() {
	// 先定义一个数组/切片
	var intSlice = []int{0. - 1, 10, 7, 90}
	// 要求对 intSlice 臼进行排序
	// 1 冒泡排序……
	// 2 也可以用系统提供的方法
	sort.Ints(intSlice)
	fmt.Println(intSlice)

	var heroes HeroSlice
	for i := 0; i < 10; i++ {
		hero := Hero{
			Name: fmt.Sprintf("英雄|%d", rand.Intn(100)),
			Age:  rand.Intn(100),
		}
		heroes = append(heroes, hero)
	}

	for _, v := range heroes {
		fmt.Println(v)
	}

	sort.Sort(heroes)
	fmt.Println("***************排序后***************")
	for _, v := range heroes {
		fmt.Println(v)
	}

	i := 10
	j := 20
	i, j = j, i
	fmt.Println("i=", i, "j=", j)
}

接口与继承的不同之处

  1. 继承的价值主要在于:解决代码的复用性和可维护性
  2. 接口的价值主要在于:设计,设计好各种规范,让其经自定义类型去实现这些方法
  3. 接口比继承更加灵活;继承是满足 is - a 的关系,而接只需满足 like - a 的关系
  4. 接口在一定程度上实现代码解耦

Goroutine

基本介绍

  1. 进程和线程
    1. 进程就是程序在操作系统中的一次执行过程,是系统进行资源分配和调度的基本单位
    2. 线程是进程的一个执行实例,是程序执行的最小单位,它是比进程更小的能独立运行的基本单位
    3. 一个进程可以创建和销毁多个线程,同一个进程中的多个线程可以并发执行
    4. 一个程序至少有一个进程,一个进程至少有一个线程
  2. 程序、进程和线程的关系
  3. 并发和并行
    1. 多线程程序在单核上运行,就是并发
    2. 多线程程序在多核上运行,就是并行

协程和主线程

一个主线程上可以起多个协程,协程是轻量级的线程[编译器做优化]

协程特点

  1. 有独立的栈空间
  2. 共享程序堆空间
  3. 高度由用户控制
  4. 因程是轻量级的线程

案例

package main

import (
	"fmt"
	"strconv"
	"time"
)

func test() {
	for i := 1; i <= 10; i++ {
		fmt.Println("test () hello,world " + strconv.Itoa(i))
		time.Sleep(time.Second)
	}
}
func main() {
	// 开启了一个协程
	go test()

	for i := 1; i <= 10; i++ {
		fmt.Println("main () hello,golang " + strconv.Itoa(i))
		time.Sleep(time.Second)
	}
}

小结:

  1. 主线程是一个物理线程,直接作用在CPU上。是重量级的,非常耗费CPU资源
  2. 协程从主线程开启的,是轻量级的线程,是逻辑态。对资源消耗相对小
  3. go的协程机制是重要的特点,可以轻松的开启上万个协程。其它编程语言的并发机制一般是基于线程的,开启过多的线程,资源耗费大,这里就突显golang在并发上的优势了

调度模型

  1. MPG
    1. M:为操作系统的主线程(物理线程)
    2. P:协程执行需要的上下文
    3. G:协程

设置Go运行的cpu数

**介绍:**为了充分的利用多CPU的优势,在Go程序中,设置运行的CPU数目

package main

import (
	"fmt"
	"runtime"
)

func main() {
	// 获取当前系统的CPU数量
	cpuNum := runtime.NumCPU()
	// 设置num-1个CPU运行go程序
	runtime.GOMAXPROCS(cpuNum)
	fmt.Println("cpuNum=", cpuNum)
}

说明:

  1. go1.8后,默认让程序运行在多个核上,可以不用设置了
  2. go1.8前,还是要设置一下,可以更高效的利用CPU

使用全局变量加锁同步改进程序

package main

import (
	"fmt"
	"sync"
	"time"
)

/*需求:现在要计算1-200 的各个数的阶乘,并且把各个数的阶乘放入到map中。最后显示出来。
要求使用goroutine完成*/

//需求:现在要计算1-200的各个数的阶乘,并且把各个数的阶乘放入到map中。.
//最后显示出来。要求使用goroutine 完成
//思路
//1.编写一个函数,来计算各个数的阶乘,并放入到map中.
//2.我们启动的协程多个,统计的将结果放入到map中
//3.map应该做出一个全局的.
var (
	myMap = make(map[int]int, 10)
	// 声明一个全局的互斥锁
	// lock 是一个全局的互斥锁
	// sync 是包:synchornized 同步
	// Mutex :是互斥
	lock sync.Mutex
)

// test 计算n!,将这个结果放入到myMap
func test(n int) {
	res := 1
	for i := 1; i <= n; i++ {
		res *= i
	}
	// 将res放入myMap
	// 加锁
	lock.Lock()
	myMap[n] = res
	// 解锁
	lock.Unlock()
}

func main() {
	// 开启多个协程完成这个
	for i := 1; i <= 200; i++ {
		go test(i)
	}
	// 休眠 10 秒钟
	time.Sleep(time.Second * 15)

	// 这里我们输出结果,遍历这个结果
	for i, v := range myMap {
		fmt.Printf("map[%d]=%d\n", i, v)
	}
}

channel

介绍:

  1. channel本质就是一个数据结构-队列
  2. 数据是先进先出【FIFO:first in first out】
  3. 线程安全,多goroutine访问时,不需要加锁,就是说channel本身就是线程安全的
  4. channel有类型的,一个string的channel只能存放string类型数据

定义/声明

  • var 变量名 chan 数据类型;如:

    var intChan chan int(intChan 用于存放int数据)

    var mapChan chan map[int]string(mapChan用于存放map[int]string类型)

    var perChan chan Person

    var PerChan2 chan *Person

  • 说明:

    1. channe是引用类型
    2. channel必须初始化才能写入数据,即make后才能使用
    3. 管道是有类型的,intChan 只能写入 整数 int
  • 案例

    package main
    
    import "fmt"
    
    func main() {
    	// 1 创建一个可以存放3个int类型的管道
    	var intChan chan int
    	intChan = make(chan int, 3)
    	// 2 看看intChan是什么
    	fmt.Printf("intChan 的w值=%v intChan 本身的地址=%p\n", intChan, &intChan)
    	// 3 向管道写入数据
    	intChan <- 10
    	num := 211
    	intChan <- num
    	intChan <- 50
    	// intChan<-98注意点,当我们给管道攷数据时,不能超过其容量
    	// 4 看看管道的长度和 cap 容量
    	fmt.Printf("channel len=%v cap=%v \n", len(intChan), cap(intChan))
    	//  5 从管道中读取数据
    	var num2 int
    	num2 = <-intChan
    	fmt.Println("num2=", num2)
    	fmt.Printf("channel len=%v cap=%v \n", len(intChan), cap(intChan))
    	// 6 在没有使用协程的情况下,如果我们的管道数据已经全部取出,再取出就会报告deadlock
    
    	num3 := <-intChan
    	num4 := <-intChan
    	num5 := <-intChan
    	fmt.Println("num3=", num3, "num4=", num4, "num5=", num5)
    
    }
    

    读写演示:

    1. 创建一个intChan最多可存放3个int

      package main
      
      import (
      	"fmt"
      )
      
      func main() {
      	var intChan chan int
      	intChan = make(chan int, 3)
      	intChan <- 10
      	intChan <- 20
      	intChan <- 30
      	// 因为intChan的容量为3,再存放会报告deadlock
      	// intChan <- 50
      	num1 := <-intChan
      	num2 := <-intChan
      	num3 := <-intChan
      	// 因为intChan的已经没有了,再取会报告deadlock
      	fmt.Printf("num1=%v  num2=%v  num3=%v ", num1, num2, num3)
      }
      
    2. 创建一个mapChan,最多可存放10个map[string]string的key-value

      package main
      
      func main() {
      	var mapChan chan map[string]string
      	mapChan = make(chan map[string]string, 10)
      	m1 := make(map[string]string, 20)
      
      	m1["city1"] = "北京"
      	m1["city2"] = "广州"
      
      	m2 := make(map[string]string, 20)
      	m2["hero01"] = "老王"
      	m2["hero01"] = "老李"
      
      	mapChan <- m1
      	mapChan <- m2
      
      }
      

遍历和关闭

使用内置函数close可以关闭channel,当channel关闭后就不能再向channel写数据了,但是仍然可以从该channel读取数据

package main

import "fmt"

func main() {
	intChan := make(chan int, 3)
	intChan <- 100
	intChan <- 200
	close(intChan)
	// 这个时候不能够再写入数据到channel
	// intChan <-300
	fmt.Println("---------------")
	// 当管道关闭后,读取数据是可以的
	n1 := <-intChan
	fmt.Println("n1=", n1)
}

遍历

channel 支持 for-range 的方式进行遍历,请注两个细节:

  1. 在遍历时,如果channel没有关闭,则会出现deadlock的错误

  2. 在遍历时,如果channel已经关闭,则会出现遍历数据,遍历完后,就会退出遍历

    package main
    
    import "fmt"
    
    func main() {
    	intChan := make(chan int, 100)
    	for i := 0; i < 100; i++ {
    		intChan <- i * 2
    	}
    	// 1. 在遍历时,如果channel没有关闭,则会出现deadlock的错误
    	// 2. 在遍历时,如果channel已经关闭,则会出现遍历数据,遍历完后,就会退出遍历
    	close(intChan)
    	for v := range intChan {
    		fmt.Println("v=", v)
    	}
    }
    

应用实例:

  • 完成goroutine和channel协同工作的案例,要求如下:
  1. 开启一个writeData协程,向ptutintChan中写入50个整数

  2. 开启一个readData协程,从管道intChan中读取witeData写入的数据

  3. 注意:writeData和readData操作的是同一个管道

  4. 主线程需要等待writeData和readDate协程都完成工作才能退出【管道】

    package main
    
    import "fmt"
    
    // writeData
    func writeData(intChan chan int) {
    	for i := 1; i <= 50; i++ {
    		// 写入管道
    		intChan <- i
    		fmt.Printf("writeData 写入数据=%v\n", i)
    	}
    }
    
    // readData
    func readData(intChan chan int, exitChan chan bool) {
    	for {
    		v, ok := <-intChan
    		if !ok {
    			break
    		}
    		fmt.Printf("readData 读到数据=%v\n", v)
    	}
    	exitChan <- true
    	close(exitChan)
    }
    func main() {
    	// 创建两个管道
    	intChan := make(chan int, 50)
    	exitChan := make(chan bool, 1)
    	go writeData(intChan)
    	// go readData(intChan, exitChan)
    	for {
    		_, ok := <-exitChan
    		if !ok {
    			break
    		}
    	}
    }
    
  • 要求统计1-8000数字中,哪些是素数?(用goroutine和channel)

    package main
    
    import (
    	"fmt"
    	"time"
    )
    
    func putNum(intChan chan int) {
    	for i := 1; i <= 8000; i++ {
    		intChan <- i
    	}
    	// 关闭intChan
    	close(intChan)
    }
    
    // 从intChan取出数据,并判断是否为素数,如果是就放入primeChan
    func primeNum(intChan chan int, primeChan chan int, exitChan chan bool) {
    	// 使用for循环
    	// var num int
    	var flag bool
    	for {
    		time.Sleep(time.Millisecond * 10)
    		num, ok := <-intChan
    		if !ok { //取不到数据
    			break
    		}
    		flag = true //假设是素数
    		// 判断num是不是素数
    		for i := 2; i < num; i++ {
    			if num%i == 0 { //说明该num不是素数
    				flag = false
    				break
    			}
    		}
    
    		if flag {
    			// 将这个数放入primeChan
    			primeChan <- num
    		}
    	}
    
    	fmt.Println(("有一个primeNum 协程因为取不到数据,退出"))
    	// 这里我们还不能关闭 primeChan
    	// 向exitChan 写入
    	exitChan <- true
    
    }
    
    func main() {
    	start := time.Now()
    	intChan := make(chan int, 1000)
    	// 放入结果
    	primeChan := make(chan int, 2000)
    	// 标识退出的管道
    	exitChan := make(chan bool, 4) //4个
    
    	// 开启一个协程,向intChan放入 1-8000个数
    	go putNum(intChan)
    	// 开启4个协程,从intChan取出数据,并判断是否为素数	如果是就放入primeChan
    	for i := 0; i < 4; i++ {
    		go primeNum(intChan, primeChan, exitChan)
    	}
    
    	// 这里我们用主线程,直接进行处理
    	go func() {
    		for i := 0; i < 4; i++ {
    			<-exitChan
    		}
    		close(primeChan)
    	}()
    
    	// 遍历 primeChan,把结果取出
    	for {
    		res, ok := <-primeChan
    		if !ok {
    			break
    		}
    		fmt.Printf("素数=%d\n", res)
    	}
    	fmt.Println("main 线程退出")
    
    	tc := time.Since(start)
    	fmt.Println("本次耗时为:", tc)
    }
    

    注意事项

    1. channel可以声明为只读,或者只写性质

      package main
      
      import "fmt"
      
      func main() {
      	// 默认情况下是双向的
      	// chan int 这样声明是双向的
      	// 只写
      	var chan2 chan<- int
      	chan2 = make(chan int, 3)
      	chan2 <- 20
      
      	fmt.Println("chan2=", chan2)
      
      	// 只读
      	var chan3 <-chan int
      	num := <-chan3
      	fmt.Println("num=", num)
      }
      
    2. 使用select可以解决从管道取数据的阻塞问题

      package main
      
      import (
      	"fmt"
      	"time"
      )
      
      func main() {
      	// 1 定义一个管道 10 个数据 int
      	intChan := make(chan int, 10)
      	for i := 0; i < 10; i++ {
      		intChan <- i
      	}
      
      	// 2 定义一个管道 5 个数据 string
      	stringChan := make(chan string, 5)
      	for i := 0; i < 5; i++ {
      		stringChan <- "hello" + fmt.Sprintf("%d", i)
      	}
      
      	// 传统的方法在遍历管道时,如果不关闭会阻塞而导致 deadlock
      	// 在实际的开发中,可能我们不好确定什么时候关闭管道
      	// 用select 解决
      	for {
      		select {
      		// 注意:这里,如果intChan一直没有关闭,不会一直阻塞而deadlock
      		// 会自动到下一个case匹配
      		case v := <-intChan:
      			fmt.Printf("从intChan读取的数据%d \n", v)
      			time.Sleep(time.Second)
      		case v := <-stringChan:
      			fmt.Printf("从stringChan读取的数据%d \n", v)
      			time.Sleep(time.Second)
      		default:
      			fmt.Printf("都取不到了,不玩了,程序员可以加入逻辑 \n")
      			time.Sleep(time.Second)
      			return
      			//break labe
      		}
      	}
      }
      
    3. goroutine中使用recover,解决协程中出现panic,导致程序崩溃问题

      package main
      
      import (
      	"fmt"
      	"time"
      )
      
      func sayHello() {
      	for i := 0; i < 10; i++ {
      		time.Sleep(time.Second)
      		fmt.Println("hello,world")
      	}
      }
      
      // 函数
      func test() {
      	// 这里我们可以使用defer+recover
      	defer func() {
      		// 捕获 test 抛出的的panic
      		if err := recover(); err != nil {
      			fmt.Println("test() 发生错误", err)
      		}
      	}()
      	//  定义一个map
      	var myMap map[int]string
      	myMap[0] = "golang" //error
      }
      
      func main() {
      	go sayHello()
      	go test()
      
      	for i := 0; i < 10; i++ {
      		fmt.Println("main() ok=", i)
      		time.Sleep(time.Second)
      	}
      }
      

反射

介绍

  1. 反射可以在运行时动态 获取变量的各种信息,比如变量的类型(type),类别(kind)
  2. 如果是结构体变量,还可以获取到结构体本身的信息(包括结构体的字段方法)
  3. 通过反射,可以修改变量的值,可以调用关联的方法

重要的函数和概念

  1. reflect.TypoOf(变量名),获取变量的类型,返回reflect.Type类型
  2. reflect。ValueOf(变量名)获取变量的值,返回reflect.Value类型reflect.Value是一个结构体类型。通过reflect.Value,可以获取到关于该变量的很多信息
  3. 变量、interface{} 和 reflect.Value 是可以相互转换的,这点在实际开发中会经常使用到

案例:

  1. 对基本数据类型、interface{}、reflect.Value 进行反射的基本操作

    package main
    
    import (
    	"fmt"
    	"reflect"
    )
    
    func reflectTest01(b interface{}) {
    	// 通过反射获取的传入的变量的type,kind 值
    	// 1 先获取到reflect.Type
    	rTyp := reflect.TypeOf(b)
    	fmt.Println("rType=", rTyp)
    
    	// 2 获取到reflect.Value
    	rVal := reflect.ValueOf(b)
    
    	n2 := 2 + rVal.Int()
    	fmt.Println("n2=", n2)
    
    	fmt.Printf("rVal=%v rVal type=%T\n", rVal, rVal)
    
    	// 下面我们将 rVal转换成 interface{}
    	iV := rVal.Interface()
    	// 将 interface{}通过断言转成需要的类型
    	num2 := iV.(int)
    	fmt.Println("num2=", num2)
    }
    
    func reflectTest02(b interface{}) {
    	// 通过反射获取的传入的变量的type,kind 值
    	// 1 先获取到reflect.Type
    	rTyp := reflect.TypeOf(b)
    	fmt.Println("rType=", rTyp)
    
    	// 2 获取到reflect.Value
    	rVal := reflect.ValueOf(b)
    
    	// 下面我们将 rVal转换成 interface{}
    	iV := rVal.Interface()
    	fmt.Printf("iv=%v iv type=%T\n", iV, iV)
    	// 将 interface{}通过断言转成需要的类型断言
    	// 在这里我们可以用switch的断言形式来做更加的灵活
    	stu, ok := iV.(Student)
    	if ok {
    		fmt.Printf("stu.Name=%v\n", stu.Name)
    	}
    }
    
    type Student struct {
    	Name string
    	Age  int
    }
    
    type Monster struct {
    	Name string
    	Age  int
    }
    
    func main() {
    	// 1. 先定义一个int
    	// var num int = 100
    	// reflectTest01(num)
    
    	// 2 定义一个Student的实例
    	stu := Student{
    		Name: "wdm",
    		Age:  25,
    	}
    	reflectTest02(stu)
    }
    

注意事项和细节

  1. reflect.Value.Kind,获取变量的类别,返回的是一个常量

  2. Type和Kind的区别

    Type是类型,Kind是类别,Type和Kind可能是相同的也可能是不同的。如:

    1. var num int = 10 num的Type是int,Kind也是int
    2. var stu Student stu的Type是 pak1.Student,Kind是struct
  3. 通过反射可以再让变量在interface{}和Reflect.Value之间相互转换

  4. 使用反射的方式获取变量的值(并返回对应的类型),要求数据类型匹配,比如x是int,那么就应该使用reflect.Value(x).Int(),而不能使用其它的,否则会报panic

  5. 通过反射来修改变量,注意当使用SetXxx方法来设置需要通过对应的指针类型来完成,这样才能改变传入的变量值,同时需要使用到reflect.Value.Elem()方法

  6. reflect.Value.Elem()

网络编程

介绍

  1. TCP socket编程是网络编程的主流。之所以叫TCP socket编程是因为底层是基于TCP/IP协议的。
  2. b/s结构的http编程,我们使用浏览器去访问服务器时,使用的就是http协议,而http底层依旧是用TCP socket实现的。

案例:

server:

package main

import (
	"fmt"
	"net"
)

func proces(conn net.Conn) {
	// 循环接收客户端发送的数据
	defer conn.Close() //关闭conn
	for {
		// 创建一个新的切片
		buf := make([]byte, 1024)
		// conn.Read(buf)
		// 1 等待客户端通过conn发送信息
		// 2 如果客户端没有wite[发送],那么协程就阻塞在这里
		fmt.Printf("服务器在等待客户端%s 发送信息\n", conn.RemoteAddr().String())
		n, err := conn.Read(buf) //从conn读取
		if err != nil {
			fmt.Printf("客户端退出 err=%v", err)
			return
		}
		// 3 显示客户端发送的内容到服务器的终端
		fmt.Print(string(buf[:n]))
	}
}

func main() {
	fmt.Println("服务端开始监听……")
	// net.Listen("tcp","0.0.0.0:8888")
	// 1 tcp表示使用的网络协议是tcp
	// 2 0.0.0.0:8888 表示在本地监听 8888 端口
	listen, err := net.Listen("tcp", "0.0.0.0:8888")
	if err != nil {
		fmt.Println("listen err=", err)
		return
	}
	defer listen.Close() //延迟关闭 listen

	// 循环等待客户端来连接我
	for {
		fmt.Println("等待客户端来连接……")
		conn, err := listen.Accept()
		if err != nil {
			fmt.Println("Accept() err=", err)
		} else {
			fmt.Printf("Accept() suc con=%v 客户端ip=%v\n", conn, conn.RemoteAddr())
		}
		// 准备一个协程,为客户端服务
		go proces(conn)
	}

}

client:

package main

import (
	"bufio"
	"fmt"
	"net"
	"os"
)

func main() {
	conn, err := net.Dial("tcp", "192.168.1.188:8888")
	if err != nil {
		fmt.Println("client dial err=", err)
		return
	}
	// 功能1 客户端可以发送单行数据,然后就退出
	reader := bufio.NewReader(os.Stdin) //os.Stdin 代表终端输入
	for {
		// 功能1 客户端可以发送单行数据,然后就退出
		line, err := reader.ReadString('\n')
		if err != nil {
			fmt.Println("readString err=", err)
		}

		if line == "exit" {
			fmt.Printf("客户端退出")
			break
		}
		// 再将line发送给 服务器
		_, err = conn.Write([]byte(line + "\n"))
		if err != nil {
			fmt.Println("con.Write err=", err)
		}
	}

}

特别说明

  1. go明确不支持一三元运算符
  2. 没有—a++a,只有a—a++
  3. 嵌套分支不宜过多,建议控制在3层内
  4. 在switch中fallthrough 默认只能穿透一层

原码、反码、补码

  1. 二进制的最高位是符号位: 0表示正数,1表示负数
  2. 正数的原码,反码,补码都一样
  3. 负数的反码=它的原码符号位不变,其它位取反(0->1,1->0)
  4. 负数的补码=它的反码+1
  5. 0的反码,补码都是0
  6. 在计算机运算的时候,都是以补码的方式来运算的

Hello World

//包名
package main

//导入包
import (
	"fmt"
)
//程序运行主函数
func main() {
  fmt.Println("Hello World")
}

执行命令,编译并运行程序

$ go run main.go
Hello World

注释

  1. 行注释
//这是行注释
  1. 块注释
/*
这是块注释
*/

打印金字塔

package main

import "fmt"

func main() {

	var totalLevel int = 10
	// i表示当前层数
	for i := 1; i <= totalLevel; i++ {
		// 打印行前空格
		for k := 1; k <= totalLevel-i; k++ {
			fmt.Print(" ")
		}
		//每行打印的*
		for j := 1; j <= 2*i-1; j++ {
			if j == 1 || j == 2*i-1 || i == totalLevel {
				fmt.Print("*")
			} else {
				fmt.Print(" ")
			}
		}
		fmt.Println()
	}

}

斐归那契数

package main

import "fmt"

func main() {
	fmt.Println("斐归那契数", fib(10))
}
func fib(n int) int {
	if n == 1 || n == 2 {
		return 1
	} else {
		return fib(n-1) + fib(n-2)
	}
}

9*9=81

package main

import (
	"fmt"
)

func main() {
	var num int = 9
	for i := 1; i <= num; i++ {
		for j := 1; j <= i; j++ {
			fmt.Printf("%v * %v = %v \t", j, i, j*i)
		}
		fmt.Println()
	}
}

Test

测试文件(xxx_test.go)必须与测试文件在同一目录下

package model

import (
	"fmt"
	"testing"
)
//在测试函数执行之前先运行,可以干自己想干的事情
func TestMain(m *testing.M) {
	fmt.Println("测试开始了")
	//	通过m.run()来执行子测试函数
	m.Run()
}
//测试用户相关函数
func TestUser(t *testing.T) {
   fmt.Println("开始测试了")
   //通过t.run来执行子测试函数
   t.Run("测试用户相关的方法", testAddUser)
}

//小写test开头 那么函数不会被执行的, 我们可以将它设置为一个子测试函数
func testAddUser(t *testing.T) {
   fmt.Println("子测试函数")
   //调用的具体方法
}

执行测试命令

$ go test
测试开始了
开始测试了
测试添加用户
PASS
ok 
//显示详细测试信息命令
$ go test -v 

MYSQL

utils包

package utils

import (
	"database/sql"
	_ "github.com/go-sql-driver/mysql"
	"strings"
)

/*
定义常量 隐
数据库配置
*/
const (
	userName = "root"
	password = "123456"
	ip       = "127.0.0.1"
	port     = "3306"
	dbName   = "vip"
	//设置数据库最大连接数
	connMax = 100
	//设置上数据库最大闲置连接数
	MaxIdleConns = 10
)

//Db数据库连接池
var (
	Db  *sql.DB
	err error
)

func init() {
	//构建连接:"用户名:密码@tcp(IP:端口)/数据库?charset=utf8"
	path := strings.Join([]string{userName, ":", password, "@tcp(", ip, ":", port, ")/", dbName, "?charset=utf8"}, "")
	//导入mysql 数据库驱动: _ "github.com/go-sql-driver/mysql"
	Db, err = sql.Open("mysql", path)
	Db.SetConnMaxLifetime(connMax)
	Db.SetMaxIdleConns(MaxIdleConns)
	//验证连接
	if err != nil {
		panic(err.Error())
	}
}

查询

  1. 判断是查询结果是否为空

    	sqlstr := "SELECT ID FROM vipInfo WHERE Name=?"
    	row := utils.Db.QueryRow(sqlstr, name)
    	var ID int
    	err := row.Scan(&ID)
    	if err == sql.ErrNoRows {
    		return false
    	} else {
    		return true
    	}
    
  2. 查询所有符合条件的记录

    //GetAllAdminInfo 获取所有用户信息
    func GetAllAdminInfo() ([]*model.AdminInfo, error) {
    	sqlstr := "SELECT id, username, sex, mobile, cretime, lastlogin FROM adminInfo WHERE isDelete=0"
    	rows, err := utils.Db.Query(sqlstr)
    	if err != nil {
    		return nil, err
    	}
    	var admins []*model.AdminInfo
    	for rows.Next() {
    		admin := &model.AdminInfo{}
    		rows.Scan(&admin.ID, &admin.Username, &admin.Sex, &admin.Mobile, &admin.CreTime, &admin.LastLogin)
    		admins = append(admins, admin)
    	}
    	return admins, nil
    }
    
  3. 分页查询

    
    

更新

//UpdateAdminPass 修改密码
func UpdateAdminPassword(password string, ID int) error {
	//写sql语句
	sqlStr := "UPDATE adminInfo SET Password=? WHERE ID=?;"
	//执行
	_, err := utils.Db.Exec(sqlStr, password, ID)
	if err != nil {
		return err
	} else {
		return nil
	}
}

添加

//AddAdmin 添加用户
func AddAdmin(admin *model.AdminInfo) error {
	//写sql语句
	sqlStr := "INSERT INTO adminInfo(username, password, sex, mobile) VALUE ( ?, ?, ?, ?)"
	//执行
	_, err := utils.Db.Exec(sqlStr, admin.Username, admin.Password, admin.Sex, admin.Mobile)
	if err != nil {
		return err
	}
	return nil
}
  1. 创建
import "net/http"

// setCookie 添加Cookie
func setCookie(w http.ResponseWriter, r *http.Request) {
	// 创建Cookie
	cookie := http.Cookie{
		Name:     "user",
		Value:    "admin",
		HttpOnly: true,
	}
	// 将Cookie发送给浏览器
  //方式1
	// w.Header().Set("Set-Cookie", cookie.String())
	// 添加第二个Cookie
	// w.Header().Add("Set-Cookie", cookie2.String())
	
  //方式2
  //直接调用http的SetCookie函数设置Cookie
	http.SetCookie(w, &cookie)
}
  1. 获取
// setCookie 添加Cookie
func setCookie(w http.ResponseWriter, r *http.Request) {
	// 创建Cookie
	cookie := http.Cookie{
		Name:     "user",
		Value:    "admin",
		HttpOnly: true,
		MaxAge:   2  0,
	}
	cookie2 := http.Cookie{
		Name:     "user2",
		Value:    "admin2",
		HttpOnly: true,
		MaxAge:   60,
	}
	// 将Cookie发送给浏览器
  //方式1
	// w.Header().Set("Set-Cookie", cookie.String())
	// 添加第二个Cookie
	// w.Header().Add("Set-Cookie", cookie2.String())
  //方式2
	//直接调用http的SetCookie函数设置Cookie
	http.SetCookie(w, &cookie)
	http.SetCookie(w, &cookie2)
}

Session

			//用户名和密码正确
			//生成UUID作为Session的id
			uuid := utils.CreateUUID()
			//创建一个Session
			sess := &model.Session{
				SessionID: uuid,
				UserName:  admin.Username,
				AdminID:   admin.ID,
			}
			//将Session保存到数据库中
			dao.AddSession(sess)
			//创建一个Cookie,让它与Session相关联
			cookie := http.Cookie{
				Name:     "user",
				Value:    uuid,
				HttpOnly: true,
			}
			//将cookie发送给浏览器
			http.SetCookie(w, &cookie)
			t := template.Must(template.ParseFiles("views/pages/admin/index.html"))
			t.Execute(w, "true")

Ajax

POST

<script>
    $(function () {
        //发送Ajax请求验证当前密码是否正确
        $("#oldpwd").change(function () {
            // var oldpwd = $(this).val();
            var oldpwd = $("#oldpwd").val();
            //设置请求地址
            var url = "/checkAdminOldPwd";
            //设置请求参数
            var param = {"oldpwd": oldpwd, "ID": 1};
            //发送Ajax请求
            $.post(url, param, function (res) {
              //res 后台返回值
                if(res=="true") {
                    $("#msg").text("")
                } else {
                    $("#msg").text("旧密码错误")
                }
            });
        });
    });
</script>
//controller

//CheckAdminOldPwd 验证旧密码是否正确
func CheckAdminOldPwd(w http.ResponseWriter, r *http.Request) {
	id := r.PostFormValue("ID")
	//类型转换
	idInt, _ := strconv.Atoi(id)
	oldpwd := r.PostFormValue("oldpwd")
	result := dao.CheckAdminPassword(oldpwd, idInt)
	if result {
		//当前密码正确
		w.Write([]byte("true"))
	} else {
		w.Write([]byte("false"))
	}
}

Jquery

性别转换

//方式1
<script>
	("{{.Sex}}" == "0") ? sex = "男" : sex = "女";
	document.write(sex);
</script>
//方式2
<script>
  switch ({{.Sex}}) {
    case 0:
    document.write("男");
    break;
    case 1:
    document.write("女");
    break;
    default:
    document.write("男");
  }
</script>
//方式3
<script>
  if ("{{.Sex}}" == "0") {
  document.write("男");
  }else{
  document.write("女");
  }
</script>

热加载调试Hot Reload

安装

go get -v -u github.com/pilu/fresh

安装好后,只需要将go run main.go命令换成fresh即可。每次更改源文件,代码将自动重新编译(Auto Compile)。

项目部署

编译

  1. MAC下编译为Linux和Windows 64位可执行文件
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build main.go
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build main.go
  1. Linux 下编译 Mac 和 Windows 64位可执行程序
CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build main.go
CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build main.go
  1. Windows 下编译 Mac 和 Linux 64位可执行程序
SET CGO_ENABLED=0
SET GOOS=darwin
SET GOARCH=amd64
go build main.go

SET CGO_ENABLED=0
SET GOOS=linux
SET GOARCH=amd64
go build main.go

GOOS:目标平台的操作系统(darwin、freebsd、linux、windows)

GOARCH:目标平台的体系架构(386、amd64、arm)

交叉编译不支持 CGO 所以要禁用它

运行

  1. Linux 后台运行
nohup ./main &

Dockerfile

# 镜像
FROM golang
# 创建工作工作目录
WORKDIR $GOPATH/src/golang
# 复制文件到到镜像指定目录
COPY . $GOPATH/src/golang
# 构建镜像
RUN go build .
# 暴露端口
EXPOSE 8001
# 程序入入口
ENTRYPOINT ["./golang"]