Go学习笔记
Version
$ go version
go version go1.14.2 darwin/amd64
编写规则:
-
文件后缀为.go
-
程序区分大小写
-
语句后,不需要带分号
-
定义的变量或import包,必须使用否则会报错
-
每一个语句一行,否则会报错
-
{}成对出现,而且风格为
func main(){ //code }
变量
声名语法:var 变量名 数据类型
备注:没有public、private等关键字来修饰,但可以简单的理解为首字母大写是公开的,首字母小写是私有的
-
指定变量类型,声明后若不赋值,使用默认值
package main import ( "fmt" ) func main(){ var i int fmt.Println("i的默认值为:",i) }
-
类型推导:根据值自行判定变量的类型
package main import ( "fmt" ) import ( "fmt" ) func main(){ var num =8.8 fmt.Println("num的值为:",num) }
-
省略var,注意新的变量名不能为已经声明过的变量,否则会编译错误。
package main import ( "fmt" ) func main(){ name := "wdm.life" fmt.Println("name的值为:",name) }
-
多变量声明
package main import "fmt" func main(){ name,sex,old := "wdm.life", "男", 25 fmt.Println("姓名:",name,"性别:",sex,"年龄:",old) }
-
全局变量声明则放在函数外面即可
-
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
-
双引号,会识别转义字符
-
反引号,以字符串原生形式输出,包括换行和特殊字符,可以实现防止攻击,输出源代码等效果
str:=` package main import "fmt" func main(){ //code } `
-
多行字符串
str:="name"+ "sex"
类型转换
基本语法:T(v) 将值v转换为类型T
var i int32 =182 var n2 float32=float32(i)
基本类型转string类型
- fmt.Sprintf(“%参数”, 表达式)
string转基本类型
使用strconv包函数
var str string="true" var b bool b,_=strconv.ParseBool(str)
指针
- 基本数据类型,变量存的就是值,也叫值类型
- 获取变量的地址,用&,比如: var num int,获取num的地址: &num分析一下基本数据类型在内存的布局.
- 指针类型,指针变量存的是一个地址,这个地址指向的空间存的才是值比如: var ptr *int = &num
- 获取指针类型所指向的值,使用: *,比如: var ptr int, 使用ptr获取ptr指向的值
值类型和引用类型
- 值类型:基本数据类型int 系列,float系列,bool, string、数组和结构体struct
- 引用类型:指针、slice 切片、map、 管道chan、 interface 等都是引用类型
特点
值类型:变量直接存储值,内存通常在栈中分配
引用类型:变量存储的是一个地址,这个地址对应的空间才真正存储数据(值),内存通常在堆上分配,当没有任何变量引用这个地址时,该地址对应的数据空间就成为一个垃圾,由GC来回收
包的使用
-
在给一个文件打包时,该包对应的一个文件夹,通常文件的包名跟文件所在的文件夹名一致,一般为小写字母
-
当一个文件要使用其它包函数或变量时,需要先引入对应的包
-
import "包名"
-
import ( "包名1" "包名2" )
-
package 指令在go文件的首行,然后是import指令
-
import包时,路径从$GOPATH的src下开始,不需要带src,编译器会自动从src下开始引入
-
-
为了让其它包的文件,可以访问到本包的函数,则该函数名的首字母要大写,类似其它语言的pubilc
-
在访问其它包函数或变量时,语法是:包名.函数名
-
如果包名较长,可以给包取别名。**注意:**取别名后,原来的包名就不能使用了。要使用别名来访问函数或变量
-
在同一个包下不能有相同的函数名、变量名,否则报重复定义
-
编译为可执行程序文件时,就需要将这个包声明为main,即package main。这个一个语法规范,如果你写一个库,包名可以自定义
函数使用的细节
-
函数的形参列表可以是多个,返回值列表也可以是多个。
-
形参列表和返回值列表的数据类型可以是值类型和引用类型。
-
函数的命名遵循标识符命名规范,首字母不能是数字,首字母大写该函数可以被本包文件和其它包文件使用,类似public,首字母小写,只能被本包文件使用,其它包文件不能使用,类似privat
-
函数中的变量是局部的,函数外不生效
-
基本数据类型和数组默认都是值传递,即进行值拷贝。在函数内修改,不会影响到原来的值
-
如果希望函数内的变量能修改函数外的亦是,可以通过传入变量的地址&,函数内以指针的方式操作变量
-
Go不支持函数重载
-
在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) }
-
函数既然是一种数据类型,因此在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 }
-
为了简化数据类型定义,Go支持自定义数据类型
基本语法为:type 自定义数据类型名 数据类型 //相当于一个别名
type myInt int
-
支持函数返回值命名
func getSumAndSub(n1 int,n2 int) (sum int,sub int){ sum = n1 + n2 sub = n1 - n2 return }
-
使用 _ 标识符,忽略返回值
-
支持可变参数
// 0~n个参数 func sum(args... int)sum int{ } // 1~n个参数 func sum(n1 int,args... int)sum int{ }
说明:
-
args是slice切片,通过args[index]可以访问到各个值
-
如果一个函数的形参中有可变参数,则可变参数需要放在在形参列表最后
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))
}
-
AddUpper 是一个函数,返回的数据类型是fun(int) int
-
闭包说明
var n int = 10 return func (x int) int{ n = n + x return n }
返回的是一个匿名函数,但是这个匿名函数引用到函数外的n,因此这个艾薇儿函数就和n形成一个整体,构成闭包
-
简单理解为:闭包是类,函数是操作,n是字段。函数和它使用到n构成闭包
-
当我们反复的调用f函数时,因为n是初始化一次,因此每骼一次不进行累计
-
我们要搞清楚闭包的关键,就是要分析出返回的函数它使用(引用)到哪些变量,因为函数和它引用到的变量共同构成闭包
闭包最佳实践
要求如下:
-
编写一个函数 makeSuffix(suffix string) 可以接收一个文件后缀名(比如.jpg),并返回一个闭包
-
调用闭包,可以传入一个文件名,如果该文件名没有指定的后缀(如.jpg),则返回 文件名.jpg,如果已经有.jpg后缀,则返回原文件名
-
要求使用闭包的方式完成
-
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 } }
上面代码的总结与说明:
- 返回的匿名函数和makeSuffix(suffix string) 的 suffix 变量 组合成一个闭包,因为返回的函数引用到suffix这个变量
- 我们体会一下闭包的好处,如果使用传统的方法,也可以轻松实现这个功能,但是传统方法需要每次都传入后缀名,比如.jpg,而闭包因为可以保留上次引用的某个值,所以我们传入一次就可以反复使用
defer函数
干啥用呢
在函数中,程序员经常需要创建资源(比如:数据库连接。文件句柄、锁等),为了在函数执行完毕后,及时的释放资源,Go的设计者提供defer(延时机制)
细节
-
当Go执行到一个defer时,不会立即执行defer后的语句,而是将defer后的语句压入到一个栈中
-
当函数执行完毕后,再从defer栈中,依次从栈顶取出语句执行(注:遵守栈的先入后出的机制)
-
在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 }
其它说明
- 创建资源后,比如打开了文件,获取了数据库的链接,或者是锁资源,可以执行defer fiel.close()defer connect.Close()
- 在defer后,可以继续使用创建资源
- 当函数执行完毕后,系统会依次从defer栈中,取出语句关闭资源
- 这种机制非常简洁,程序员不用再为在什么时机关闭资源而烦恼
函数参数传递方式
两种传递方式
- 值传递
- 引用传递
其实,不管是值传递还是引用传递,传递给函数的都是变量的副本,不同的是,值传递的是值的拷贝,引用传递的是地址的拷贝,一般来说,地址拷贝效率高,因为数据量小,而值拷贝决定拷贝的数据大小,数据越大,效率越低。
时间
方式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 //小时
)
内置函数
- len 用来求长度,如string、array、slice、map、channel
- new 用来分配内存,主要用来分配值类型,比如int、float32、struct……返回的是指针
- make 用来分配内存,主要用来分配引用类型,比如chnnel、map、slice、
错误处理
- 在默认的情况下,当发生错误后(panic),程序就会退出(崩溃)
- 我们希望当发生错误后,可以捕获到错误,并进行处理,保证程序可以继续执行。还可以在捕获到错误后给我们一个提示
自定义错误
- errors.New(“错误说明”),会返回一个error类型的值,表示一个错误
- 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)
}
数组使用的注意事项和细节
-
数组是多个相同类型数据的组合,一个数组一旦声明/定义了,其长度是固定的,不能动态变化
-
var arr []int 这个arr 就是一个slice切片
-
数组中的元素可以是任何数据类型,包括类型和引用类型,但是不能混用
-
数组创建后,如果没有赋值,有默认值(零值)
数值类型数组:0
字符串数组:“”
bool数组:false
-
使用数组的步骤:1. 声明数组并开辟空间 2. 给数组各个元素赋值 3. 使用数组
-
数组的下标是从0开始的
-
数组下标必须在指定范围内使用,否则报panic:数组越界,如:var arr[5]int 则有效下标为0-4
-
数组属值类型,在默认情况下是值传递,因此会进行值拷贝。数组间不会影响
-
如想在其它函数中,去修改原来的数组,可以使用引用传递(指针方式)
func test01(arr *[3]int){ (*arr)[0] = 88 }
-
长度是数组类型的一部分,在传递函数参数时需要考虑数组的长度
切片
基本介绍
-
切片的英文是slice
-
切片是数组的一个引用,因此切片是引用类型,在进行传递时,遵守引用传递的机制
-
切片的使用和数组类似,遍历切片、访问切片的元素和求切片长度len(slice)都一样
-
切片的长度是可以变化的,因此切片是一个可以动态变化数组
-
切片定义的基本语法:
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)) }
-
使用
-
定义一个切片,然后让切片去引用一个已经创建好的数组,比如:
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)) }
-
通过make来创建切片
基本语法:var 切片名 []type = make([]type,len,[cap])
参数说明:type:数据类型 len:大小 cap:指定切片容量,可选,如果你分配了cap,则要求vap>=len
- 通过make方式创建切片可以指定切片的大小和容量
- 如果没有给切片的各个元素赋值,那么就会使用默认值[int, float=>0 string=>”” bool=>false]
- 通过make方式创建的切片对应的数组是由make底层维护,对外不可见即只能通过slice去访问各个元素
-
定义一个切片,直接就指定具体数据,使用原理类似make的方式
1和2的区别:1是直接引用数组,这个数组是事先存在的程序员是可见的;2是通过make来创建切片,make也会创建一个数组,是由切片在底层进行维护,程序员是不见的
遍历
-
for循环是常规方式遍历
-
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) } }
切片使用的注意事项和细节讨论
-
切片初始化时 var slice = arr[startIndex:endIndex];说明:从arr数组下标为startIndex,取下下标为endIndex的元素(不含arr[endIdex])
-
切片初始化时,仍然不能越界。范围在[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[:]
-
cap是一个内置函数,用于统计切片的容量,即最大可以存放多少个元素
-
切片定义后,还不能使用,因为本身是个空的,需要让其引用到一个数组,或者make一个空间供切片来使用
-
切片可以继续切片
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) }
-
用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)
分析:
- 切片append操作的本质就是对数组扩容
- go底层会创建一个新的数组newArr(安装扩容后大小)
- 将slice原来包含的元素拷贝到新的数组newArr
- slice重要引用到newArr
-
切片的拷贝操作
var slice4 []int = []int{1,2,4,5} var slice5 = make([]int,10) copy(slice5,slice4) fmt.Println("slice4=",slice4) fmt.Println("slice5=",slice5)
- copy(para1,para2)参数的数据类型是切片
- 按照上面的代码来看,slice4和slice5的数据空间是独立,相互不影响,就是说slice4[0]=999,slice5[0]仍然是 1
-
拷贝注意事项
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]
-
切片是引用类型,所以在传递时,遵守引用传递机制。
案例:斐波那契的数列
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) }
-
排序与查找
基本介绍
排序是将一组数据,依指定的顺序进行排列的过程
分类
- 内部排序:指将要处理的所有数据都加载到内部存储器中进行排序,包括(交换式排序法、选择式排序法和插入式排序法)
- 外部排序法:数据量过大,无法全部加载到内存中,需要借助外部存储进行排序。包括(合并排序法和直接合并排序法)
冒泡排序
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)
}
查找
-
顺序查找
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) } } }
-
二分查找(该数组是有序)
/* 思路 查找的数是 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"]="武松"
说明:
- map 在使用前一定要make
- map 的 key 是不能重复,如果重复了,则以最后这个 key-value 为准
- map 的value 是可以相同的
- map 的key-value 是无序
- 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排序
-
go 中没有一个专门的方法针对map的key进行排序
-
go 中的map默认是无序的,注意也不是按照添加的顺序存放的,每次遍历输出的顺序可能不一样
-
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]) } }
细节:
- 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)
}
- map 的容量达到后,再想增加元素会自动扩容,并不会发生panic,也就说明map能动态的增长 键值对(key-value)
- mao 的 value 也经常使用struct类型,更适合管理复杂的数据(比前面value是一个map更好),比如 value 为Student的结构体
结构体
基本语法
type 结构体名称 struct{
field1 type
field2 type
}
案例
type Student struct{
Name string
Age int
Score float32
}
说明:结构体是值类型
创建与访问
- 直接声明
var persion Psersion
-
{}
var persion Persion = Persion{}
-
-&
var persion *Persion =new (Persion)
-
-{}
var persion *Persion = &Persion{}
说明:
- 3和4返回的 结构体
- 结构体指针访问字段的标准方式应该是:(*结构体指针).字段名,比如(*persion).Name = “tom”
- 但go做了一个简化,也支持 结构体.字段名,比如 persion.Name = “tom”。更加符合程序员使用的习惯,go编译器底层 对 persion.Name 做了转化 (*persion).Name
接口
结节:
- 接口本身不能创建实例,但是可以指向一个实现了该接口的自定义类型的变量
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()
}
-
接口中所有的方法都没有就去体即都没有实现的方法
-
在Go中,一个自定义类型需要将某个接口的所有方法都实现,我们说这个自定义类型实现了该接口
-
一个自定义类型只有实现了某个接口,才能将该自定义类型的实例(变量)赋给接口类型
-
只要是自定义数据类型,就可以实现接口,不仅仅是结构体类型
type integer int func (i integer) Say(){ fmt.Println("wdm.life \n i = ",i) }
var i ineger = 10 var b AInterface = i b.Sa()
-
一个自定义类型可以实现多个接口
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()
-
Go接口中不能有任何变量
type AInterface interface { Name string //这个是不正确的,不允许这个 Test01() Test02() }
-
一个接口(比如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() }
-
interface odfa默认是一个指针(引用类型),如果没有对interface初始化就使用,那么会输出nil
-
空接口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)
}
接口与继承的不同之处
- 继承的价值主要在于:解决代码的复用性和可维护性
- 接口的价值主要在于:设计,设计好各种规范,让其经自定义类型去实现这些方法
- 接口比继承更加灵活;继承是满足 is - a 的关系,而接只需满足 like - a 的关系
- 接口在一定程度上实现代码解耦
Goroutine
基本介绍
- 进程和线程
- 进程就是程序在操作系统中的一次执行过程,是系统进行资源分配和调度的基本单位
- 线程是进程的一个执行实例,是程序执行的最小单位,它是比进程更小的能独立运行的基本单位
- 一个进程可以创建和销毁多个线程,同一个进程中的多个线程可以并发执行
- 一个程序至少有一个进程,一个进程至少有一个线程
- 程序、进程和线程的关系
- 并发和并行
- 多线程程序在单核上运行,就是并发
- 多线程程序在多核上运行,就是并行
协程和主线程
一个主线程上可以起多个协程,协程是轻量级的线程[编译器做优化]
协程特点
- 有独立的栈空间
- 共享程序堆空间
- 高度由用户控制
- 因程是轻量级的线程
案例
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)
}
}
小结:
- 主线程是一个物理线程,直接作用在CPU上。是重量级的,非常耗费CPU资源
- 协程从主线程开启的,是轻量级的线程,是逻辑态。对资源消耗相对小
- go的协程机制是重要的特点,可以轻松的开启上万个协程。其它编程语言的并发机制一般是基于线程的,开启过多的线程,资源耗费大,这里就突显golang在并发上的优势了
调度模型
- MPG
- M:为操作系统的主线程(物理线程)
- P:协程执行需要的上下文
- 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)
}
说明:
- go1.8后,默认让程序运行在多个核上,可以不用设置了
- 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
介绍:
- channel本质就是一个数据结构-队列
- 数据是先进先出【FIFO:first in first out】
- 线程安全,多goroutine访问时,不需要加锁,就是说channel本身就是线程安全的
- 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
-
说明:
- channe是引用类型
- channel必须初始化才能写入数据,即make后才能使用
- 管道是有类型的,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) }
读写演示:
-
创建一个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) }
-
创建一个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 的方式进行遍历,请注两个细节:
-
在遍历时,如果channel没有关闭,则会出现deadlock的错误
-
在遍历时,如果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协同工作的案例,要求如下:
-
开启一个writeData协程,向ptutintChan中写入50个整数
-
开启一个readData协程,从管道intChan中读取witeData写入的数据
-
注意:writeData和readData操作的是同一个管道
-
主线程需要等待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) }
注意事项
-
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) }
-
使用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 } } }
-
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) } }
-
反射
介绍
- 反射可以在运行时动态 获取变量的各种信息,比如变量的类型(type),类别(kind)
- 如果是结构体变量,还可以获取到结构体本身的信息(包括结构体的字段方法)
- 通过反射,可以修改变量的值,可以调用关联的方法
重要的函数和概念
- reflect.TypoOf(变量名),获取变量的类型,返回reflect.Type类型
- reflect。ValueOf(变量名)获取变量的值,返回reflect.Value类型reflect.Value是一个结构体类型。通过reflect.Value,可以获取到关于该变量的很多信息
- 变量、interface{} 和 reflect.Value 是可以相互转换的,这点在实际开发中会经常使用到
案例:
-
对基本数据类型、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) }
注意事项和细节
-
reflect.Value.Kind,获取变量的类别,返回的是一个常量
-
Type和Kind的区别
Type是类型,Kind是类别,Type和Kind可能是相同的也可能是不同的。如:
- var num int = 10 num的Type是int,Kind也是int
- var stu Student stu的Type是 pak1.Student,Kind是struct
-
通过反射可以再让变量在interface{}和Reflect.Value之间相互转换
-
使用反射的方式获取变量的值(并返回对应的类型),要求数据类型匹配,比如x是int,那么就应该使用reflect.Value(x).Int(),而不能使用其它的,否则会报panic
-
通过反射来修改变量,注意当使用SetXxx方法来设置需要通过对应的指针类型来完成,这样才能改变传入的变量值,同时需要使用到reflect.Value.Elem()方法
-
reflect.Value.Elem()
网络编程
介绍
- TCP socket编程是网络编程的主流。之所以叫TCP socket编程是因为底层是基于TCP/IP协议的。
- 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)
}
}
}
特别说明
- go明确不支持一三元运算符
- 没有—a++a,只有a—a++
- 嵌套分支不宜过多,建议控制在3层内
- 在switch中fallthrough 默认只能穿透一层
原码、反码、补码
- 二进制的最高位是符号位: 0表示正数,1表示负数
- 正数的原码,反码,补码都一样
- 负数的反码=它的原码符号位不变,其它位取反(0->1,1->0)
- 负数的补码=它的反码+1
- 0的反码,补码都是0
- 在计算机运算的时候,都是以补码的方式来运算的
Hello World
//包名
package main
//导入包
import (
"fmt"
)
//程序运行主函数
func main() {
fmt.Println("Hello World")
}
执行命令,编译并运行程序
$ go run main.go
Hello World
注释
- 行注释
//这是行注释
- 块注释
/*
这是块注释
*/
打印金字塔
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())
}
}
查询
-
判断是查询结果是否为空
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 }
-
查询所有符合条件的记录
//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 }
-
分页查询
更新
//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
}
Cookie
- 创建
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)
}
- 获取
// 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)。
项目部署
编译
- 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
- 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
- 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 所以要禁用它
运行
- Linux 后台运行
nohup ./main &
Dockerfile
# 镜像
FROM golang
# 创建工作工作目录
WORKDIR $GOPATH/src/golang
# 复制文件到到镜像指定目录
COPY . $GOPATH/src/golang
# 构建镜像
RUN go build .
# 暴露端口
EXPOSE 8001
# 程序入入口
ENTRYPOINT ["./golang"]