概述
Go起源
总而言之,go总结了很多厉害语言经验,由大佬们组队创造的,如图
go的hello
package main //包名,声明下面代码在哪个包里面,相同包里面内容可见,
import "fmt" //导入依赖名称,fmt包,包含了go用来输入输出的函数
func main() { //main函数,go项目的入口函数,
fmt.Println("Hello, 世界") //使用标准化输出函数显示内容在屏幕。
}
注
1,一般程序运行获取命令行参数,golang中在os.Args中,
其为slice,打印为fmt.Println(os.Args[1:]),os.Args[0]为程序名
2,格式化输出的,go是类c语言,输入也是可以用格式化输出的
fmt.Printf("%s\n",str)
%d 十进制整数
%x, %o, %b 十六进制,八进制,二进制整数。
%f, %g, %e 浮点数: 3.141593 3.141592653589793 3.141593e+00
%t 布尔:true或false
%c 字符(rune) (Unicode码点)
%s 字符串
%q 带双引号的字符串"abc"或带单引号的字符'c'
%v 变量的自然形式(natural format)
%T 变量的类型
%% 字面上的百分号标志(无操作数)
go的协程
func waitm(msg string) {
for i := 0; ; i++ { //开启无限循环
fmt.Println(msg, i) //输出传进来的msg
time.Sleep(time.Duration(rand.Intn(1e3)) * time.Millisecond
} //rand.Intn(1e3)随机数,0到1e3即1000,睡眠随机0到1秒
}
func main() {
go waitm("work!") //开启协程
fmt.Println("start")
time.Sleep(2 * time.Second) //主进程睡眠两秒,time包里面的时间常量
fmt.Println("end") //伴随主进程结束,go开启的协程也全部结束,
}
/*
start
work! 0
work! 1
work! 2
work! 3
work! 4
end
*/
go关键字即开启一个协程,和协程与主线程并发执行,并发执行的控制就要结合管道chan,go实现高并发的模型GMP模型来理解,后面写。
goweb相关
package main
import (
"fmt"
"log"
"net/http"
)
func main() {
http.HandleFunc("/", handler) // 地址与处理函数绑定,访问/地址做出handler函数内容的反应
log.Fatal(http.ListenAndServe("localhost:8000", nil))//监听8000端口,并给出日志文件
}
// 函数处理函数
func handler(w http.ResponseWriter, r *http.Request) { //wr即为连接的上下文
fmt.Fprintf(w, "URL.Path = %q\n", r.URL.Path) //把内容从w返回,返回到请求端
}
//浏览器访问8000端口,便可以得到访问地址, http://localhost:8000/help 得到URL.Path = "/help"
go实现一个web访问请求还是比较简单的,标准库里面就有相关函数,但是一般还是使用框架gin类似,标准库比较简单了框架也比较小性能强。
go其他
go设计与其他语言明显差异点,
1,循环语句只有for循环
2,switch语句默认break不用加,想顺序执行反而要加fallthrough
3,导出使用的首字母大小写
4,隐式函数接口
5,defer延迟处理
6,错误处理,经常返回错误,根据错误语法处理错误
7,if,for语言条件语句不加括号
8,函数可以接收不定长参数,返回值可以是多个
9,交换值,i, j = j, i
程序结构
命名
规则如大部分语法基本无异,一个名字必须以一个字母(Unicode字母)或下划线开头,后面可以跟任意数量的字母、数字或下划线。大写字母和小写字母是不同的:heapSort和Heapsort是两个不同的名字。
Go语言程序员推荐使用 驼峰式 命名,如QuoteRuneToASCII和parseRequestLine。
声明,变量,赋值
//声明结构体(类)接口
type name struct{
para string //定义属性
}
type name interface{
funname (name string)(result string) //<参数列表>,<返回值列表>
}
/*
const 名称 类型 = 值
var 变量名字 类型 = 表达式
名字 := 表达式 ,简短变量声明语句中必须至少要声明一个新的变量,左边必须有未声明变量
*/
const boilingF = 212.0 //省略掉类型编译器自动推导
var s string //仅定义不赋值,默认为类型零值
var i, j, k int //同时声明多个int变量
var b, f, s = true, 2.3, "four" //同时什么多个不同类型的变量
s:="abc" //简短格式声明不用类型编译器自动推导
in, err := os.Open(infile) //返回值赋值
x := 1
p := &x //p为指针,*p就是取指针的值,在函数调用中值传递,要想修改值就要用到指针
//new和make
p := new(int) //创建的变量是指定类型的零值,并返回该变量的指针
m := make(map[string]int)
//用于创建并初始化引用类型的变量,
//适用于创建切片、映射和通道等引用类型的变量
指针理解?
从问题角度出发理解指针,先看函数特性栈结构,函数会把参数复制,所以在函数里面修改的仅仅是副本,对外面的数并无影响,要解决这个问题所以出现了指针,穿进去的会复制一份,这里就传一个p里面是地址,p复制会值不变还是地址,然后函数体里面就可以知道要操作数的地址,而不去操作p,所以才有了指针,即关于指针的各种操作。
make和new的区别?
new()
用于创建任意类型的变量,而make()
仅用于创建引用类型的变量,只用于 chan、map 以及 slice 的内存创建。new()
返回的是指针,而make()
返回的是初始化后的值。new()
创建的变量是零值,make()
创建的变量是根据类型进行初始化。
var p *[]int = new([]int) //分配slice结构内存
var m []int = make([]int,100) //m指向一个新分配的有100个整数的数组
type IntSlice struct {
ptr *int
len, cap int
}
make创建slice操作可以理解为创建匿名的数据,然后在数据基础上创建一个intslice
x = 1 // 命名变量的赋值
*p = true // 通过指针间接赋值
person.name = "bob" // 结构体字段赋值
count[x] = count[x] * scale // 数组、slice或map的元素赋值
i++ //不存在++i
v, ok = m[key] // key存在返回,ok为true,否则为false
v, ok = x.(T) // 是否为T类型,ok为true,否则为false
v, ok = <-ch // 管道接收,管道关闭了,再接收ok为false
数据类型理解
为什么要有数据类型,也就是代称,一个8bit的地址,我们叫它byte就知道他是8bit,int是32bit的用来存数字的,就是一个隐式的约定,隐式约定在计算机中就特别重要,栈实际是链表或者线性表只是规定了只能从那进从那出,计组中的cache地址也是同意道理,组相联,全相连也是这样隐式规则。
包
包是程序构成的基本单元,go程序由一系列包组成,main包特殊。
从main包开始走,依赖包按深度优先导入并初始化,初始化过程中
包内按以“常量 -> 变量 -> init 函数”的顺序进行,包中有多个init函数依次调用,深度优先递归会main后,按main包中main函数进行执行代码逻辑。
俩特殊函数:
main包的mian函数:Go 语言中有一个特殊的函数:main 包中的 main 函数,也就是 main.main
,它是所有 Go 可执行程序的用户层执行逻辑的入口函数
init函数:Go 程序会在这个包初始化的时候,自动调用它的 init 函数,所以 init 函数的执行会发生在 main 函数之前,init 函数在一个包中可以有多个,每个 Go 源文件都可以定义多个 init 函数
基本数据类型
整形
int8、int16、int32和int64,有符号整形
uint8、uint16、uint32和uint64无符号整数类型
算术运算 + - * / %
逻辑运算
& 位运算 AND
| 位运算 OR
^ 位运算 XOR
<< 左移 //乘除法运算特殊的使用左移右移可以大大提高效率
>> 右移
比较运算
== 等于
!= 不等于
< 小于
<= 小于等于
> 大于
>= 大于等于
浮点数,复试
浮点数有float32和float64,类似于float,double区别,ieee754表示法( 符号位,阶码,数值位)
const Avogadro = 6.02214129e23 // 阿伏伽德罗常数
const Planck = 6.62606957e-34 // 普朗克常数
//浮点数可以用e来表示 1e3=1000
复数有complex64和complex128,内置的complex函数用于构建复数
var x complex128 = complex(1, 2) // 1+2i
var y complex128 = complex(3, 4) // 3+4i
布尔型,字符串,常量
布尔类型的值只有两种:true和false,布尔值和&&(AND)和||(OR)操作符结合,并且有短路行为:如果运算符左边值已经可以确定整个布尔表达式的值,那么运算符右边的值将不再被求值。
一个字符串是一个不可改变的字节序列。尝试修改字符串则会报错,但是是可以拼接形成新字符串
s[0] = 'L' // compile error: cannot assign to s[0]
关于utf-8
ASIIC码有8位数,每位是一个比特 (bit),8位就是一个字节 (byte),ASCII 包括所有的字母,漂亮国发明的仅仅考虑了字符键盘一些键等。
统一编码unicode,4字节也就是32位,基本代表了所以的符号语言,但是4字节一个符号,占用的存储容量就会很大。
然后就有了utf-8,是一种变成编码格式,类似于计组中的指令变成编码,大概就是1字节可以表示一部分符号,然后2字节又可以表示一部分,直到4字节,就可以表示所有了,1234字节符号地址之间有界限,类似于0或1开头来区分,也就是说的巧妙的隐式规则。解决了容量大的问题同时又可以表示多种符号要求。
s := "Hello, 世界"
fmt.Println(len(s)) // "13"
fmt.Println(utf8.RuneCountInString(s)) // "9"
关于以上代码,go使用的utf-8编码所以世界的就不仅仅是2byte了,len求的事byte长度,使用utf8包内函数才可以求出我们要的长度。
fmt.Println(string(65)) // "A", not "65"
fmt.Println(string(0x4eac)) // "京"
utf8说到底也是二进制,就可以看作数字,65还是代表a与ASIIC码就兼容了。
标准库中有四个包对字符串处理尤为重要:bytes、strings、strconv和unicode包。
strings包提供了许多如字符串的查询、替换、比较、截断、拆分和合并等功能。
bytes包也提供了很多类似功能的函数,但是针对和字符串有着相同结构的[]byte类型。因为字符串是只读的,因此逐步构建字符串会导致很多分配和复制。在这种情况下,使用bytes.Buffer类型将会更有效,稍后我们将展示。
strconv包提供了布尔型、整型数、浮点数和对应字符串的相互转换,还提供了双引号转义相关的转换。
unicode包提供了IsDigit、IsLetter、IsUpper和IsLower等类似功能,它们用于给字符分类。每个函数有一个单一的rune类型的参数,然后返回一个布尔值。而像ToUpper和ToLower之类的转换函数将用于rune字符的大小写转换。所有的这些函数都是遵循Unicode标准定义的字母、数字等分类规范。strings包也有类似的函数,它们是ToUpper和ToLower,将原始字符串的每个字符都做相应的转换,然后返回新的字符串。
const (
e = 2.71828182845904523536028747135266249775724709369995957496696763
pi = 3.14159265358979323846264338327950288419716939937510582097494459
)
常量const声明,编译期就能完成,减少运行时工作。
iota生成器,其实就是一组变量,右边表达式依次0123456对应个数。iota还可以用来弄表达式,
1 << iota 即0,下一个,就是1,2,4,8,等等
const (
Sunday Weekday = iota
Monday
Tuesday
Wednesday
Thursday
Friday
Saturday
)
无类型常量
用来解决显示类型装换,无类型就不用转换,可以赋值给其他类型变量,获得他的类型,就是为确定类型的白值,给谁用都可以,不用频繁显示装换类型了。
math.Pi无类型的浮点数常量,可以被赋值给有类型变量,从而获得类型。
0、0.0、0i和\u0000虽然有着相同的常量值,但是它们分别对应无类型的整数、无类型的浮点数、无类型的复数和无类型的字符等不同的常量类型。同样,true和false也是无类型的布尔类型,字符串面值常量是无类型的字符串类型。
复合数据类型
数组,slice(切片)
数组是一个由固定长度的特定类型元素组成的序列,物理地址连续,可以通过下标来访问。
var arr [3]int = [3]int{1,2,3} //规定数组长度,和值,没有值就会初始化为零值
q := [...]int{1, 2, 3} //不规定长度,用...来代替,根据值来推断长度
p = new([5]int) //还可以通过new函数创建数组,返回地址。
Slice(切片)代表变长的序列,序列中每个元素都有相同的类型。数组与切片的区别就是一个是固定长度,一个是变长的。
切片扩容机制简述
数组改变时改变原始数组的值,数组增加到比容量还大时,就要扩容,改变基本数组了,创建一个新的原始数组,机制是在原容量的前提下扩大两倍当容量小于1024时,大于1024则增加1024,省空间比较。然后把原内容复制进去。
//切片是使用append函数来扩容的,利用可变机制切片可以用来做栈
stack = append(stack, v) // push v,进栈
top := stack[len(stack)-1] // top of stack,得到栈顶元素
stack = stack[:len(stack)-1] // pop,出栈
/*
关于copy函数,Go语言的内置函数 copy() 可以将一个数组切片复制到另一个数组切片中,
如果加入的两个数组切片不一样大,就会按照其中较小的那个数组切片的元素个数进行复制。
*/
//copy( destSlice, srcSlice []T) int
slice1 := []int{1, 2, 3, 4, 5}
slice2 := []int{5, 4, 3}
copy(slice2, slice1) // 只会复制slice1的前3个元素到slice2中
copy(slice1, slice2) // 只会复制slice2的3个元素到slice1的前3个位置
map
map就是一个哈希表的引用,map类型可以写为map[K]V,其中K和V分别对应key和value。map中所有的key都有相同的类型,所有的value也有着相同的类型,但是key和value之间可以是不同的数据类型。
//map的创建使用make函数
ages := make(map[string]int) // mapping from strings to ints
ages["carol"] = 21 // 向一个未初始化的map里面添加则会造成panic异常
age, ok := ages["bob"] //不存在ok为false
if !ok { /* "bob" is not a key in this map; age == 0. */ }
结构体
结构体是一种聚合的数据类型,是由零个或多个任意类型的值聚合成的实体。实际就是一组变量的集合,函数是一组逻辑的集合,接口是函数的组合。
结构体就是其他语言的类,关于go语言结构体实现面向对象
封装:私有公有属性,go结构体中使用首字母的大小写来导出实现。
继承:一个结构体里面可以含有另一个结构体,就是继承了这个类了。分为匿名继承和不匿名。
多态:go中也有接口,一个接口就是一个状态,一个结构可以实现多个接口,用不同接口接收就是实现了不同状态,go中的接口实现也是隐私的,只要实现了接口中的那组函数就是实习了那个接口。
//关于匿名字段
type Person struct {
id int
name string
age int
}
type Student struct {
Person // 匿名字段又叫类型提升,使得不是student的属性相当于student,不匿名p Person
name string // 和Persion同名
score float64
}
//因为是匿名字段,s.age=10也是可以的,不是匿名的话需要s.p.age=10
s1.name = "zhangsan"
//当结构体中有与person结构体同名的属性
//先找本结构体,再找内嵌结构体
函数
函数声明
func name(parameter-list) (result-list) {//参数列表,返回值列表均可为多个值
body
}
//函数也可以当作参数传递给另一个函数
func sum(vals ...int) int { //...int可以传多个相同类型的数,在函数体面作为一个数组来用
total := 0
for _, val := range vals {
total += val
}
return total
}
a=[3]int{1,2,3}
sum(a...) //把a中元素依次传入不是一个数组方式
函数返回错误时的错误处理
1,得到返回值后,对err字段进行if错误判断,在if里面处理错误
2,如果错误发生后,程序无法继续运行,os.Exit(1)退出程序,若不是main函数中错误返回错误即可,交由下一步来处理。
3,如果错误不致命,可以选择记录下错误,打印到文件,或者直接无视
匿名函数与defer使用
resp, err := http.Get(url)
if err != nil {
return err
}
defer resp.Body.Close()
//为了防止忘记关闭body,defer延后函数会在函数结束时执行命令
匿名函数除了编程简单清楚另一个应用闭包
func counter() func() int { count := 0 return func() int { count++ return count } }
这个一个简单闭包,c1=counter()c1函数可以使用函数外的变量count,叫逃逸,counter执行完,count本该被销毁,c1在使用count,故逃逸出来,调用一次counter返回c1,多次调用c1时发现count会增长,这是因为每次调用c1使用的都是同一地址的count。c2=counter(),c2又应用另一个逃逸出来的count与c1没关系了。
函数内部执行顺序
func triple(x int) (result int) { defer func() { result += x }() return x*x } fmt.Println(triple(4)) // "12"
defer、return、返回值三者的执行顺序应该是:return最先给返回值赋值;接着defer开始执行一些收尾工作;最后RET指令携带返回值退出函数。
- return赋值,result=8
- defer执行,result=12
- 返回得到triple(4)的值
函数声明返回值名称
- Go 函数的返回或结果 "参数 "可以被命名并作为常规变量使用,就像传入参数一样。
- 当命名时,它们在函数开始时被初始化为其类型的零值。
- 如果函数执行没有参数的返回语句,结果参数的当前值被用作返回值。
异常恢复
func BBB() {
defer func() {
if err := recover(); err != nil {
fmt.Println("recover in func BBB")
fmt.Println(fmt.Sprintf("%T %v", err, err))
//...handle 打日志等
}
}()
panic("func BBB") //panic之前,如果没有recover,则程序会崩溃,异常退出。有则继续执行
}
recover
必须搭配defer
来使用,否则panic
捕获不到;defer
一定要在可能引发panic
的语句之前定义;
详见一文初探Go的defer、panic和recover - 掘金 (juejin.cn)
方法
方法是建立在结构体上,附属与一个结构体,这种附属有两种方式,值附属也就是直接附属,指针附属也就是附属与指针,一般调用时直接调用,发现没有方法,会在值前面加取地址符来调用指针方法,反过来指针不能调用值的方法,一般用值调用就可以了。
关于指针对象与值对象
- 方法是否需要修改 receiver 本身。如果需要,那 receiver 必然要是指针了。
- 效率问题。如果 receiver 是值,那在方法调用时一定会产生 struct 拷贝,而大对象拷贝代价很大哦。
当结构体进行嵌套时会先找本结构体方法,找不到找内嵌类型的方法。
func (p Point) square() { p.x *= p.x //这不会改变p的值,p是值拷贝的。调用的时候构造的一个临时变量 p.y *= p.y } func (p *Point) square() { p.x *= p.x //这会改变p的值,只是传递一个指针 p.y *= p.y }
接口
前面提过接口就是一类方法的集合,实现了这个方法就是实习了这个接口,比如写的接口,实现了这个接口,就叫他一个可以写的家伙,它是否实现了其他接口不用管,接口是一个抽象的概念,可以写的东西就在需要他的地方发挥作用,那个地方只在乎她是否能写。如func Fprintf(w io.Writer, format string, args ...interface{}) (int, error),常用的fmt.Fprintf,传入一个可写的w,调用w的写方法(实现了写接口就一定有写方法),具体怎么写就看w怎么实现这个写方法的了,就可以把工作顺利交接下去,通过接口这个抽象,这个约束,这个函数集合。
接口类型,接口实现
type ReadWriter interface {
Read(p []byte) (n int, err error) //接口可以自己生命
Write(p []byte) (n int, err error)
}
type ReadWriter interface { //接口也可以嵌套
Reader
Writer
}
type ReadWriter interface { //接口也可以混合使用
Read(p []byte) (n int, err error)
Writer
}
接口实现方式就是实习了接口规定的所有方法,变成了可以干啥的那家伙。一个类可以实现不同接口,
var w io.Writer
w = os.Stdout // OK: *os.File has Write method
w = new(bytes.Buffer) // OK: *bytes.Buffer has Write method
w = time.Second // compile error: time.Duration lacks Write method
关于空接口
空接口就是不含有任何方法的接口,换言之所有类型都实现了空接口。用途如下
1,泛型编程,就是可以在编程时,需要参数可以为任意类型,就不用为每个类型写一个方法了func Println(v interface{}) { fmt.Println(v) }
2,处理未知值,解析json中kv中v可以为好多类型,这里就可以用空接口
data := []byte(`{"name":"John","age":30}`)
var result map[string]interface{} json.Unmarshal(data, &result)
3,实现数据结构中列表树等结构时也可以用空接口可以接收任意类型来实现。
空接口不能赋值给一个声明了类型的变量会报错。,任意类型都可以是空接口这个家伙,但是想知道空接口的类型,就需要用到类型断言来判断他的类型。
接口值的概念
var w io.Writer
w = os.Stdout
w = new(bytes.Buffer)
w = nil
接口值由动态类型与动态值的组成,仅当两者均为nil时,接口值才为空。