Go语言笔记

Go 语言笔记

Go语言环境搭建(Ubuntu版)

  • 下载 Go 语言安装包
# 国内下载地址
# https://golang.google.cn/
# 这里我们下载linux版本
wget https://dl.google.com/go/go1.14.2.linux-amd64.tar.gz -O /tmp/
  • 安装 Go 解释器
  1. 解压文件到指定目录
sudo tar xf /tmp/go*.tar.gz -C /usr/local/
mv /usr/local/go* /usr/local/go
  1. 配置 Go 环境变量

几个环境变量介绍:

GOROOT 目录是go的安装目录

GOPATH 目录,即go的代码存放目录,类似于其他语言IDEworkspace, 其下会有三个子目录:

1. src 里面的每一个子目录,就是一个包。包内是Go的源码文件
2. pkg 编译后生成的,包的目标文件
3. bin 生成的可执行文件。

Ubuntu中可以通过编辑$HOME/.profile文件来配置环境变量:

export GOROOT="/usr/local/go"
export GOPATH="$HOME/go" 
export PATH="$GOROOT/bin:$PATH"

Go 源码文件

Go 源码文件分为三类,分别是:命令源码文件、库源码文件、测试源码文件。

  1. 命令源码文件

声明自己属于main包,并且包含无参数声明和结果的main函数。

命令源码文件被安装后,GOPATH如果只有一个工作区,那么相应的可执行文件会被存放在当前工作区的bin文件夹下;如果有多个工作区,就会被安装到GOBIN指向的目录下。

命令源码文件就是Go程序的入口。

多个源码文件虽然可以被单独通过go run命令执行,但是无法通过go buildgo install。所以命令源码文件需要单独放在一个代码包中。

  1. 库源码文件

库源码文件就是不具备命令源码文件的特征,即不属于main包也不包含main函数。它是存放于某个代码包中的普通源码文件。库源码文件被安装后,相应的归档文件(.a文件)会被存放到当前工作区的pkg相关目录下。

  1. 测试源码文件

名称以_ test.go为后缀的代码文件,并且必须包含Test或者Benchmark名称前缀的函数:

func TestXXX(t *testing.T) {
    
}

名称以Test为名称前缀的函数,只能接受*testing.T的参数,这种测试函数是功能测试函数。

func BenchmarkXXX(b *testing.B) {
    
}

总结:

命令源码文件是可以单独运行的,可以使用go run命令直接运行,也可以通过go buildgo install命令得到相应的可执行文件。所以命令源码文件是可以在机器的任何目录下运行的。

Go 命令

通用的命令标记,对go命令都适用的参数:

名称说明
-a用于强制重新编译所有涉及的 Go 语言代码包(包括 Go 语言标准库中的代码包),即使它们已经是最新的了。该标记可以让我们有机会通过改动底层的代码包做一些实验。
-n使命令仅打印其执行过程中用到的所有命令,而不去真正执行它们。如果不只想查看或者验证命令的执行过程,而不想改变任何东西,使用它正好合适。
-race用于检测并报告指定 Go 语言程序中存在的数据竞争问题。当用 Go 语言编写并发程序的时候,这是很重要的检测手段之一。
-v用于打印命令执行过程中涉及的代码包。这一定包括我们指定的目标代码包,并且有时还会包括该代码包直接或间接依赖的那些代码包。这会让你知道哪些代码包被执行过了。
-work用于打印命令执行时生成和使用的临时工作目录的名字,且命令执行完成后不删除它。这个目录下的文件可能会对你有用,也可以从侧面了解命令的执行过程。如果不添加此标记,那么临时工作目录会在命令执行完毕前删除。
-x使命令打印其执行过程中用到的所有命令,并同时执行它们。

go run

go run 专门用来运行命令源码文件的命令,注意,这个命令不是用来运行所有 Go 的源码文件的!

go run 命令只能接受一个命令源码文件以及若干个库源码文件(必须同属于 main 包)作为文件参数,且不能接受测试源码文件。它在执行时会检查源码文件的类型。如果参数中有多个或者没有命令源码文件,那么 go run 命令就只会打印错误提示信息并退出,而不会继续执行。

这个命令具体干了些什么事情呢?来分析分析,我们先重新创建一个新文件:mytest.go,并加入以下代码:

package main
import "fmt"
func main(){
    fmt.Println("HelloWorld")
    fmt.Println("你好,Go!!!")
}

执行go run 配合-n可以查看详细执行结果。

go build

go build 命令主要是用于测试编译。在包的编译过程中,若有必要,会同时编译与之相关联的包。

  1. 如果是普通包,当你执行go build命令后,不会产生任何文件。
  2. 如果是main包,当只执行go build命令后,会在当前目录下生成一个可执行文件。如果需要在$GOPATH/bin目录下生成相应的exe文件,需要执行go install 或者使用 go build -o 路径/可执行文件
  3. 如果某个文件夹下有多个文件,而你只想编译其中某一个文件,可以在 go build 之后加上文件名,例如 go build a.gogo build 命令默认会编译当前目录下的所有go文件。
  4. 你也可以指定编译输出的文件名。比如,我们可以指定go build -o 可执行文件名,默认情况是你的package名(非main包),或者是第一个源文件的文件名(main包)。
  5. go build 会忽略目录下以_或者.开头的go文件。
  6. 如果你的源代码针对不同的操作系统需要不同的处理,那么你可以根据不同的操作系统后缀来命名文件。

当代码包中有且仅有一个命令源码文件的时候,在文件夹所在目录中执行 go build 命令,会在该目录下生成一个与目录同名的可执行文件。

go build 用于编译我们指定的源码文件或代码包以及它们的依赖包。但是注意如果用来编译非命令源码文件,即库源码文件,go build 执行完是不会产生任何结果的。这种情况下,go build 命令只是检查库源码文件的有效性,只会做检查性的编译,而不会输出任何结果文件。

go build 编译命令源码文件,则会在该命令的执行目录中生成一个可执行文件。

go build 后面不追加目录路径的话,它就把当前目录作为代码包并进行编译。go build 命令后面如果跟了代码包导入路径作为参数,那么该代码包及其依赖都会被编译。

go mod

其他命令

go fix 用来修复以前老版本的代码到新版本,例如go1之前老版本的代码转化到go1

go version 查看go当前的版本

go env 查看当前go的环境变量

go list 列出当前全部安装的package

Go 语言标识符

标识符与关键字

标识符

在编程语言中标识符就是程序员定义的具有特殊意义的词,比如变量名、常量名、函数名等等。 Go 语言中标识符由字母数字和_(下划线)组成,并且只能以字母和_开头。 举几个例子:abc, _, _123, a123

关键字

关键字是指编程语言中预先定义好的具有特殊含义的标识符。 关键字和保留字都不建议用作变量名。

Go 语言中有 25 个关键字:

    break        default      func         interface    select
    case         defer        go           map          struct
    chan         else         goto         package      switch
    const        fallthrough  if           range        type
    continue     for          import       return       var

此外,Go 语言中还有 37 个保留字。

    Constants:    true  false  iota  nil

        Types:    int  int8  int16  int32  int64
                  uint  uint8  uint16  uint32  uint64  uintptr
                  float32  float64  complex128  complex64
                  bool  byte  rune  string  error

    Functions:   make  len  cap  new  append  copy  close  delete
                 complex  real  imag
                 panic  recover

Go 语言变量

变量的来历

程序运行过程中的数据都是保存在内存中,我们想要在代码中操作某个数据时就需要去内存上找到这个变量,但是如果我们直接在代码中通过内存地址去操作变量的话,代码的可读性会非常差而且还容易出错,所以我们就利用变量将这个数据的内存地址保存起来,以后直接通过这个变量就能找到内存上对应的数据了。

变量类型

变量(Variable)的功能是存储数据。不同的变量保存的数据类型可能会不一样。经过半个多世纪的发展,编程语言已经基本形成了一套固定的类型,常见变量的数据类型有:整型、浮点型、布尔型等。

Go 语言中的每一个变量都有自己的类型,并且变量必须经过声明才能开始使用。

Go 语言数据类型

整数类型

  • 示例
package main

import "fmt"

// 整型
func main() {
	// 十进制
	var i1 = 101
	fmt.Printf("%d\n", i1)
	fmt.Printf("%b\n", i1) // 十进制转二进制
	fmt.Printf("%o\n", i1) // 十进制转八进制
	fmt.Printf("%x\n", i1) // 十进制转十六进制

	// 八进制
	i2 := 0o77
	fmt.Printf("%d\n", i2)

	// 十六进制
	i3 := 0x7ff8
	fmt.Printf("%d\n", i3)
	// 查看变量的类型
	fmt.Printf("%T\n", i3)

	// 声明int8类型的变量
	i4 := int8(9) // 明确指定int8类型,否则默认int类型
	fmt.Printf("%T\n", i4)
}

浮点型

  • 示例
package main

import "fmt"

// 浮点型
func main() {
	// math.MaxFloat32 => float32 最大值
	f1 := 3.1415926
	fmt.Printf("%T\n", f1)

}

格式化输出

// fmt占位符
func main() {
varn=100
//查看类型
fmt. . Printf("%T\n", n)查看变量类型
fmt. Printf("%v\n", n) 查看变量的值
fmt. Printf("%b\n", n)表示二进制
fmt.Printf("%d\n", n)表示十进制
fmt. Printf("%o\n", n)表示八进制
fmt. Printf("%x\n",明表示十六进制
vars="Hello沙河!
fmt. Printf("字符串: %s\n", s)
fmt. Printf("字符串: %v\n", s)
fmt. Printf("字符串: %#v\n", s)
}

字符串

Go 语言中字符串是用双引号包裹

Go 语言中字符是用单引号包裹

package main

import (
	"fmt"
	"strings"
)

func main() {
	path := "D:\\Go\\code\\day01"
	fmt.Println(path)

	// 多行字符串
	s2 := `
		但愿人长久
		千里共婵娟
	`
	fmt.Println(s2)

	// 字符串长度
	s3 := "hello I'm lina"
	fmt.Println(len(s3))

	// 字符串拼接
	s4 := "hello "
	fmt.Println(s4 + "Go")

	// 分割字符串
	s5 := "a,b,c,d"
	fmt.Println(strings.Split(s5, ","))

	// 包含
	fmt.Println(strings.Contains(s5, "a,"))
	fmt.Println(strings.Contains(s5, "ab"))

	// 前缀
	s6 := "大器何必晚成,赢在当下"
	fmt.Println(strings.HasPrefix(s6, "大器"))
	fmt.Println(strings.HasPrefix(s6, "大气"))

	// 后缀
	fmt.Println(strings.HasSuffix(s6, "完成"))
	fmt.Println(strings.HasSuffix(s6, "当下"))

    // 获取子串的索引
	s7 := "abcde"
	fmt.Println(strings.Index(s7, "c"))
	fmt.Println(strings.Index(s7, "bc"))

}

数组

package main

import "fmt"

/*
数组
	- 存放元素的的容器
	- 必须指定存放元素的类型和容量
	- 数组长度是数组类型的一部分
*/

func arrayDemo01() {
	var a1 [3]bool
	fmt.Printf("a1:%T a1:%v\n", a1, a1)

	// 数组的初始化
	// 如果不初始化,默认元素都是零值(布尔值:false,整型和浮点型都是0,字符串:"")
	// 1. 初始化方式1
	var a2 = [3]bool{true, false, true}
	fmt.Println(a2)
	// 2. 初始化方式2:根据初始值自动判断数组长度进行初始化
	a3 := [...]int{0, 4, 2, 65, 87, -1, 3, 8}
	fmt.Println(a3)
	// 3. 初始化方式3:根据索引来初始化
	a4 := [5]string{"a", "b", "c", "d", "e"}
	fmt.Println(a4)
}

func arrayDemo02() {
	// 数组遍历
	citys := [...]string{"北京", "上海", "深圳"}
	// 1、根据索引进行遍历
	for i := 0; i < len(citys); i++ {
		fmt.Println(citys[i])
	}
	// 2、for range 遍历
	for i, v := range citys {
		fmt.Println(i, v)
	}
}

// 多维数组
func arrayDemo03() {
	citys := [3][2]string{
		[2]string{"海淀区", "丰台区"},
		[2]string{"金水区", "二七区"},
		[2]string{"余杭区", "西湖区"},
	}
	// 遍历多维数组
	for _, v1 := range citys {
		for _, v2 := range v1 {
			fmt.Printf("%v ", v2)
		}
		fmt.Println()
	}
}

// 数组是值类型
func arrayDmeo04() {
	b1 := [3]int{1, 2, 3}
	b2 := b1
	b2[0] = 10
	fmt.Println(b1) // [1 2 3]
	fmt.Println(b2) // [10 2 3]
}

// 数组练习
func arrayDemo05() {
	// 1. 求数组[1, 3, 5, 7, 9]中所有元素的和
	a1 := [...]int{1, 3, 5, 7, 9}
	sum := 0
	for _, v1 := range a1 {
		sum = sum + v1
	}
	fmt.Println(sum)
}

func main() {
	//arrayDemo01()
	//arrayDemo02()
	//arrayDemo03()
	//arrayDmeo04()
	arrayDemo05()
}

切片

  • 切片( Slice )是一个拥有相同类型元素的可变长度的序列。它是基于数组类型做的一层封装。它非常灵活,支持自动扩容。
  • 切片是一个引用类型,它的内部结构包含地址长度容量。切片一般用于快速地操作一块数据集合。
package main

import (
	"fmt"
)

func sliceDmeo01() {
	// 切片的定义
	var s1 []int    // 定义一个int类型的切片
	var s2 []string // 定义一个字符串类型的切片
	fmt.Println(s1, s2)
	fmt.Println(s1 == nil)
	fmt.Println(s2 == nil)
	// 切片的初始化
	s1 = []int{1, 2, 3}
	s2 = []string{"余杭", "西湖", "江滨"}
	fmt.Println(s1, s2)
}

func sliceDemo02() {
	// 从数组得到切片
	a1 := [...]int{1, 3, 5, 7, 9}
	s1 := a1[0:3]
	fmt.Println(s1)
	// 切片长度和容量, 切片的容量是指从索引0到最后的底层数组的容量
	fmt.Printf("len(s1):%d cap(s1):%d\n", len(s1), cap(s1))
}

func main() {
	sliceDmeo01()
	sliceDemo02()
}
  • 使用make()函数构造切片

切片的本质

切片就是一个框,框住了一块连续的内存。真正的数据都是保存在底层数组里的。
func sliceDemo03() {
	// 使用make()函数创建切片
	s1 := make([]int, 5, 10)
	// len(s1):5 cap(s1):10
	fmt.Printf("len(s1):%v cap(s1):%v\n", len(s1), cap(s1))

	// 切片的赋值
	s3 := []int{1, 3, 5}
	s4 := s3            // s3和s4指向了同一个底层数组
	fmt.Println(s3, s4) //[1 3 5] [1 3 5]
	s3[0] = 10
	fmt.Println(s3, s4) //[10 3 5] [10 3 5]
}
  • 切片不能直接比较
切片之间是不能比较的,我们不能使用|==操作符来判断两个切片是否含有全部相等元素。切片唯一 合法的比较操作是和nil比较。一个nil 值的切片并没有底层数组,一个nil值的切片的长度和容量都是0。但是我们不能说一个长度和容量都是0的切片一定是nil。
  • 使用append()方法向切片追加元素
func sliceDemo04() {
	s1 := []string{"北京", "上海", "广州"}
	fmt.Println(s1)
	fmt.Printf("len(s1):%v cap(s1):%v\n", len(s1), cap(s1))
	// 调用append()函数必须使用原来的切片变量接受返回值
	s1 = append(s1, "深圳")
	fmt.Println(s1)
	// append()追加元素,原来的底层数组放不下时,Go会把底层数组替换一个
	fmt.Printf("len(s1):%v cap(s1):%v\n", len(s1), cap(s1))
	/*
	结论:
		- append()函数将元素追加到切片后会返回该元素
		- 切片的numberSlice的容量会按照1,2,4,8,16进行自动扩容,每次扩容都是扩容前的两倍
	*/

    s2 := []int{1, 2, 3, 4, 5}
	s3 := []int{0, -1, -2, -3}
	// ... 表示展开结果
	s2 = append(s2, s3...)
	fmt.Println(s2, s3)
}
  • 扩容策略
- 首先判断, 如果新申请容量(cap)大于2倍的旧容量(old.cap),最终容量(newcap)就是新申请的容量
(cap);
- 否则判断 ,如果旧切片的长度小于1024,则最终容量(newcap)就是旧容量(old.cap)的两倍,即
(newcap=doublecap);
- 否则判断,如果旧切片长度大于等于1024 ,则最终容量(newcap)从旧容量( old.cap )开始循环增加原来的
1/4 ,即(newcap=old.cap,for {newcap += newcap/4} )直到最终容量(newcap )大于等于新申请的容量(cap),即(newcap>=cap);
- 如果最终容量(cap)计算值溢出,则最终容量(cap)就是新申请容量(cap)。


需要注意的是,切片扩容还会根据切片中元素的类型不同而做不同的处理,比如int和string类型的处理方式就不一样。

  • 使用copy()函数复制切片
func sliceDemo05() {
	a1 := []int{1,2,3}
	a2 := a1 // 简单赋值
	a1[0] = 10
	var a3 = make([]int, 3, 3)
	copy(a3, a1)
	fmt.Println(a3, a1, a2)

    // 删除索引为1的元素
	a3 = append(a1[:1], a1[2:]...)
	fmt.Println(a3)
}

指针

Go 语言中的指针需要记住两个符号:

  • & 取地址符号
  • * 根据地址取值

取地址操作符&和取值操作符*是一对互补操作符,&取出地址,* 根据地址取出地址指向的值。Go 语言中指针只能读取不能修改,即不能修改地址值。
变量、指针地址、指针变量、取地址、取值的相互关系和特性如下:

  • 对变量进行取地址(&)操作,可以获得这个变量的指针变量。
  • 指针变量的值是指针地址。
  • 对指针变量进行取值(*)操作,可以获得指针变量指向的原变量的值。
func pointerDemo01() {
	n := 10
	p := &n
	fmt.Printf("%T\n", p)
	v := *p
	fmt.Printf("%v\n", v)
}

func pointerDemo02() {
	// new()函数申请一个内存地址
	var a = new(int) // 创建一个int类型的指针变量
	*a = 100
	fmt.Println(*a)
}
make
make也是用于内存分配的,区别于new,它只用于slice、map以及chan的内存创建,而且它返回的类型就是这三个类型本身,而不是他们的指针类型,因为这三种类型就是引用类型,所以就没有必要返回他们的指针了。make函数的函数签名如下:
	func make(t Type, size ...IntegerType) Type

make函数是无可替代的,我们在使用slice、map以及channel的时候,都需要使用make进行初始化,然后才可以对它们进行操作。

make 和 new 的区别
- make 和 new 都是用来申请内存的
- new 比较少用,一般是用来给基本数据类型申请内存的,比如string、int等
- make 是用来给slice、map、chan申请内存的,返回值是其类型本身

map

map是一种无序的基于 key-value 的数据结构, Go 语言中的map是引用类型,必须初始化才能使用。

func maoDemo01() {
	// 定义map
	var m1 map[string]string
	// map必须先初始化才能使用
	m1 = make(map[string]string, 10) // 计算好内存,避免动态分配内存
	m1["地址"] = "北京"
	m1["姓名"] = "王琦"
	m1["年龄"] = "20"
	fmt.Println(m1)

	value, ok := m1["年龄"]
	if !ok {
		fmt.Println("没有此key")
	} else {
		fmt.Println(value)
	}

	// map 遍历
	for k, v := range m1 {
		fmt.Println(k, v)
	}

	// 遍历key
	for k := range m1 {
		fmt.Println(k)
	}
	// 遍历value
	for _, v := range m1 {
		fmt.Println(v)
	}
	// 删除
	delete(m1, "年龄")
	fmt.Println(m1) //map[地址:北京 姓名:王琦]
}

查看 go 文档

go doc builtin.delete
package builtin // import "builtin"

func delete(m map[Type]Type1, key Type)
    The delete built-in function deletes the element with the specified key
    (m[key]) from the map. If m is nil or there is no such element, delete is a
    no-op.

Go 语言函数

package main

import "fmt"

/*
函数
*/

// 函数的定义
func sum(x int, y int) (ret int) {
	return x + y
}

// 没有返回值
func func1(x int, y int) {
	fmt.Println(x + y)
}

// 没有参数没有返回值
func sayHello() {
	fmt.Println("Hello Golang!")
}

// 没有参数但有返回值
func func2() int {
	return 10
}

// 参数可以命名也可以不命名,命名返回值相当于在函数中声明了一个变量
func func3(x int, y int) (ret int) {
	ret = x + y
	// 此处可以省略ret
	return
}

// 有多个返回值
func func4() (int, string) {
	return 10, "十"
}

// 参数简写, 当参数的类型一致时,可以省却前边的,保留最后一个参数的类型
func func5(x, y int) int {
	return x - y
}

// 可变参数
// 注意:可变参数必须放在函数参数的最后
func func6(x string, y ...int) {
	fmt.Println(x)
	fmt.Println(y) // y 是一个切片
}

func main() {
	sum := sum(10, 20)
	fmt.Println(sum)

	s1 := func3(100, 200)
	fmt.Println(s1)

	m, n := func4()
	fmt.Println(m, n)
	_, v1 := func4()
	fmt.Println(v1)

	s2 := func5(200, 100)
	fmt.Println(s2)

	func6("上班啦")
	func6("上班啦", 1, 2, 3, 4, 5)
}

作业:

package main

import (
	"fmt"
	"strings"
	"unicode"
)

func job1() {
	// 1. 判断字符串中汉字的数量
	// 定义字符串
	s1 := "Hello中国"
	// 全局计数器
	count := 0
	for _, v := range s1 {
		// 判断是否是汉字
		if unicode.Is(unicode.Han, v) {
			// 是汉字,计数器累加1
			count++
		}
	}
	// 打印结果
	fmt.Println(count)
}

func job2() {
	// 2. how do you do 判断单词出现的次数
	s1 := "how do you do"
	// 按空格切割字符串
	selice1 := strings.Split(s1, " ")
	// 遍历切片存储到一个map中
	m1 := make(map[string]int, 10)
	for _, v:= range selice1 {
		// 如果map中存在
		if _, ok := m1[v]; !ok {
			m1[v] = 1
		} else {
			m1[v] ++
		}
	}
	// 累加出现的次数
	// 计数器
	count := 0
	for _, v := range m1 {
		count = count + v
	}
	fmt.Println(count)
}

func main() {
	//job1()
	job2()
}

函数定义和 defer 语句

定义函数

package main

import "fmt"

// 函数:一段代码的封装

func f1() {
	fmt.Println("Hello Golang")
}

func f2(name string) {
	fmt.Println(name)
}

// 参数简写
func f3(x, y int) int {
	sum := x + y
	return sum
}

// 可变长参数, y是一个int类型的切片
func f4(x string, y ...int) int {
	fmt.Println(x, y)
	return 1
}

// 命名返回值
func f5(x, y int) (sum int) {
	sum = x + y
	return
}

func main() {
	f1() // 函数调用
	f2("九尾狐")
	sum := f3(10, 20)
	fmt.Println(sum)
	ret := f4("赵力肖", 1, 2, 3, 4, 5, 6, 7, 8)
	fmt.Println(ret)
	fmt.Println(f5(100, 200))
}

defer语句

Go 语言中的defer语句会将其后面跟随的语句进行延迟处理。在defer归属的函数即将返回时,将延迟处理的语句按defer定义的逆序进行执行,也就是说,先被defer的语句最后被执行,最后被defer的语句,最先被执行。

func deferDemo01() {
	fmt.Println("start")
	// defer 把它后边的语句延迟到函数即将返回
	// 一个函数可以有多个defer语句
	// 多个语句按先进后出的延迟顺序执行
	defer fmt.Println("nice")
	defer fmt.Println("good")
	defer fmt.Println("right")
	fmt.Println("end")
}

defer执行时机

在 Go 语言中return 语句在底层并不是原子操作,它分为给返回值赋值和RET指令两步。而defer语句执行的时机就在返回值赋值操作后, RET指令执行前,即defer语句在两者之间执行。

defer经典案例

// Go语言函数中的return 不是原子操作,在底层分为两步操作:
// 1. 返回值赋值
// defer 的执行介于第一层和第二层之间
// 2. 真正的RET返回

func f1() int {
	x := 5
	defer func() {
		x++
	}()
	return x // 返回值 = x = 5
}

func f2() (x int) {
	defer func() {
		x++
	}()
	return 5 // 返回值 = 5 = x = 6
}

func f3() (y int) {
	x := 5
	defer func() {
		x++
	}()
	return x // 返回值赋值
}
func f4() (x int) {
	defer func(x int) {
		x++
	}(x)
	return 5
}
func main() {
	fmt.Println(f1())
	fmt.Println(f2())
	fmt.Println(f3())
	fmt.Println(f4())
}

函数作用域

全局变量

全局变量是定义在函数外部的变量,它在程序整个运行周期内都有效。 在函数中可以访问到全局变量。

package main

import "fmt"

//定义全局变量num
var num int64 = 10

func testGlobalVar() {
	fmt.Printf("num=%d\n", num) //函数中可以访问全局变量num
}
func main() {
	testGlobalVar() //num=10
}

局部变量

局部变量又分为两种: 函数内定义的变量无法在该函数外使用,例如下面的示例代码 main 函数中无法使用testLocalVar函数中定义的变量 x:

func testLocalVar() {
	//定义一个函数局部变量x,仅在该函数内生效
	var x int64 = 100
	fmt.Printf("x=%d\n", x)
}

func main() {
	testLocalVar()
	fmt.Println(x) // 此时无法使用变量x
}

如果局部变量和全局变量重名,优先访问局部变量。

package main

import "fmt"

//定义全局变量num
var num int64 = 10

func testNum() {
	num := 100
	fmt.Printf("num=%d\n", num) // 函数中优先使用局部变量
}
func main() {
	testNum() // num=100
}

接下来我们来看一下语句块定义的变量,通常我们会在 if 条件判断、for 循环、switch 语句上使用这种定义变量的方式。

func testLocalVar2(x, y int) {
	fmt.Println(x, y) //函数的参数也是只在本函数中生效
	if x > 0 {
		z := 100 //变量z只在if语句块生效
		fmt.Println(z)
	}
	//fmt.Println(z)//此处无法使用变量z
}

还有我们之前讲过的 for 循环语句中定义的变量,也是只在 for 语句块中生效:

func testLocalVar3() {
	for i := 0; i < 10; i++ {
		fmt.Println(i) //变量i只在当前for语句块中生效
	}
	//fmt.Println(i) //此处无法使用变量i
}

函数类型与变量

定义函数类型

我们可以使用type关键字来定义一个函数类型,具体格式如下:

type calculation func(int, int) int

上面语句定义了一个calculation类型,它是一种函数类型,这种函数接收两个 int 类型的参数并且返回一个 int 类型的返回值。

简单来说,凡是满足这个条件的函数都是 calculation 类型的函数,例如下面的 add 和 sub 是 calculation 类型。

func add(x, y int) int {
	return x + y
}

func sub(x, y int) int {
	return x - y
}

add 和 sub 都能赋值给 calculation 类型的变量。

var c calculation
c = add

匿名函数

// 匿名函数
var f1 = func(x, y int) {
	fmt.Println(x + y)
}

func main() {
	f1(10, 20)
	// 在函数内部,没办法声明带名字的函数
	f2 := func(x, y int) {
		fmt.Println(x - y)
	}
	f2(20, 10)
	// 如果函数只调用一次,还可以简写成立即执行函数
	func() {
		fmt.Println("Hello Golang")
	}()
}

闭包

// 闭包
func f1(f func()) {
	fmt.Println("this is f1")
	f()
}

func f2(x, y int) {
	fmt.Println("this is f2")
	fmt.Println(x + y)
}

// 定义一个函数对f2进行包装
func f3(f func(int, int)) func() {
	tmp := func() {
		f(10, 20)
	}
	return tmp
}

func main() {
	f1(f3(f2))
}
闭包是什么?
闭包是一个函数,这个函数包含了他外部作用域的一个变量

底层的原理:
1. 函数可以作为返回值
2. 函数内部查找变量的顺序,先在自己内部找,找不到往外层找

闭包 = 函数 + 外部变量的用

Go 内置函数

内置函数介绍
close主要用来关闭channel
len用来求长度,比如string、array、slice、map、channel
new用来分配内存,主要用来分配值类型,比如int、struct。返回的是指针
make用来分配内存,主要用来分配引用类型,比如chan、map、slice
append用来追加元素到数组、slice中
panic和recover用来做错误处理

panic/recover

Go语言中目前(Go1.12)是没有异常机制,但是使用panic/recover模式来处理错误。 panic可以在任何地方引发,但recover只有在defer调用的函数中有效。 首先来看一个例子:

package main

import "fmt"

func main() {
	funA()
	funB()
	funC()
}

func funA() {
	fmt.Println("A函数")
}

func funB() {
	defer func() {
		//如果程序出出现了panic错误,可以通过recover恢复过来
		err := recover()
		if err != nil {
			fmt.Println(err)
			fmt.Println("崩溃已修复")
		}
	}()
	panic("出现严重错误!")
	fmt.Println("B函数")
}

func funC() {
	fmt.Println("C函数")
}

注意:

  1. recover()必须搭配defer使用。
  2. defer一定要在可能引发panic的语句之前定义。

Go语言fmt包的使用

fmt

fmt包实现了类似C语言printfscanf的格式化I/O。主要分为向外输出内容和获取输入内容两大部分。

package main

import "fmt"

func main() {
	fmt.Println("沙河娜扎")
	fmt.Print("沙河")
	fmt.Print("娜扎")
	fmt.Println()
	/*
		fmt.Printf() 打印格式化字符串
		%T 查看类型
		%f 浮点数
		%d 十进制
		%b 二进制
		%o 八进制
		%x 十六进制
		%U 表示Unicode格式
		%c 字符
		%s 字符串
		%p 指针
		%v 值的默认格式
		%+v 类似%v,但输出结构体时会添加字符名
		%#v 值的Go语法表示
		%T 值的类型
		%% 百分号
		%t 布尔值

		字符串和[]byte
		%s 直接输出字符串或[]byte

		宽度标识符
		- 宽度通过一个紧跟在百分号后面的十进制数指定,如果未指定宽度,则表示值时除必需之外不作填充。精度通过(可选的)
		- 宽度后跟点号后跟的十进制数指定。如果未指定精度,会使用默认精度;如果点号后没有跟数字,表示精度为0。举例如下:
		%f	默认宽度,默认精度
		%9f		宽度9,默认精度
		%.2f	默认宽度,精度2
		%9.2f 宽度9,精度2
		%9.f	宽度9,精度0
	*/
	m1 := make(map[string]int, 1)
	m1["Id"] = 1001
	fmt.Printf("%v\n", m1)
	fmt.Printf("%#v\n", m1)

	fmt.Printf("%5.2s\n", "沙河娜扎")
	fmt.Printf("%6.4f\n", 3.1415926)

}
  • 获取输入

Go语言fmt包下有fmt.Scanfmt.Scanffmt.Scanln三个函数,可以在程序运行过程中从标准输入获取用户的输入。

fmt.Scan

函数定签名如下:

func Scan(a ...interface{}) (n int, err error)
  • Scan从标准输入扫描文本,读取由空白符分隔的值保存到传递给本函数的参数中,换行符视为空白符。
  • 本函数返回成功扫描的数据个数和遇到的任何错误。如果读取的数据个数比提供的参数少,会返回一个错误报告原因。
func main() {
	// 获取用户输入
	var (
		// name      string
		numID     int
		className string
	)
	// fmt.Print("请输入你的姓名:")
	// fmt.Scan(&name)
	// fmt.Println(name)
	fmt.Print("请输入学号和班级:")
	fmt.Scanf("%d %s", &numID, &className)
	fmt.Println(numID, className)
}

Go 语言结构体

Go语言中的基础数据类型可以表示一些事物的基本属性,但是当我们想表达一个事物的全部或部分属性时,这时候再用单一的基本数据类型明显就无法满足需求了,Go语言提供了一种自定义数据类型,可以封装多个基本数据类型,这种数据类型叫结构体,英文名称struct。 也就是我们可以通过struct来定义自己的类型了。

结构体的定义

使用typestruct关键字来定义结构体,具体代码格式如下:

type 类型名 struct {
    字段名 字段类型
    字段名 字段类型
    …
}

其中:

  • 类型名:标识自定义结构体的名称,在同一个包内不能重复。
  • 字段名:表示结构体字段名。结构体中的字段名必须唯一。
  • 字段类型:表示结构体字段的具体类型。
package main

import "fmt"

type person struct {
	name string
	age int
	gender string
	hobby []string
}

func main() {
	var p person
	p.name = "沙河娜扎"
	p.age = 18
	p.gender = "女"
	p.hobby = []string{"篮球","音乐","电影"}
	fmt.Printf("%#v\n", p)
}

匿名结构体

在定义一些临时数据结构等场景下还可以使用匿名结构体。

package main
     
import (
    "fmt"
)
     
func main() {
    var user struct{Name string; Age int}
    user.Name = "沙河娜扎"
    user.Age = 18
    fmt.Printf("%#v\n", user)
}

Go语言接口

  • 在Go语言中接口(interface)是一种类型,一种抽象的类型。

  • interface是一组method的集合,是duck-type programming的一种体现。接口做的事情就像是定义一个协议(规则),只要一台机器有洗衣服和甩干的功能,我就称它为洗衣机。不关心属性(数据),只关心行为(方法)。

为了保护你的Go语言职业生涯,请牢记接口(interface)是一种类型。

接口的定义

Go语言提倡面向接口编程。

每个接口由数个方法组成,接口的定义格式如下:

type 接口类型名 interface{
    方法名1( 参数列表1 ) 返回值列表1
    方法名2( 参数列表2 ) 返回值列表2}

其中:

  • 接口名:使用type将接口定义为自定义的类型名。Go语言的接口在命名时,一般会在单词后面添加er,如有写操作的接口叫Writer,有字符串功能的接口叫Stringer等。接口名最好要能突出该接口的类型含义。
  • 方法名:当方法名首字母是大写且这个接口类型名首字母也是大写时,这个方法可以被接口所在的包(package)之外的代码访问。
  • 参数列表、返回值列表:参数列表和返回值列表中的参数变量名可以省略。

举个例子:

type writer interface{
    Write([]byte) error
}

当你看到这个接口类型的值时,你不知道它是什么,唯一知道的就是可以通过它的Write方法来做一些事情。

package main

import (
	"fmt"
)

// 接口2
// 不管什么牌子的车,都能跑
type car interface {
	run()
}

// 英菲尼迪
type yfnd struct {
	brand string
}

func (y yfnd) run() {
	fmt.Println("英菲尼迪跑")
}

// 保时捷
type porsche struct {
	brand string
}

func (p porsche) run() {
	fmt.Println("保时捷跑")
}

func drive(c car) {
	c.run()
}

func main() {
	var y1 yfnd = yfnd{
		"英菲尼迪",
	}
	var p1 porsche = porsche{
		"保时捷",
	}
	y1.run()
	p1.run()
	drive(y1)
	drive(p1)
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值