GO语言基础教程
文章目录
1、Hello World
1.1 初体验
main.go
package main
import "fmt"
func main() {
fmt.Print("Hello World")
}
运行 go run main.go
编译程序命令 为go build
1.2 fmt格式化参数介绍
源码介绍路径
C:\Go\src\fmt\doc.go
中文文档参考 https://studygolang.com/pkgdoc fmt篇
通用:
%v 值的默认格式表示
%+v 类似%v,但输出结构体时会添加字段名
%#v 值的Go语法表示
%T 值的类型的Go语法表示
%% 百分号
布尔值:
%t 单词true或false
整数:
%b 表示为二进制
%c 该值对应的unicode码值
%d 表示为十进制
%o 表示为八进制
%q 该值对应的单引号括起来的go语法字符字面值,必要时会采用安全的转义表示
%x 表示为十六进制,使用a-f
%X 表示为十六进制,使用A-F
%U 表示为Unicode格式:U+1234,等价于"U+%04X"
浮点数与复数的两个组分:
%b 无小数部分、二进制指数的科学计数法,如-123456p-78;参见strconv.FormatFloat
%e 科学计数法,如-1234.456e+78
%E 科学计数法,如-1234.456E+78
%f 有小数部分但无指数部分,如123.456
%F 等价于%f
%g 根据实际情况采用%e或%f格式(以获得更简洁、准确的输出)
%G 根据实际情况采用%E或%F格式(以获得更简洁、准确的输出)
字符串和[]byte:
%s 直接输出字符串或者[]byte
%q 该值对应的双引号括起来的go语法字符串字面值,必要时会采用安全的转义表示
%x 每个字节用两字符十六进制数表示(使用a-f)
%X 每个字节用两字符十六进制数表示(使用A-F)
指针:
%p 表示为十六进制,并加上前导的0x
没有%u。整数如果是无符号类型自然输出也是无符号的。类似的,也没有必要指定操作数的尺寸(int8,int64)。
宽度通过一个紧跟在百分号后面的十进制数指定,如果未指定宽度,则表示值时除必需之外不作填充。精度通过(可选的)宽度后跟点号后跟的十进制数指定。如果未指定精度,会使用默认精度;如果点号后没有跟数字,表示精度为0。举例如下:
%f: 默认宽度,默认精度
%9f 宽度9,默认精度
%.2f 默认宽度,精度2
%9.2f 宽度9,精度2
%9.f 宽度9,精度0
1.3 fmt例子
一定要记住的
%v %+v %#v %T %s %d %p
//https://blog.csdn.net/qq_34777600/article/details/81266453
package main
import "fmt"
import "os"
type point struct {
x, y int
}
func main() {
//Go 为常规 Go 值的格式化设计提供了多种打印方式。例如,这里打印了 point 结构体的一个实例。
p := point{1, 2}
fmt.Printf("%v\n", p) // {1 2}
//如果值是一个结构体,%+v 的格式化输出内容将包括结构体的字段名。
fmt.Printf("%+v\n", p) // {x:1 y:2}
//%#v 形式则输出这个值的 Go 语法表示。例如,值的运行源代码片段。
fmt.Printf("%#v\n", p) // main.point{x:1, y:2}
//需要打印值的类型,使用 %T。
fmt.Printf("%T\n", p) // main.point
//格式化布尔值是简单的。
fmt.Printf("%t\n", true)
//格式化整形数有多种方式,使用 %d进行标准的十进制格式化。
fmt.Printf("%d\n", 123)
//这个输出二进制表示形式。
fmt.Printf("%b\n", 14)
//这个输出给定整数的对应字符。
fmt.Printf("%c\n", 33)
//%x 提供十六进制编码。
fmt.Printf("%x\n", 456)
//对于浮点型同样有很多的格式化选项。使用 %f 进行最基本的十进制格式化。
fmt.Printf("%f\n", 78.9)
//%e 和 %E 将浮点型格式化为(稍微有一点不同的)科学技科学记数法表示形式。
fmt.Printf("%e\n", 123400000.0)
fmt.Printf("%E\n", 123400000.0)
//使用 %s 进行基本的字符串输出。
fmt.Printf("%s\n", "\"string\"")
//像 Go 源代码中那样带有双引号的输出,使用 %q。
fmt.Printf("%q\n", "\"string\"")
//和上面的整形数一样,%x 输出使用 base-16 编码的字符串,每个字节使用 2 个字符表示。
fmt.Printf("%x\n", "hex this")
//要输出一个指针的值,使用 %p。
fmt.Printf("%p\n", &p)
//当输出数字的时候,你将经常想要控制输出结果的宽度和精度,可以使用在 % 后面使用数字来控制输出宽度。默认结果使用右对齐并且通过空格来填充空白部分。
fmt.Printf("|%6d|%6d|\n", 12, 345)
//你也可以指定浮点型的输出宽度,同时也可以通过 宽度.精度 的语法来指定输出的精度。
fmt.Printf("|%6.2f|%6.2f|\n", 1.2, 3.45)
//要最对齐,使用 - 标志。
fmt.Printf("|%-6.2f|%-6.2f|\n", 1.2, 3.45)
//你也许也想控制字符串输出时的宽度,特别是要确保他们在类表格输出时的对齐。这是基本的右对齐宽度表示。
fmt.Printf("|%6s|%6s|\n", "foo", "b")
//要左对齐,和数字一样,使用 - 标志。
fmt.Printf("|%-6s|%-6s|\n", "foo", "b")
//到目前为止,我们已经看过 Printf了,它通过 os.Stdout输出格式化的字符串。Sprintf 则格式化并返回一个字符串而不带任何输出。
s := fmt.Sprintf("a %s", "string")
fmt.Println(s)
//你可以使用 Fprintf 来格式化并输出到 io.Writers而不是 os.Stdout。
fmt.Fprintf(os.Stderr, "an %s\n", "error")
}
2、变量
Go 语言变量名由字母、数字、下划线组成,其中首个字符不能为数字。
声明变量的一般形式是使用 var 关键字:
-
声明变量,再赋值
var name string name = "zhangsan" -
简洁赋值,声明变量+赋值
name := "zhangsan"
在实际中,采用简洁赋值可读性比较好
3、基本类型
Go 的基本类型有
//bool
var ok bool = true
//string
var name string = "xxx"
//int int8 int16 int32 int64
//uint uint8 uint16 uint32 uint64 uintptr
var num int = 99
//byte 是 uint8 的别名
var numByte byte = 'c'
//rune 是 int32 的别名
//float32 float64
var f float32 = 12.1
//复数
//complex64 complex128
//数组定义
var n [2]int
//数组定义并赋值
var n = [2]int{1,2}
零值
没有明确初始值的变量声明会被赋予它们的零值:
零值是:
- 数值类型为
0, - 布尔类型为
false, - 字符串为
""(空字符串)。
3.1 字符串类型
- 采用utf-8编码,不会存在中文乱码问题
- 双引号表示,会识别转义符
- 反单引号`表示,不转移特定字符 ,结合fmt.Sprintf常用于复杂的字符串拼接
声明
var name string
//简洁方式
name := "xxx"
package main
import "fmt"
func main() {
fmt.Println(`反单引号测试\t测试2\n,转义符不起作用`)
user := fmt.Sprintf(`{"name":"zhangsan","age":%d}`,10)
fmt.Println(user)
//反单引号测试\t测试2\n,转义符不起作用
//{"name":"zhangsan","age":10}
}
- strings标准库使用C:\Go\src\strings\strings.go
//判断s是否有后缀字符串suffix
func HasSuffix(s, suffix string) bool
//判断字符串s是否包含子串substr
func Contains(s, substr string) bool
//子串sep在字符串s中第一次出现的位置,不存在则返回-1。
func Index(s, sep string) int
//返回将所有字母都转为对应的小写版本的拷贝。
func ToLower(s string) string
//返回将所有字母都转为对应的大写版本的拷贝。
func ToUpper(s string) string
//返回count个s串联的字符串。
func Repeat(s string, count int) string
//返回将s中前n个不重叠old子串都替换为new的新字符串,如果n<0会替换所有old子串。
func Replace(s, old, new string, n int) string
//返回将s前后端所有cutset包含的utf-8码值都去掉的字符串。
func Trim(s string, cutset string) string
//用去掉s中出现的sep的方式进行分割,会分割到结尾,并返回生成的所有片段组成的切片(每一个sep都会进行一次切割,即使两个sep相邻,也会进行两次切割)。如果sep为空字符,Split会将s切分成每一个unicode码值一个字符串。
func Split(s, sep string) []string
package main
import (
"fmt"
"strings"
)
func main() {
fmt.Println(strings.HasSuffix("avatar.png","png"))
//redis里获取一个value里面
value:="100:secret"
index := strings.Index(value,":")
if index>0 {
fmt.Println("secret:",value[index+1:])
}
fmt.Printf("%q\n", strings.Split("a,b,c", ","))
log("100",20,30)
}
func log(v ... interface{}){
format := ""
format+=strings.Repeat("%v ",len(v))
fmt.Printf(format,v...)
}
3.2 数字类型
// uint8 is the set of all unsigned 8-bit integers.
// Range: 0 through 255.
type uint8 uint8
// uint16 is the set of all unsigned 16-bit integers.
// Range: 0 through 65535.
type uint16 uint16
// uint32 is the set of all unsigned 32-bit integers.
// Range: 0 through 4294967295.
type uint32 uint32
// uint64 is the set of all unsigned 64-bit integers.
// Range: 0 through 18446744073709551615.
type uint64 uint64
// int8 is the set of all signed 8-bit integers.
// Range: -128 through 127.
type int8 int8
// int16 is the set of all signed 16-bit integers.
// Range: -32768 through 32767.
type int16 int16
// int32 is the set of all signed 32-bit integers.
// Range: -2147483648 through 2147483647.
type int32 int32
// int64 is the set of all signed 64-bit integers.
// Range: -9223372036854775808 through 9223372036854775807.
type int64 int64
// float32 is the set of all IEEE-754 32-bit floating-point numbers.
type float32 float32
// float64 is the set of all IEEE-754 64-bit floating-point numbers.
type float64 float64
// int is a signed integer type that is at least 32 bits in size. It is a
// distinct type, however, and not an alias for, say, int32.
// int在32操作系统为32位,最大值是2147483647;在64操作系统中为64位,最大值是9223372036854775807。注意,他不是int32
type int int
/*
package main
import "fmt"
func main() {
fmt.Println(int(1<<63 -1))
//9223372036854775807
}
*/
// uint is an unsigned integer type that is at least 32 bits in size. It is a
// distinct type, however, and not an alias for, say, uint32.
type uint uint
/*
package main
import "fmt"
func main() {
fmt.Println(uint(1<<64 -1))
//18446744073709551615
}
*/
开发过程中,我们经常用到时间戳,请定义为int64,不然又会埋下像“千年虫”事件一样的定时炸弹
2020-11-1 00:00:00 1604160000
2038-01-19 11:14:07 2147483647
3.2 基本类型间转换
3.2.1 数字类型相互转换
表达式 T(v) 将值 v 转换为类型 T。
一些关于数值的转换:
var i int = 42
var f float64 = float64(i)
var u uint = uint(f)
或者,更加简单的形式:
i := 42
f := float64(i)
u := uint(f)
与 C 不同的是,Go 在不同类型的项之间赋值时需要显式转换,没有隐式转换。
注意一点,大范围转小范围,会出现精度丢失
package main
import "fmt"
//小端模式下
func main() {
//十六进制值0x1234 存储格式为0001 0010 0011 0100
//int8 只能存储一个字节,0011 0100 = 0x34
var num64 int64 = 0x1234
fmt.Printf("%x",int8(num64))
//打印16进制为34
}
补充一个知识点:网络中传输的数据统一是大端模式传输的,和两段的系统无关,所以解析网络数据的时候要特别注意,大于一个字节的数字类型,一定严格按照大端模式的数据解析
func tcpReadProto(rd *bufio.Reader, proto *Proto) ([]byte, error) {
var (
packLen int32
headerLen int16
err error
decodeBody []byte
)
// read
if err = binary.Read(rd, binary.BigEndian, &packLen); err != nil {
return nil, err
}
log.Debug("packLen: %d", packLen)
if err = binary.Read(rd, binary.BigEndian, &headerLen); err != nil {
return nil, err
}
log.Debug("headerLen: %d", headerLen)
...
}
3.2.2 数字和字符串相互转换
-
具体代码参考 C:\Go\src\strconv包
-
数字转字符串
//base 指定进制,必须在2到36之间。
func FormatInt(i int64, base int) string
- 字符串转数字
//返回字符串表示的整数值,接受正负号。
//base指定进制(2到36),如果base为0,则会从字符串前置判断,"0x"是16进制,"0"是8进制,否则是10进制;
//bitSize指定结果必须能无溢出赋值的整数类型,0、8、16、32、64 分别代表 int、int8、int16、int32、int64;返回的err是*NumErr类型的,如果语法有误,err.Error = ErrSyntax;如果结果超出类型范围err.Error = ErrRange。
func ParseInt(s string, base int, bitSize int) (i int64, err error)
- 简洁写法,int范围内
//Itoa是FormatInt(i, 10) 的简写。
func Itoa(i int) string
//Atoi是ParseInt(s, 10, 0)的简写。
func Atoi(s string) (i int, err error)
- 例子
package main
import (
"fmt"
"log"
"strconv"
)
func main() {
str := strconv.FormatInt(111,10)
fmt.Printf("str is %s \n",str)
num,err := strconv.ParseInt("666",10,64)
if err !=nil {
log.Fatal(err.Error())
}
fmt.Printf("num is %d \n",num)
str2 := strconv.Itoa(99)
fmt.Printf("str2 is %s \n",str2)
num2,_ := strconv.Atoi("99")
fmt.Printf("num2 is %d \n",num2)
}
4 常量
- 常量在程序运行时,不可被修改。
- 常量中的数据类型只可以是布尔型、数字型和字符串型。
- 常量不能用
:=语法声明。 - 用const 定义
iota是一个预先声明的标识符,iota 在 const关键字出现时将被重置为 0,const 中每新增一行常量声明将使 iota 计数一次,即下一行=前一行+1
package main
import "fmt"
func main() {
const (
Unknown = iota
Female
Male
)
fmt.Println(Unknown,Female,Male)
//0 1 2
}
5 语句
5.1 条件语句 if
if 表达式 {
/* 在表达式为 true 时执行 */
}
Go 的 if 语句与 for 循环类似,表达式外无需小括号 ( ) ,而大括号 { } 则是必须的。
import (
"fmt"
)
func check() (string,bool) {
return "namespace",true
}
func main() {
if ns,ok := check(); ok {
fmt.Println(ns)
}
//注意ns的作用域在条件语句{}内,下列会编译会报错undefined: ns
//fmt.Println(ns)
}
5.2 条件语句 switch
//switch语句将expr表达式结果与可能的value值的列表进行匹配,然后执行响应代码
switch expr {
case vaule:
...
case vaule:
...
default:
...
}
switch 是编写一连串 if - else 语句的简便方法。它运行第一个值等于条件表达式的 case 语句。
Go 的 switch 语句类似于 C、C++、Java、JavaScript 和 PHP 中的,不过 Go 只运行选定的 case,而非之后所有的 case。 实际上,Go 自动提供了在这些语言中每个 case 后面所需的 break 语句。 除非以 fallthrough 语句结束,否则分支会自动终止。 Go 的另一点重要的不同在于 switch 的 case 无需为常量,且取值不必为整数。
package main
import (
"fmt"
"runtime"
)
func main() {
fmt.Print("Go runs on ")
os := runtime.GOOS
switch os {
case "darwin":
fmt.Println("OS X.")
case "linux":
fmt.Println("Linux.")
default:
fmt.Printf("%s.\n", os)
}
}
注意:switch 的 case 语句从上到下顺次执行,直到匹配成功时停止。
package main
import "fmt"
func main() {
switch 3 {
case f1():
fmt.Println("execute 1 case statement")
case f2():
fmt.Println("execute 2 case statement")
case f3():
fmt.Println("execute 3 case statement")
case f4():
fmt.Println("execute 4 case statement")
default:
fmt.Println("execute default statement")
}
}
func f1() int {
fmt.Println("f1 function")
return 1
}
func f2() int {
fmt.Println("f2 function")
return 2
}
func f3() int {
fmt.Println("f3 function")
return 3
}
func f4() int {
fmt.Println("f4 function")
return 4
}
5.2.1 没有条件的 switch
没有条件的 switch 同 switch true 一样。
这种形式能将一长串 if-then-else 写得更加清晰。
package main
import (
"fmt"
)
func main() {
score:=55
if score>90 {
fmt.Println("优秀")
}else if score>80{
fmt.Println("良")
}else if score>70{
fmt.Println("中")
}else if score>60{
fmt.Println("及格")
}else {
fmt.Println("不及格")
}
switch {
case score>90:
fmt.Println("优秀")
case score>80:
fmt.Println("良")
case score>70:
fmt.Println("中")
case score>60:
fmt.Println("及格")
default:
fmt.Println("不及格")
}
}
5.2.2 fallthrough
使用 fallthrough 会强制执行后面的 case 语句,fallthrough 不会判断下一条 case 的表达式结果是否为 true。
package main
import "fmt"
func main() {
switch {
case false:
fmt.Println("1、case 条件语句为 false")
fallthrough
case true:
fmt.Println("2、case 条件语句为 true")
fallthrough
case false:
fmt.Println("3、case 条件语句为 false")
fallthrough
case true:
fmt.Println("4、case 条件语句为 true")
case false:
fmt.Println("5、case 条件语句为 false")
fallthrough
default:
fmt.Println("6、默认 case")
}
}
5.3 循环语句
5.3.1 for
for 初始化; 控制条件语句; 控制条件赋值 {
//控制条件语句为true执行循环体
}
例子
package main
import "fmt"
func main() {
for i := 0; i < 10; i++ {
fmt.Println(i)
}
}
5.3.2 for range
for 循环的 range 格式可以对 slice、map、数组、字符串等进行迭代循环。格式如下:
for key, value := range arr {
//获取key,value,然后对应操作
}
例子
package main
import "fmt"
func main() {
nums := []int{1, 2, 3, 5}
for key, value := range nums {
fmt.Printf("key=%d, value=%d\n", key, value)
}
}
/*
结果为
key=0, value=1
key=1, value=2
key=2, value=3
key=3, value=5
*/
5.3.3 continue
- 跳过当前循环的剩余语句,然后继续进行下一轮循环。
5.3.4 break
- 跳出循环,并开始执行循环之后的语句。
- break有坑
for循环在和switch语句,select语句连用时候,使用break不当,容易造成死循环
break是跳出最近的for循环,或者switch和select语句
可以实验下面代码,break语句无法跳出循环
package main
import (
"fmt"
"time"
)
func main() {
for i := 1; ; i++ {
time.Sleep(time.Second)
switch i {
case 1:
fmt.Println("case 1,i=", i)
case 2:
fmt.Println("case 2,i=", i)
default:
fmt.Println("case default", i)
break
}
}
}
解决办法:
- 可以加标签,指定break出那一层。建议不要这样做,代码不简洁,可读性差。
- 可以封装switch语句在函数内,函数自带作用域范围,就不会引起这样问题了。
package main
import (
"fmt"
"time"
)
func main() {
re:
for i := 1; ; i++ {
time.Sleep(time.Second)
switch i {
case 1:
fmt.Println("case 1,i=", i)
case 2:
fmt.Println("case 2,i=", i)
default:
fmt.Println("case default", i)
break re
}
}
}
package main
import (
"fmt"
"time"
)
func f(i int) {
switch i {
case 1:
fmt.Println("case 1,i=", i)
case 2:
fmt.Println("case 2,i=", i)
default:
fmt.Println("case default", i)
}
}
func main() {
for i := 1; ; i++ {
time.Sleep(time.Second)
f(i)
break
}
}
5.3.4 for range的坑
-
切记,不要对for range 的引用value的值
-
key和value只定义一次,在循环体内每循环一次做一次赋值。他们的地址是不变化的
package main
import (
"fmt"
)
func main() {
nums := []int{1, 3, 5, 7}
nums2 := make([]*int, 0)
for key, value := range nums {
nums2 = append(nums2, &value)
fmt.Printf("key=%d,keyp=%p, value=%d,valuep=%p\n", key, &key, value, &value)
}
for _, v := range nums2 {
fmt.Printf("p=%p,value=%d\n", v, *v)
}
}
go for range源码
https://github.com/golang/gofrontend/blob/master/go/statements.cc
For_range_statement::lower_range_slice
// The loop we generate:
// for_temp := range
// len_temp := len(for_temp)
// for index_temp = 0; index_temp < len_temp; index_temp++ {
// value_temp = for_temp[index_temp]
// index = index_temp
// value = value_temp
// original body
// }
6 指针
Go 拥有指针。指针保存了值的内存地址。
类型 *T 是指向 T 类型值的指针。其零值为 nil。
var p *int
& 操作符会生成一个指向其操作数的指针。
i := 42
p = &i
* 操作符表示指针指向的底层值。
fmt.Println(*p) // 通过指针 p 读取 i
*p = 21 // 通过指针 p 设置 i
这也就是通常所说的“间接引用”或“重定向”。
与 C 不同,Go 没有指针运算。
7 结构体
一个结构体(struct)就是一组字段(field)。
结构体字段使用点号来访问。
结构体字段可以通过结构体指针来访问。
package main
import "fmt"
type Vertex struct {
X, Y int
}
var (
v1 = Vertex{1, 2} // 创建一个 Vertex 类型的结构体
v2 = Vertex{X: 5, Y: 6} //指定成员赋值,与顺序无关
v3 = Vertex{} // X:0 Y:0
p = &Vertex{9, 10} // 创建一个 *Vertex 类型的结构体(指针)
)
func main() {
fmt.Println("v1.x = ", v1.X)
fmt.Println("p.x = ", p.X)
fmt.Println(v1, v2, v3, p)
}
8 数组
- 类型
[n]T表示拥有n个T类型的值的数组。 - 数组的长度是其类型的一部分,数组不能改变大小
- 表达式
var a [10]int //声明
var f = [5]float32{1.0, 2.0, 3.1, 4.6, 20.1} //声明并赋值
会将变量 a 声明为拥有 10 个整数的数组。
package main
import "fmt"
func main() {
var a [2]string
a[0] = "Hello"
a[1] = "World"
fmt.Println(a[0], a[1])
fmt.Println(a)
primes := [6]int{2, 3, 5, 7, 11, 13}
fmt.Println(primes)
}
9 切片
9.1 声明
切片声明类似于没有长度的数组文法。
这是一个数组文法:
//[3]int{1, 2, 3}
var bools = [3]int{1, 2, 3}
下面这样则会创建一个和上面相同的数组,然后构建一个引用了它的切片:
//[]int{1, 2, 3}
var s = []int{1, 2, 3}
每个数组的大小都是固定的。而切片则为数组元素提供动态大小的、灵活的视角。在实践中,切片比数组更常用。
类型 []T 表示一个元素类型为 T 的切片。
切片通过两个下标来界定,即一个上界和一个下界,二者以冒号分隔:
a[0 : 3]
它会选择一个半开区间,包括第一个元素,但排除最后一个元素。
以下表达式创建了一个切片,它包含 a 中下标从 1 到 3 的元素:
a[1:4]
例子:
package main
import "fmt"
func main() {
primes := [6]int{2, 3, 5, 7, 11, 13}
var s []int = primes[1:4]
fmt.Println(s)
}
nil是一个预先声明的标识符,表示指针、通道、函数、接口、映射或切片类型的零值。
9.2 切片就像数组的引用
type slice struct {
array unsafe.Pointer
len int
cap int
}
切片并不存储任何数据,它只是描述了底层数组中的一段。
更改切片的元素会修改其底层数组中对应的元素。
与它共享底层数组的切片都会观测到这些修改。
package main
import "fmt"
func main() {
names := [4]string{
"John",
"Paul",
"George",
"Ringo",
}
fmt.Println(names)
a := names[0:2]
b := names[1:3]
fmt.Println(a, b)
b[0] = "XXX"
fmt.Println(a, b)
fmt.Println(names)
}
9.3 切片的默认行为
在进行切片时,你可以利用它的默认行为来忽略上下界。
切片下界的默认值为 0,上界则是该切片的长度。
对于数组
var a [10]int
来说,以下切片是等价的:
a[0:10]
a[:10]
a[0:]
a[:]
package main
import "fmt"
func main() {
s := []int{2, 3, 5, 7, 11, 13}
s = s[1:4]
fmt.Println(s)
s = s[:2]
fmt.Println(s)
s = s[1:]
fmt.Println(s)
}
9.4 切片的长度与容量
切片拥有 长度 和 容量。
切片的长度就是它所包含的元素个数。
切片的容量是从它的第一个元素开始数,到其底层数组元素末尾的个数。
切片 s 的长度和容量可通过表达式 len(s) 和 cap(s) 来获取。
你可以通过重新切片来扩展一个切片,给它提供足够的容量。试着修改示例程序中的切片操作,向外扩展它的容量,看看会发生什么。
package main
import "fmt"
func main() {
s := []int{2, 3, 5, 7, 11, 13}
printSlice(s)
// 截取切片使其长度为 0
s = s[:0]
printSlice(s)
// 拓展其长度
s = s[:4]
printSlice(s)
// 舍弃前两个值
s = s[2:]
printSlice(s)
}
func printSlice(s []int) {
fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s)
}
9.5 切片零值
切片的零值是 nil。
nil 切片的长度和容量为 0 且没有底层数组。
package main
import "fmt"
func main() {
var s []int
fmt.Println(s, len(s), cap(s))
if s == nil {
fmt.Println("nil!")
}
}
9.6 用 make 创建切片
切片可以用内建函数 make 来创建,这也是你创建动态数组的方式。
make 函数会分配一个元素为零值的数组并返回一个引用了它的切片:
a := make([]int, 5) // len(a)=5
要指定它的容量,需向 make 传入第三个参数:
b := make([]int, 0, 5) // len(b)=0, cap(b)=5
b = b[:cap(b)] // len(b)=5, cap(b)=5
b = b[1:] // len(b)=4, cap(b)=4
9.7 向切片追加元素
为切片追加新的元素是种常用的操作,为此 Go 提供了内建的 append 函数。内建函数的文档对此函数有详细的介绍。
func append(s []T, vs ...T) []T
append 的第一个参数 s 是一个元素类型为 T 的切片,其余类型为 T 的值将会追加到该切片的末尾。
append 的结果是一个包含原切片所有元素加上新添加元素的切片。
当 s 的底层数组太小,不足以容纳所有给定的值时,它就会分配一个更大的数组。返回的切片会指向这个新分配的数组。
package main
import "fmt"
func main() {
var s []int
printSlice(s)
// 添加一个空切片
s = append(s, 0)
printSlice(s)
// 这个切片会按需增长
s = append(s, 1)
printSlice(s)
// 可以一次性添加多个元素
s = append(s, 2, 3, 4)
printSlice(s)
}
func printSlice(s []int) {
fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s)
}
9.8 切片扩容机制
- 当原切片长度小于1024时,新切片的容量会直接翻倍。而当原切片的容量大于等于1024时,会反复地增加25%,直到新容量超过所需要的容量。
- 若新入元素大小超过了原有的容量,则新容量取两者相加计算出来的最小cap值。
- 出于内存的高效利用考虑,还要进行内存对齐
src/runtime/slice.go
...
newcap := old.cap
doublecap := newcap + newcap
if cap > doublecap {
newcap = cap
} else {
if old.len < 1024 {
newcap = doublecap
} else {
// Check 0 < newcap to detect overflow
// and prevent an infinite loop.
for 0 < newcap && newcap < cap {
newcap += newcap / 4
}
// Set newcap to the requested cap when
// the newcap calculation overflowed.
if newcap <= 0 {
newcap = cap
}
}
}
...
下列实验观察切片容量增长
package main
import "fmt"
var sCap int
func main() {
var s []int
for i := 0; i < 100000; i++ {
s = append(s, i)
printCapChange(s)
}
}
func printCapChange(s []int) {
oldCap := sCap
sCap = cap(s)
if oldCap != sCap && oldCap > 0 {
fmt.Printf("cap change %d %d \n", sCap, (sCap)*100/oldCap)
}
}
切片指向的数组,每次扩容数组会发生变化实验
package main
import (
"fmt"
)
var sCap int
func main() {
var arr = [4]int{1, 2, 3, 4}
var s = arr[0:4]
fmt.Printf("数组 %v ,%p\n", arr, &arr)
fmt.Printf("切片 %v ,%p\n", s, s)
for i := 0; i < 20; i++ {
s = append(s, i)
printCapChange(s)
}
s[0] = 999
fmt.Printf("数组%v ,%p\n", arr, &arr)
fmt.Printf("切片底层的数组已经改变 %v ,%p\n", s, s)
}
func printCapChange(s []int) {
oldCap := sCap
sCap = cap(s)
if oldCap != sCap && oldCap > 0 {
fmt.Printf("切片%p 容量改变为%d,扩大为原来%d%% \n", &s, sCap, (sCap)*100/oldCap)
}
}
9.9 切片引起的bug
s := arr[0:4]
//当你改变切片元素,底层的数组或指向该数组的其他切片,都可能会被修改,所以,如有修改操作,请使用copy函数
s[0] = 999
9.10 切片copy操作
//dst目标切片,目标切片的大小要和源切片大小一样;src源切片
func copy(dst, src []Type) int
10 map
- 对应其他语言字典或hash表
- map是一种无序的键值对的集合
- map的零值是nil,可以用make来创建
- map并发读写不安全,不能同时并发读写,不能同时并发写
- 如果只用来读,是并发安全的
10.1 map操作
map声明
//使用make声明并初始化map
m := make(map[int]int)
//声明并初始化map
var score = map[string]int{"zhangsan": 99, "lisi": 88, "wangwu": 59}
//简洁声明并初始化map
score1 := map[string]int{"zhangsan": 99, "lisi": 88, "wangwu": 59}
在map m 中插入或修改元素:
m[key] = elem
获取元素:
elem = m[key]
删除元素:
delete(m, key)
通过双赋值检测某个键是否存在:
elem, ok = m[key]
若 key 在 m 中,ok 为 true ;否则,ok 为 false。
若 key 不在map中,那么 elem 是该map元素类型零。
同样的,当从map中读取某个不存在的键时,结果是map射的元素类型的零值。
注 :若 elem 或 ok 还未声明,你可以使用短变量声明:
elem, ok := m[key]
10.2 map并发不安全
下面代码可能会出现
-
并发读写错误 fatal error: concurrent map read and map write
-
并发写错误 fatal error: concurrent map writes
package main
import "time"
func main() {
m := make(map[int]int)
for i := 0; i < 100; i++ {
go writeMap(m, i, i)
go readMap(m, i)
}
time.Sleep(time.Second)
}
func readMap(m map[int]int, key int) int {
return m[key]
}
func writeMap(m map[int]int, key int, value int) {
m[key] = value
}
10.3 map加锁版本
package main
import (
"fmt"
"math/rand"
"strconv"
"sync"
)
type userMap struct {
sync.RWMutex
m map[string]string
}
func (u *userMap) read(key string) {
u.RLock()
defer u.RUnlock()
n := u.m[key]
fmt.Println("value:", n)
}
func (u *userMap) write(key, value string) {
u.Lock()
defer u.Unlock()
u.m[key] = value
}
func main() {
c := userMap{
m: make(map[string]string),
}
for i := 0; i < 1000; i++ {
v := rand.Int31n(9999)
go c.write(strconv.Itoa(i), strconv.Itoa(int(v)))
go c.read(strconv.Itoa(i))
}
}
11 函数
11.1 函数定义
- 定义:函数名、形参列表、返回值列表、函数体
func function_name( [parameter list] ) [return_types] {
函数体
}
func funcName(param1 int,param2 string)(int, error){
return 0,nil
}
-
多返回值
-
函数大写开头表示public,包外面可见。小写开头表示private,包外部可见
-
参数传递是值传递
基础类型、数组、结构体,在函数内修改,不会影响到原来的值。
slice、map、channel本身是引用类型,在函数内修改,会影响原来的值
-
函数也可以当做变量
-
可变参数,用 … 表示
func myfunc(args ...int) {
for _, arg := range args {
fmt.Println(arg)
}
}
11.1 retrun
- Go 语言支持多个返回值
- 函数返回是分两步的,1、先给返回值赋值,2、然后再返回
package main
func foo() (int, int) {
i := 1
j := 2
return i, j
}
func main() {
foo()
}
查看汇编代码go tool compile -S -N -l types.go
0x0000 00000 (types.go:3) TEXT "".foo(SB), NOSPLIT|ABIInternal, $24-16
0x0000 00000 (types.go:3) SUBQ $24, SP
0x0004 00004 (types.go:3) MOVQ BP, 16(SP)
0x0009 00009 (types.go:3) LEAQ 16(SP), BP
0x000e 00014 (types.go:3) PCDATA $0, $-2
0x000e 00014 (types.go:3) PCDATA $1, $-2
0x000e 00014 (types.go:3) FUNCDATA $0, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
0x000e 00014 (types.go:3) FUNCDATA $1, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
0x000e 00014 (types.go:3) FUNCDATA $2, gclocals·33cdeccccebe80329f1fdbee7f5874cb(SB)
0x000e 00014 (types.go:3) PCDATA $0, $0
0x000e 00014 (types.go:3) PCDATA $1, $0
0x000e 00014 (types.go:3) MOVQ $0, "".~r0+32(SP)
0x0017 00023 (types.go:3) MOVQ $0, "".~r1+40(SP)
0x0020 00032 (types.go:4) MOVQ $1, "".i+8(SP)
0x0029 00041 (types.go:5) MOVQ $2, "".j(SP)
0x0031 00049 (types.go:6) MOVQ "".i+8(SP), AX
0x0036 00054 (types.go:6) MOVQ AX, "".~r0+32(SP)
0x003b 00059 (types.go:6) MOVQ "".j(SP), AX
0x003f 00063 (types.go:6) MOVQ AX, "".~r1+40(SP)
0x0044 00068 (types.go:6) MOVQ 16(SP), BP
0x0049 00073 (types.go:6) ADDQ $24, SP
0x004d 00077 (types.go:6) RET
11.2 defer
- 在函数退出时执行
- 多个defer按先进后出方式执行
- defer调用在返回值赋值后,在函数返回前
返回值赋值
defer f()
return
package main
import "fmt"
//1、返回值为定义一个临时变量,赋值为1
//2、defer中i++
//3、return;所以返回值是1,defer中i改变不影响返回值
func foo() int {
i := 1
defer func() {
i++
fmt.Printf("i=%d\n", i)
}()
return i
}
func main() {
res := foo()
fmt.Printf("返回值=%d\n", res)
}
返回值如果是命名返回值,情况又不一样
package main
import "fmt"
//1、i=1
//2、defer中i=11
//3、return
func foo() (i int) {
defer func() {
i = i + 10
}()
return 1
}
func main() {
res := foo()
fmt.Printf("res=%d\n", res)
}
11.3 闭包函数
闭包是可以包含自由(未绑定到特定对象)变量的代码块,这些变量不在这个代码块内或者
任何全局上下文中定义,而是在定义代码块的环境中定义。要执行的代码块(由于自由变量包含
在代码块中,所以这些自由变量以及它们引用的对象没有被释放)为自由变量提供绑定的计算环
境(作用域)。
应用《Go语言编程》的描述,我们可以理解闭包为带有状态的函数,由变量和函数两部分组成。
package main
import (
"fmt"
"strconv"
)
func add() func(int) int {
n := 10
str := "good luck"
return func(x int) int {
n = x + n
str = str+strconv.Itoa(x)
fmt.Println(str)
return n
}
}
func main() {
fn := add()
fmt.Printf("%d\n", fn(1))
fmt.Printf("%d\n", fn(1))
fmt.Printf("%d\n", fn(1))
}
闭包可以看做下面变量和匿名函数的结合
n := 10
str := "good luck"
return func(x int) int {
n = x + n
str += strconv.Itoa(x)
fmt.Println(str)
return n
}
12 接口interface
12.1 Any类型
由于Go语言中任何对象实例都满足空接口interface{},所以interface{}看起来像是可
以指向任何对象的Any类型,如下:
var v1 interface{} = 1 // 将int类型赋值给interface{}
var v2 interface{} = "abc" // 将string类型赋值给interface{}
var v3 interface{} = &v2 // 将*interface{}类型赋值给interface{}
var v4 interface{} = struct{ X int }{1}
var v5 interface{} = &struct{ X int }{1}
当函数可以接受任意的对象实例时,我们会将其声明为interface{},最典型的例子是标
准库fmt中PrintXXX系列的函数,例如:
func Printf(fmt string, args ...interface{})
func Println(args ...interface{})
...
12.2 类型断言
类型断言 提供了访问接口值底层具体值的方式。
t := i.(T)
该语句断言接口值 i 保存了具体类型 T,并将其底层类型为 T 的值赋予变量 t。
若 i 并未保存 T 类型的值,该语句就会触发一个恐慌。
为了 判断 一个接口值是否保存了一个特定的类型,类型断言可返回两个值:其底层值以及一个报告断言是否成功的布尔值。
t, ok := i.(T)
若 i 保存了一个 T,那么 t 将会是其底层值,而 ok 为 true。
否则,ok 将为 false 而 t 将为 T 类型的零值,程序并不会产生恐慌。
请注意这种语法和读取一个映射时的相同之处
package main
import "fmt"
func main() {
var i interface{} = "hello"
s := i.(string)
fmt.Println(s)
s, ok := i.(string)
fmt.Println(s, ok)
f, ok := i.(float64)
fmt.Println(f, ok)
f = i.(float64) // 报错(panic)
fmt.Println(f)
}
12.3 类型选择
类型选择 是一种按顺序从几个类型断言中选择分支的结构。
类型选择与一般的 switch 语句相似,不过类型选择中的 case 为类型(而非值), 它们针对给定接口值所存储的值的类型进行比较。
switch v := i.(type) {
case T:
// v 的类型为 T
case S:
// v 的类型为 S
default:
// 没有匹配,v 与 i 的类型相同
}
类型选择中的声明与类型断言 i.(T) 的语法相同,只是具体类型 T 被替换成了关键字 type。
此选择语句判断接口值 i 保存的值类型是 T 还是 S。在 T 或 S 的情况下,变量 v 会分别按 T 或 S 类型保存 i 拥有的值。在默认(即没有匹配)的情况下,变量 v 与 i 的接口类型和值相同。
package main
import "fmt"
func do(i interface{}) {
switch v := i.(type) {
case int:
fmt.Printf("Twice %v is %v\n", v, v*2)
case string:
fmt.Printf("%q is %v bytes long\n", v, len(v))
default:
fmt.Printf("I don't know about type %T!\n", v)
}
}
func main() {
do(21)
do("hello")
do(true)
}
12.4 非侵入式接口
在Go语言中,一个类只需要实现了接口要求的所有函数,我们就说这个类实现了该接口,
例如:
type File struct {
// ...
}
func (f *File) Read(buf []byte) (n int, err error){
...
}
func (f *File) Write(buf []byte) (n int, err error){
...
}
func (f *File) Seek(off int64, whence int) (pos int64, err error){
...
}
func (f *File) Close() error{
...
}
这里我们定义了一个File类,并实现有Read()、Write()、Seek()、Close()等方法。设
想我们有如下接口:
type IFile interface {
Read(buf []byte) (n int, err error)
Write(buf []byte) (n int, err error)
Seek(off int64, whence int) (pos int64, err error)
Close() error
}
type IReader interface {
Read(buf []byte) (n int, err error)
}
type IWriter interface {
Write(buf []byte) (n int, err error)
}
type ICloser interface {
Close() error
}
尽管File类并没有从这些接口继承,甚至可以不知道这些接口的存在,但是File类实现了
这些接口,可以进行赋值:
var file1 IFile = new(File)
var file2 IReader = new(File)
var file3 IWriter = new(File)
var file4 ICloser = new(File)
13 协程goroutine
-
goroutine 是轻量级线程,goroutine 的调度是由 Golang 运行时进行管理的。
-
线程和goroutine区别
在linux操作系统中,线程是一种轻量级进程,由操作系统来调度。线程切换需要由用户态切换到内核态进行,将一些线程资源保存在内核内存空间(32位系统是3-4G),然后从内核内存空间恢复被调度的下一个线程资源。
goroutine是真正意义上的线程,go实现了goroutine的管理,goroutine的切换只在用户态进行。
-
Don’t communicate by sharing memory; share memory by communicating.不要通过共享数据来通讯,恰恰相反,要以通讯的方式共享数据。
-
在业务量并发大的情况下,请注意控制goroutine数量,你后端的程序性能跟不上(数据库),你程序并发支持再多也没用。请使用协程池。
goroutine 语法格式:
go 函数名(参数列表)
14 channel
-
channel是并发安全的
-
类型字面量如
chan int,其中的chan是表示通道类型的关键字,而int则说明了该通道类型的元素类型 -
可以使用make方法来创建,第二个参数代表通道缓冲容量,0或者没有代表非缓冲通道
-
是引用类型,零值是nil,写或读零值channel,会一直阻塞
-
通道是先进先出。阻塞时,发送有发送队列,接收有接收队列,通道可发送或可接收时,通知队列里面最新等待发送或接收的goroutine
-
数据进入通道是复制右边的元素到通道(注意引用类型,只复制元素本身,他指向的数据还是不变)
-
channel在close操作后,继续发送数据会导致panic。close的通道可以继续读取数据,可以定义两个变量接收读取通道的结果
-
一般在发送端关闭通道
//通道关闭的时候,读取完通道数据后,ok=false,所以在读取的时候注意判断ok的值 value, ok := <-c -
阻塞情况
有缓冲区的通道:1、写入缓冲已满的通道会阻塞。2、读取没有数据的通道。
无缓冲通道:发送或接收有一方没准备,就会阻塞在那里
14.1 声明
//未赋值的channel的值是nil
var c chan int
//声明并赋值
var c = make(chan int, 10)
c := make(chan int, 10)
- 写入和读取chan,用<-表示
package main
import (
"fmt"
"time"
)
func main() {
//var c chan int = make(chan int, 10)
c := make(chan int, 10)
//写入
c <- 1
c <- 2
c <- 3
go func() {
for {
//读取
value, ok := <-c
if ok {
fmt.Println(value)
} else {
fmt.Println("channel close")
time.Sleep(2 * time.Second)
}
}
}()
c <- 4
close(c)
time.Sleep(2 * time.Second)
}
14.2 channel的for range
- 这样一条
for语句会不断地尝试从chan种取出元素值,即使chan被关闭,它也会在取出所有剩余的元素值之后再结束执行。
package main
import (
"fmt"
)
func main() {
num := 5
ch := make(chan int, num)
for i := 0; i < num; i++ {
ch <- i
}
close(ch)
for elem := range ch {
fmt.Printf("The element is: %v\n", elem)
}
fmt.Println("666")
}
- 当chan中没有元素值时,它会被阻塞在有
for关键字的那一行,直到有新的元素值可取。
package main
import (
"fmt"
"time"
)
func main() {
ch := make(chan int, 5)
go func() {
for elem := range ch {
fmt.Printf("The element is: %v\n", elem)
}
}()
time.Sleep(5 * time.Second)
fmt.Println("start send element")
for i := 0; i < 5; i++ {
ch <- i
}
close(ch)
fmt.Println("close channel")
time.Sleep(1 * time.Second)
}
- 假设
chan的没有赋值,那么它会被永远阻塞在有for关键字的那一行。
/*
这段代码会报fatal error: all goroutines are asleep - deadlock!
所有协程都阻塞,for后面的代码运行不到了
*/
package main
import (
"fmt"
)
func main() {
var ch chan int
for elem := range ch {
fmt.Printf("The elem is: %v\n", elem)
}
fmt.Println("666")
}
14.3 select
select语句是专为通道而设计的,每个case表达式中都只能包含操作通道的表达式,接收表达式和发送表达式。- 进入select语句时,所有case的chan都阻塞情况下,会执行default语句
select语句发现同时有多个候选case分支满足选择条件,那么它就会用一种伪随机的算法在这些分支中选择一个并执行- 仅当
select语句中的所有case表达式都被求值完毕后,它才会开始选择候选分支。 - select中也是有break语法,在for循环中的select要注意,break是跳出最近的select、switch、for。请注意break的坑
//多运行几次该程序,可以看到select的case分支是随机的
//多运行几次该程序,可以看到select的case分支是随机的
package main
import (
"fmt"
"math/rand"
"time"
)
func send(ch chan int) {
value := rand.Int31n(9999)
ch <- int(value)
}
func main() {
rand.Seed(time.Now().UnixNano())
ch1 := make(chan int, 1)
ch2 := make(chan int, 1)
send(ch1)
send(ch2)
time.Sleep(1000)
timer := time.NewTimer(3 * time.Second).C
loop:
for {
select {
case v := <-ch1:
fmt.Println("ch1通道获取到数据", v)
case v := <-ch2:
fmt.Println("ch2通道获取到数据", v)
case <-timer:
fmt.Println("退出for循环")
break loop
//default:
// time.Sleep(1000)
// fmt.Println("通道中没有数据")
}
}
}
注意:case表达式都是先求值的,可以实验下面例子
package main
import (
"fmt"
"math/rand"
"time"
)
func send(ch chan int) {
value := rand.Int31n(9999)
ch <- int(value)
}
func recv(ch chan int, id int) <-chan int {
fmt.Println(id, "在这里可以进行一些逻辑")
return ch
}
func main() {
rand.Seed(time.Now().UnixNano())
ch1 := make(chan int, 1)
ch2 := make(chan int, 1)
send(ch1)
send(ch2)
time.Sleep(1000)
timer := time.NewTimer(3 * time.Second).C
loop:
for {
select {
case v := <-recv(ch1, 1):
fmt.Println("ch1通道获取到数据", v)
case v := <-recv(ch2, 2):
fmt.Println("ch2通道获取到数据", v)
case <-timer:
fmt.Println("退出for循环")
break loop
//default:
// fmt.Println("通道中没有数据")
}
}
}
/*结果
1 在这里可以进行一些逻辑
2 在这里可以进行一些逻辑
ch1通道获取到数据 795
1 在这里可以进行一些逻辑
2 在这里可以进行一些逻辑
ch2通道获取到数据 5534
1 在这里可以进行一些逻辑
2 在这里可以进行一些逻辑
退出for循环
*/
14.4 单方向channel的应用
var send chan<- int// 只能发送
var recv <-chan int // 只能接收
// 一般用于接口中约束某个方法中的channel只能用于发送数据或者接收数据,在传入参数的时候是可以传双向的channel
type Notifier interface {
SendInt(ch chan<- int)
RcevInt(ch <-chan int)
}

16万+

被折叠的 条评论
为什么被折叠?



