GO语言基础教程

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 表示拥有 nT 类型的值的数组。
  • 数组的长度是其类型的一部分,数组不能改变大小
  • 表达式
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]

keym 中,oktrue ;否则,okfalse

key 不在map中,那么 elem 是该map元素类型零。

同样的,当从map中读取某个不存在的键时,结果是map射的元素类型的零值。

:若 elemok 还未声明,你可以使用短变量声明:

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,包外部可见

  • 参数传递是值传递

    基础类型、数组、结构体,在函数内修改,不会影响到原来的值。

    slicemapchannel本身是引用类型,在函数内修改,会影响原来的值

  • 函数也可以当做变量

  • 可变参数,用 表示

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 将会是其底层值,而 oktrue

否则,ok 将为 falset 将为 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。在 TS 的情况下,变量 v 会分别按 TS 类型保存 i 拥有的值。在默认(即没有匹配)的情况下,变量 vi 的接口类型和值相同。

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)
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值