<!-- 注意* -->
-
初始化工程
-
go mod init GoDemo
-
-
结构体,接口
-
type i struct{} type i interface{}
-
-
条件,选择
-
循环
-
键值对
-
make(map[string]int)
-
-
切片,集合
-
make([]int,10)
-
-
函数
-
通道
Channel
-
make(chan int) ch <- v v := <-ch
-
-
Go协程
Goroutine
-
错误处理
-
defer func(){ recover() }()func except(){ recover() } func test(){ defer except() panic("runtime error") }
-
-
make
与new
的区别-
/* 都用来初始化内存 new返回指针类型 多用来初始化基本类型(bool.string.int...) make返回的是对应的类型 用来初始化slice.map.channel类型 */
-
package mainimport "fmt"func main() {// var num string// _,_=fmt.Scan(&num)num := "-415"var s strings = ""for _, item := range num {switch item {case '0':s += "ling "case passdefault:s += "fu "}}fmt.Printf(s[:len(s)-1]) //注意最后的空格 fmt.Print(s[len(s)-1]) }
*<!--字符串可以用切片切除字符-->
*<!--字符串可以用索引取其中的字符-->
%v:使用默认格式输出变量的值 %#v:输出Go语言的反射类型表示 %T:输出变量的类型 %%:输出一个单独的%字符对于整数类型(int、uint、int64等): %b:输出整数的二进制表示 %d:输出整数的十进制表示 %o:输出整数的八进制表示 %x:输出整数的小写十六进制表示 %X:输出整数的大写十六进制表示对于浮点数和复数类型(float32、float64、complex64、complex128): %e:输出科学计数法表示,如-1234.456e+78 %E:输出科学计数法表示,但使用大写字母E,如-1234.456E+78 %f:输出不带指数部分的浮点数表示 %g:根据大小自动选择%e或%f,不输出多余的尾随零 %G:根据大小自动选择%E或%f,不输出多余的尾随零对于字符串和字节切片: %s:输出字符串的默认表示 %q:输出字符串的双引号表示,其中的特殊字符会转义对于布尔值: %t:输出true或false对于指针: %p:输出指针的十六进制表示这些格式说明符可以与宽度和精度修饰符一起使用,以控制输出的格式。例如,%5d表示输出的整数宽度至少为5个字符,%.2f表示输出浮点数时保留两位小数。
01_go run 及 go build
执行代码 使用 go run 命令
$ go run hello.go
可用 go build 命令生成二进制文件:
$ go build hello.go $ ls hello hello.go $ ./hello
02_环境安装
<!-- 详情见go安装及配置 -->
创建项目
-
创建
GoDemo
文件夹 -
执行
go mod init GoDemo
命令,初始化工程-
$ go mod init GoDemo
-
-
后创建
main.go
文件即可
03_结构
输入
package mainimport ("fmt" )func main() {var n intvar tag byte_, _ = fmt.Scanf("%d %c", &n, &tag) }
package mainimport "fmt"func main() {fmt.Println("输入您的名字:")var name stringfmt.Scan(&name)fmt.Println("你输入的名字是", name) }
fmt.Scanln(&name) // 读取一行直到换行符// bufio.Scanner 提供了一个更灵活和高效的方式来读取输入,特别是当处理大量数据或需要逐行读取时 func main() { scanner := bufio.NewScanner(os.Stdin) for scanner.Scan() { fmt.Println("Read line:", scanner.Text()) // 打印读取的每一行 } if err := scanner.Err(); err != nil { fmt.Fprintln(os.Stderr, "reading standard input:", err) } }// bufio.Reader 提供了一种底层的方式来读取输入,允许你以字节为单位或以特定的分隔符读取 func main() { reader := bufio.NewReader(os.Stdin) text, _ := reader.ReadString('\n') // 读取直到换行符 text = strings.TrimSpace(text) // 去除字符串两端的空白字符,包括换行符 }
package mainimport "fmt"func main() {/* 这是我的第一个简单的程序 */fmt.Println("Hello, World!") } /* 标识符以一个大写字母开头则可以被外部包的代码所使用(需先导入)*/ /* { 不能单独放在一行 */ /* 文件名与包名没有直接关系, 同一个文件夹下的文件只能有一个包名 */
当标识符(包括常量、变量、类型、函数名、结构字段等等)以一个大写字母开头,使用这种形式的标识符的对象就可以被外部包的代码所使用(客户端程序需要先导入这个包),这被称为导出(像面向对象语言中的 public);标识符如果以小写字母开头,则对包外是不可见的
笔记
创建GoDemo
文件夹,执行 go mod init GoDemo
命令,初始化工程
文件结构:
Test --helloworld.gomyMath --myMath1.go --myMath2.go
测试代码:
// helloworld.go package mainimport ( "fmt" "GoDemo/myMath" ) // GoDemo是go.mod中的modulefunc main(){fmt.Println("Hello World!")fmt.Println(mathClass.Add(1,1)) // mathClass.Add()是package mathClass中的.Add()fmt.Println(mathClass.Sub(1,1)) }
// myMath1.go package mathClass func Add(x,y int) int {return x + y }
// myMath2.go package mathClass func Sub(x,y int) int {return x - y }
04_基础语法
标记
Go 程序可以由多个标记组成,可以是关键字,标识符,常量,字符串,符号
行分隔符
fmt.Println("Hello, World!") fmt.Println("菜鸟教程:runoob.com")
fmt.Println("Hello, World!");fmt.Println("菜鸟教程:runoob.com") /* 不鼓励这种做法 */
注释
-
//
-
/**/
标识符
字母与数字以及下划线组成的序列 第一个字符不能是数字
不能是关键字
字符串连接
package main import "fmt" func main() {fmt.Println("Google" + "Runoob") }
关键字
下面列举了 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 语言还有 36 个预定义标识符
程序一般由关键字、常量、变量、运算符、类型和函数组成
程序中可能会使用到这些分隔符:括号 (),中括号 [] 和大括号 {}
程序中可能会使用到这些标点符号:.、,、;、: 和 …
格式化字符串
Go 语言中使用 fmt.Sprintf
或 fmt.Printf
格式化字符串并赋值给新串:
-
Sprintf
根据格式化参数生成格式化的字符串并返回该字符串。 -
Printf
根据格式化参数生成格式化的字符串并写入标准输出。
/* Sprintf */ package mainimport ("fmt" )func main() {// %d 表示整型数字,%s 表示字符串var stockcode = 123var enddate = "2020-12-31"var url = "Code=%d&endDate=%s"var target_url = fmt.Sprintf(url,stockcode,enddate)fmt.Println(target_url) } /* Code=123&endDate=2020-12-31 */
/* Printf */ package mainimport ("fmt" )func main() {// %d 表示整型数字,%s 表示字符串var stockcode = 123var enddate = "2020-12-31"var url = "Code=%d&endDate=%s"fmt.Printf(url,stockcode,enddate) } /* Code=123&endDate=2020-12-31 */
笔记
Go 程序的一般结构: basic_structure.go
package mainimport . "fmt"// 常量定义 const PI = 3.14// 全局变量的声明和赋值 var name = "gopher"// 一般类型声明 type newType inttype gopher struct{}type golang interface{}// 由main函数作为程序入口点启动 func main() {Println("Hello World!") }
Go 程序是通过 package 来组织的
只有 package 名称为 main 的源码文件可以包含 main 函数
一个可执行程序有且仅有一个 main 包
通过 import 关键字来导入其他非 main 包
可以通过 import 关键字单个导入:
import "fmt" import "io"
也可以同时导入多个:
package main import ("fmt""math" ) func main() {fmt.Println(math.Exp2(10)) // .Exp2()2: 2的指数 1024 }
使用 <PackageName>.<FunctionName> 调用:
package 别名: // 为fmt起别名为fmt2 import fmt2 "fmt"
省略调用(不建议使用):
// 调用的时候只需要Println(),而不需要fmt.Println() import . "fmt"
前面加个点表示省略调用,那么调用该模块里面的函数,可以不用写模块名称了:
import . "fmt" func main (){Println("hello,world") }
通过 const 关键字来进行常量的定义。
通过在函数体外部使用 var 关键字来进行全局变量的声明和赋值。
通过 type 关键字来进行结构(struct)和接口(interface)的声明。
通过 func 关键字来进行函数的声明。
可见性规则
Go语言中,使用大小写来决定该常量、变量、类型、接口、结构或函数是否可以被外部包所调用。
函数名首字母小写即为 private :
func getId() {}
函数名首字母大写即为 public :
func Printf() {}
Go 语言的包引入一般为: 项目名/包名
import "test/controllers"
方法的调用为: 包名.方法名()
controllers.Test()
本包内方法名可为小写,包外调用方法名首字母必须为大写
Golang fmt 包
/* 在函数中可以使用 := 来声明变量 如: s:=1 在函数外使用var来声明变量 */
Print() 函数将参数列表 a 中的各个参数转换为字符串并写入到标准输出中。
非字符串参数之间会添加空格,返回写入的字节数。
func Print(a ...interface{}) (n int, err error)/* a ...interface{}: 这是函数的参数列表。... 表示这是一个可变参数,意味着你可以传递任意数量的参数给这个函数。interface{} 是一个空接口,它可以接受任何类型的值。因此,a ...interface{} 可以接受任意类型和任意数量的参数。(n int, err error): 这是函数的返回值列表。该函数返回两个值:n int: 一个整数,通常用于表示函数执行时写入的字符数或类似的信息。 err error: 一个错误值。在Go中,error 是一个内置接口,通常用于表示函数执行过程中可能出现的错误。如果函数成功执行,err 通常是 nil;如果函数执行失败,err 将包含一个描述错误的非 nil 值。 一个典型的 Print 函数实现可能类似于标准库中的 fmt.Print,它会将参数列表中的值打印到标准输出(通常是控制台),并返回写入的字符数和任何可能的错误。 */
Println()
函数功能类似 Print,只不过最后会添加一个换行符
所有参数之间会添加空格,返回写入的字节数
func Println(a ...interface{}) (n int, err error)
Printf() 函数将参数列表 a 填写到格式字符串 format 的占位符中。
填写后的结果写入到标准输出中,返回写入的字节数。
func Printf(format string, a ...interface{}) (n int, err error)
以下三个函数功能同上面三个函数,只不过将转换结果写入到 w 中。
func Fprint(w io.Writer, a ...interface{}) (n int, err error) func Fprintln(w io.Writer, a ...interface{}) (n int, err error) func Fprintf(w io.Writer, format string, a ...interface{}) (n int, err error)
以下三个函数功能同上面三个函数,只不过将转换结果以字符串形式返回。
func Sprint(a ...interface{}) string func Sprintln(a ...interface{}) string func Sprintf(format string, a ...interface{}) string
以下函数功能同 Sprintf() 函数,只不过结果字符串被包装成了 error 类型。
func Errorf(format string, a ...interface{}) error
实例:
func main() {fmt.Print("a", "b", 1, 2, 3, "c", "d", "\n")fmt.Println("a", "b", 1, 2, 3, "c", "d")fmt.Printf("ab %d %d %d cd\n", 1, 2, 3)// ab1 2 3cd// a b 1 2 3 c d// ab 1 2 3 cdif err := percent(30, 70, 90, 160); err != nil {fmt.Println(err)}// 30%// 70%// 90%// 数值 160 超出范围(100) }func percent(i ...int) error {for _, n := range i {/* for-range 循环,用于遍历传入的每一个整数 */if n > 100 {return fmt.Errorf("数值 %d 超出范围(100)", n)/* fmt.Errorf 创建一个错误 */}fmt.Print(n, "%\n")}return nil }
Formatter 由自定义类型实现,用于实现该类型的自定义格式化过程
当格式化器需要格式化该类型的变量时,会调用其 Format 方法。
*<!-- 接口==>方法签名 相当于Rust中的Trait -->
type Formatter interface {// f 用于获取占位符的旗标、宽度、精度等信息,也用于输出格式化的结果// c 是占位符中的动词Format(f State, c rune) }
由格式化器(Print 之类的函数)实现,用于给自定义格式化过程提供信息:
type State interface {// Formatter 通过 Write 方法将格式化结果写入格式化器中,以便输出。Write(b []byte) (ret int, err error)// Formatter 通过 Width 方法获取占位符中的宽度信息及其是否被设置。Width() (wid int, ok bool)// Formatter 通过 Precision 方法获取占位符中的精度信息及其是否被设置。Precision() (prec int, ok bool)// Formatter 通过 Flag 方法获取占位符中的旗标[+- 0#]是否被设置。Flag(c int) bool }
Stringer 由自定义类型实现,用于实现该类型的自定义格式化过程。
当格式化器需要输出该类型的字符串格式时就会调用其 String 方法。
type Stringer interface {String() string }
Stringer 由自定义类型实现,用于实现该类型的自定义格式化过程。
当格式化器需要输出该类型的 Go 语法字符串(%#v)时就会调用其 String 方法。
type GoStringer interface {GoString() string }
实例:
package mainimport ("fmt""strconv""strings" )type Ustr stringfunc (us Ustr) String() string {return strings.ToUpper(string(us)) }func (us Ustr) GoString() string {return `"` + strings.ToUpper(string(us)) + `"` }func (u Ustr) Format(f fmt.State, c rune) {write := func(s string) {f.Write([]byte(s))}switch c {case 'm', 'M':write("旗标:[")for s := "+- 0#"; len(s) > 0; s = s[1:] {if f.Flag(int(s[0])) {write(s[:1])}}write("]")if v, ok := f.Width(); ok {write(" | 宽度:" + strconv.FormatInt(int64(v), 10))}if v, ok := f.Precision(); ok {write(" | 精度:" + strconv.FormatInt(int64(v), 10))}case 's', 'v': // 如果使用 Format 函数,则必须自己处理所有格式,包括 %#vif c == 'v' && f.Flag('#') {write(u.GoString())} else {write(u.String())}default: // 如果使用 Format 函数,则必须自己处理默认输出write("无效格式:" + string(c))} }func main() {u := Ustr("Hello World!")// "-" 标记和 "0" 标记不能同时存在fmt.Printf("%-+ 0#8.5m\n", u) // 旗标:[+- #] | 宽度:8 | 精度:5fmt.Printf("%+ 0#8.5M\n", u) // 旗标:[+ 0#] | 宽度:8 | 精度:5fmt.Println(u) // HELLO WORLD!fmt.Printf("%s\n", u) // HELLO WORLD!fmt.Printf("%#v\n", u) // "HELLO WORLD!"fmt.Printf("%d\n", u) // 无效格式:d }
Scan 从标准输入中读取数据,并将数据用空白分割并解析后存入 a 提供的变量中(换行符会被当作空白处理),变量必须以指针传入
当读到 EOF
或所有变量都填写完毕则停止扫描
返回成功解析的参数数量。
func Scan(a ...interface{}) (n int, err error)
Scanln 和 Scan 类似,只不过遇到换行符就停止扫描。
func Scanln(a ...interface{}) (n int, err error)
Scanf 从标准输入中读取数据,并根据格式字符串 format 对数据进行解析,将解析结果存入参数 a 所提供的变量中,变量必须以指针传入。
输入端的换行符必须和 format 中的换行符相对应(如果格式字符串中有换行符,则输入端必须输入相应的换行 符)。
占位符 %c 总是匹配下一个字符,包括空白,比如空格符、制表符、换行符。
返回成功解析的参数数量。
func Scanf(format string, a ...interface{}) (n int, err error)
以下三个函数功能同上面三个函数,只不过从 r 中读取数据。
func Fscan(r io.Reader, a ...interface{}) (n int, err error) func Fscanln(r io.Reader, a ...interface{}) (n int, err error) func Fscanf(r io.Reader, format string, a ...interface{}) (n int, err error)
以下三个函数功能同上面三个函数,只不过从 str 中读取数据。
func Sscan(str string, a ...interface{}) (n int, err error) func Sscanln(str string, a ...interface{}) (n int, err error) func Sscanf(str string, format string, a ...interface{}) (n int, err error)
实例:
// 对于 Scan 而言,回车视为空白 func main() {a, b, c := "", 0, falsefmt.Scan(&a, &b, &c)fmt.Println(a, b, c)// 在终端执行后,输入 abc 1 回车 true 回车// 结果 abc 1 true }// 对于 Scanln 而言,回车结束扫描 func main() {a, b, c := "", 0, falsefmt.Scanln(&a, &b, &c)fmt.Println(a, b, c)// 在终端执行后,输入 abc 1 true 回车// 结果 abc 1 true }// 格式字符串可以指定宽度 func main() {a, b, c := "", 0, falsefmt.Scanf("%4s%d%t", &a, &b, &c)fmt.Println(a, b, c)// 在终端执行后,输入 1234567true 回车// 结果 1234 567 true }
Scanner 由自定义类型实现,用于实现该类型的自定义扫描过程。
当扫描器需要解析该类型的数据时,会调用其 Scan 方法。
type Scanner interface {// state 用于获取占位符中的宽度信息,也用于从扫描器中读取数据进行解析。// verb 是占位符中的动词Scan(state ScanState, verb rune) error }
由扫描器(Scan 之类的函数)实现,用于给自定义扫描过程提供数据和信息。
type ScanState interface {// ReadRune 从扫描器中读取一个字符,如果用在 Scanln 类的扫描器中,// 则该方法会在读到第一个换行符之后或读到指定宽度之后返回 EOF。// 返回“读取的字符”和“字符编码所占用的字节数”ReadRune() (r rune, size int, err error)// UnreadRune 撤消最后一次的 ReadRune 操作,// 使下次的 ReadRune 操作得到与前一次 ReadRune 相同的结果。UnreadRune() error// SkipSpace 为 Scan 方法提供跳过开头空白的能力。// 根据扫描器的不同(Scan 或 Scanln)决定是否跳过换行符。SkipSpace()// Token 用于从扫描器中读取符合要求的字符串,// Token 从扫描器中读取连续的符合 f(c) 的字符 c,准备解析。// 如果 f 为 nil,则使用 !unicode.IsSpace(c) 代替 f(c)。// skipSpace:是否跳过开头的连续空白。返回读取到的数据。// 注意:token 指向共享的数据,下次的 Token 操作可能会覆盖本次的结果。Token(skipSpace bool, f func(rune) bool) (token []byte, err error)// Width 返回占位符中的宽度值以及宽度值是否被设置Width() (wid int, ok bool)// 因为上面实现了 ReadRune 方法,所以 Read 方法永远不应该被调用。// 一个好的 ScanState 应该让 Read 直接返回相应的错误信息。Read(buf []byte) (n int, err error) }
实例:
type Ustr stringfunc (u *Ustr) Scan(state fmt.ScanState, verb rune) (err error) {var s []byteswitch verb {case 'S':s, err = state.Token(true, func(c rune) bool { return 'A' <= c && c <= 'Z' })if err != nil {return}case 's', 'v':s, err = state.Token(true, func(c rune) bool { return 'a' <= c && c <= 'z' })if err != nil {return}default:return fmt.Errorf("无效格式:%c", verb)}*u = Ustr(s)return nil }func main() {var a, b, c, d, e Ustrn, err := fmt.Scanf("%3S%S%3s%2v%x", &a, &b, &c, &d, &e)fmt.Println(a, b, c, d, e)fmt.Println(n, err)// 在终端执行后,输入 ABCDEFGabcdefg 回车// 结果:// ABC DEFG abc de// 4 无效格式:x }
05_数据类型
在 Go 编程语言中,数据类型用于声明函数和变量。
数据类型的出现是为了把数据分成所需内存大小不同的数据,编程的时候需要用大数据的时候才需要申请大内存,就可以充分利用内存。
Go 语言按类别有以下几种数据类型:
序号 | 类型和描述 |
---|---|
1 | 布尔型 布尔型的值只可以是常量 true 或者 false。一个简单的例子:var b bool = true。 |
2 | 数字类型 整型 int 和浮点型 float32、float64,Go 语言支持整型和浮点型数字,并且支持复数,其中位的运算采用补码。 |
3 | 字符串类型: 字符串就是一串固定长度的字符连接起来的字符序列。Go 的字符串是由单个字节连接起来的。Go 语言的字符串的字节使用 UTF-8 编码标识 Unicode 文本。 |
4 | 派生类型: 包括:(a) 指针类型(Pointer)(b) 数组类型(c) 结构化类型(struct)(d) Channel 类型(e) 函数类型(f) 切片类型(g) 接口类型(interface)(h) Map 类型 |
数字类型
Go 也有基于架构的类型,例如:int、uint 和 uintptr。
序号 | 类型和描述 |
---|---|
1 | uint8 无符号 8 位整型 (0 到 255) |
2 | uint16 无符号 16 位整型 (0 到 65535) |
3 | uint32 无符号 32 位整型 (0 到 4294967295) |
4 | uint64 无符号 64 位整型 (0 到 18446744073709551615) |
5 | int8 有符号 8 位整型 (-128 到 127) |
6 | int16 有符号 16 位整型 (-32768 到 32767) |
7 | int32 有符号 32 位整型 (-2147483648 到 2147483647) |
8 | int64 有符号 64 位整型 (-9223372036854775808 到 9223372036854775807) |
浮点型
序号 | 类型和描述 |
---|---|
1 | float32 IEEE-754 32位浮点型数 |
2 | float64 IEEE-754 64位浮点型数 |
3 | complex64 32 位实数和虚数 |
4 | complex128 64 位实数和虚数 |
其他数字类型
以下列出了其他更多的数字类型:
序号 | 类型和描述 |
---|---|
1 | byte 类似 uint8 |
2 | rune 类似 int32 |
3 | uint 32 或 64 位 |
4 | int 与 uint 一样大小 |
5 | uintptr 无符号整型,用于存放一个指针 |
*自定义类型
package mainimport "fmt"type Code intfunc main() {var i Codei = 1fmt.Printf("%T", i) }
类型别名
package mainimport "fmt"type Code intconst (SCode MyCode = 0 )func main() {var i SCodefmt.Printf("%t", i) }
*泛型
func add[T int | float64 | int32](a, b T) T {return a + b }
type Response[T any] struct {Code int `json:"code"`Msg string `json:"msg"`Data T `json:"data"` }func main() {type User struct {Name string `json:"name"`}type UserInfo struct {Name string `json:"name"`Age int `json:"age"` }
package maintype MySlice[T any] []Tfunc main() {var mySlice MySlice[string]mySlice = append(mySlice, "枫枫")var intSlice MySlice[int]intSlice = append(intSlice, 2) }
package mainimport "fmt"type MyMap[K string | int, V any] map[K]Vfunc main() {var myMap = make(MyMap[string, string])myMap["name"] = "枫枫"fmt.Println(myMap) }
*类型断言
// i.(TypeNname) value, ok := x.(T)
func main() {var x interface{}x = 10value, ok := x.(int)fmt.Printf("%t,%v", value,ok) }
func main() {var a inta = 10getType(a) } func getType(a interface{}) {switch a.(type) {case int:fmt.Println("the type of a is int")case string:fmt.Println("the type of a is string")case float64:fmt.Println("the type of a is float")default:fmt.Println("unknown type")} }
var name any = "tom" if nameStringData, ok1 := name.(string); ok1 {nameStringData = "Mike"fmt.Printf("ok1 true, change name to %v\n", nameStringData) } else {nameStringData = "mike"fmt.Printf("ok1 false, change name to %v\n", nameStringData) }
笔记
package main import ( "fmt" "strings" ) func main() { str := "这里是 www\n.runoob\n.com" fmt.Println("-------- 原字符串 ----------") fmt.Println(str) // 去除空格 str = strings.Replace(str, " ", "", -1) // 去除换行符 str = strings.Replace(str, "\n", "", -1) fmt.Println("-------- 去除空格与换行后 ----------") fmt.Println(str) }
06_变量
声明变量的一般形式是使用 var 关键字:
var identifier type
可以一次声明多个变量:
var identifier1, identifier2 type
package main import "fmt" func main() {var a string = "Runoob"fmt.Println(a)var b, c int = 1, 2fmt.Println(b, c) }
变量声明
第一种,指定变量类型,如果没有初始化,则变量默认为零值。
var v_name v_type v_name = value
零值就是变量没有做初始化时系统默认设置的值
package main import "fmt" func main() {// 声明一个变量并初始化var a = "RUNOOB"fmt.Println(a)// 没有初始化就为零值var b intfmt.Println(b)// bool 零值为 falsevar c boolfmt.Println(c) }
-
数值类型(包括
complex64/128
)为 0 -
布尔类型为 false
-
字符串为 ""(空字符串)
-
以下几种类型为 nil:
var a *int var a []int var a map[string] int var a chan int var a func(string) int var a error // error 是接口
package mainimport "fmt"func main() {var i intvar f float64var b boolvar s stringfmt.Printf("%v %v %v %q\n", i, f, b, s) }
输出结果是:
0 0 false ""
第二种,根据值自行判定变量类型。
var v_name = value
package main import "fmt" func main() {var d = truefmt.Println(d) }
第三种,如果变量已经使用 var 声明过了,再使用 *:=* 声明变量,就产生编译错误,格式:
v_name := value
例如:
var intVal int intVal :=1 // 这时候会产生编译错误,因为 intVal 已经声明,不需要重新声明
直接使用下面的语句即可:
intVal := 1 // 此时不会产生编译错误,因为有声明新的变量,因为 := 是一个声明语句
intVal := 1 相等于:
var intVal int intVal =1
可以将 var f string = "Runoob
" 简写为 f := "Runoob
":
package main import "fmt" func main() {f := "Runoob" // var f string = "Runoob"fmt.Println(f) }
输出结果是:
Runoob
多变量声明
//类型相同多个变量, 非全局变量 var vname1, vname2, vname3 type vname1, vname2, vname3 = v1, v2, v3var vname1, vname2, vname3 = v1, v2, v3 // 和 python 很像,不需要显示声明类型,自动推断vname1, vname2, vname3 := v1, v2, v3 // 出现在 := 左侧的变量不应该是已经被声明过的,否则会导致编译错误// 这种因式分解关键字的写法一般用于声明全局变量 var (vname1 v_type1vname2 v_type2 )
package main import "fmt"var x, y int var ( // 这种因式分解关键字的写法一般用于声明全局变量a intb bool )var c, d int = 1, 2 var e, f = 123, "hello"//这种不带声明格式的只能在函数体中出现 //g, h := 123, "hello"func main(){g, h := 123, "hello"fmt.Println(x, y, a, b, c, d, e, f, g, h) }
以上实例执行结果为:
0 0 0 false 1 2 123 hello 123 hello
值类型和引用类型
所有像 int、float、bool
和string
这些基本类型都属于值类型,使用这些类型的变量直接指向存在内存中的值:
当使用等号 =
将一个变量的值赋值给另一个变量时,如:j = i
,实际上是在内存中将 i 的值进行了拷贝:
你可以通过 &i 来获取变量 i 的内存地址,例如:0xf840000040
(每次的地址都可能不一样)
值类型变量的值存储在堆中
内存地址会根据机器的不同而有所不同,甚至相同的程序在不同的机器上执行后也会有不同的内存地址。因为每台机器可能有不同的存储器布局,并且位置分配也可能不同
更复杂的数据通常会需要使用多个字,这些数据一般使用引用类型保存
一个引用类型的变量r1
存储的是 r1
的值所在的内存地址(数字),或内存地址中第一个字所在的位置
这个内存地址称之为指针,这个指针实际上也被存在另外的某一个值中。
同一个引用类型的指针指向的多个字可以是在连续的内存地址中(内存布局是连续的),这也是计算效率最高的一种存储形式;也可以将这些字分散存放在内存中,每个字都指示了下一个字所在的内存地址。
当使用赋值语句 r2 = r1 时,只有引用(地址)被复制。
如果 r1 的值被改变了,那么这个值的所有引用都会指向被修改后的内容,在这个例子中,r2 也会受到影响。
简短形式,使用 := 赋值操作符
我们知道可以在变量的初始化时省略变量的类型而由系统自动推断,声明语句写上 var 关键字其实是显得有些多余了,因此我们可以将它们简写为 a := 50 或 b := false
a 和 b 的类型(int 和 bool
)将由编译器自动推断
这是使用变量的首选形式,但是它只能被用在函数体内,而不可以用于全局变量的声明与赋值。使用操作符 := 可以高效地创建一个新的变量,称之为初始化声明
注意事项
如果在相同的代码块中,我们不可以再次对于相同名称的变量使用初始化声明,例如:a := 20 就是不被允许的,编译器会提示错误 no new variables on left side of :=,但是 a = 20 是可以的,因为这是给相同的变量赋予一个新的值。
如果你在定义变量 a 之前使用它,则会得到编译错误 undefined: a。
如果你声明了一个局部变量却没有在相同的代码块中使用它,同样会得到编译错误,例如下面这个例子当中的变量 a:
package mainimport "fmt"func main() {var a string = "abc"fmt.Println("hello, world") }
尝试编译这段代码将得到错误 a declared but not used。
此外,单纯地给 a 赋值也是不够的,这个值必须被使用,所以使用
fmt.Println("hello, world", a)
会移除错误。
但是全局变量是允许声明但不使用的。 同一类型的多个变量可以声明在同一行,如:
var a, b, c int
多变量可以在同一行进行赋值,如:
var a, b int var c string a, b, c = 5, 7, "abc"
上面这行假设了变量 a,b 和 c 都已经被声明,否则的话应该这样使用:
a, b, c := 5, 7, "abc"
右边的这些值以相同的顺序赋值给左边的变量,所以 a 的值是 5, b 的值是 7,c 的值是 "abc"。
这被称为 并行 或 同时 赋值。
如果你想要交换两个变量的值,则可以简单地使用 a, b = b, a,两个变量的类型必须是相同。
空白标识符 _ 也被用于抛弃值,如值 5 在:_, b = 5, 7 中被抛弃。
_ 实际上是一个只写变量,你不能得到它的值。这样做是因为 Go 语言中你必须使用所有被声明的变量,但有时你并不需要使用从一个函数得到的所有返回值。
并行赋值也被用于当一个函数返回多个返回值时,比如这里的 val 和错误 err 是通过调用 Func1 函数同时得到:val, err = Func1(var1)。
笔记
空白标识符在函数返回值时的使用:
package mainimport "fmt"func main() {_,numb,strs := numbers() //只获取函数返回值的后两个fmt.Println(numb,strs) }//一个可以返回多个值的函数 func numbers()(int,int,string){a , b , c := 1 , 2 , "str"return a,b,c }
输出结果:
2 str
07_常量
常量是一个简单值的标识符,在程序运行时,不会被修改的量
常量中的数据类型只可以是布尔型、数字型(整数型、浮点型和复数)和字符串型
常量的定义格式:
const identifier [type] = value
你可以省略类型说明符 [type],因为编译器可以根据变量的值来推断其类型
-
显式类型定义:
const b string = "abc"
-
隐式类型定义:
const b = "abc"
多个相同类型的声明可以简写为:
const c_name1, c_name2 = value1, value2
以下实例演示了常量的应用:
package mainimport "fmt"func main() {const LENGTH int = 10const WIDTH int = 5 var area intconst a, b, c = 1, false, "str" //多重赋值area = LENGTH * WIDTHfmt.Printf("面积为 : %d", area)println()println(a, b, c) }
常量还可以用作枚举:
const (Unknown = 0Female = 1Male = 2 )
数字 0、1 和 2 分别代表未知性别、女性和男性。
常量可以用len(), cap(), unsafe.Sizeof()函数计算表达式的值。常量表达式中,函数必须是内置函数,否则编译不过:
package mainimport "unsafe" const (a = "abc"b = len(a)c = unsafe.Sizeof(a) )func main(){println(a, b, c) }
*iota
iota,特殊常量,可以认为是一个可以被编译器修改的常量
iota 在 const
关键字出现时将被重置为 0(const
内部的第一行之前),const
中每新增一行常量声明将使 iota 计数一次(iota 可理解为 const
语句块中的行索引)
iota 可以被用作枚举值:
const (a = iotab = iotac = iota )
第一个 iota 等于 0,每当 iota 在新的一行被使用时,它的值都会自动加 1;所以 a=0, b=1, c=2 可以简写为如下形式:
const (a = iotabc )
iota 用法
package mainimport "fmt"func main() {const (a = iota //0b //1c //2d = "ha" //独立值,iota += 1e //"ha" iota += 1f = 100 //iota +=1g //100 iota +=1h = iota //7,恢复计数i //8)fmt.Println(a,b,c,d,e,f,g,h,i) }
package mainimport "fmt" const (i=1<<iotaj=3<<iotakl )func main() {fmt.Println("i=",i)fmt.Println("j=",j)fmt.Println("k=",k)fmt.Println("l=",l) }
以上实例运行结果为:
i= 1 j= 6 k= 12 l= 24
iota 表示从 0 开始自动加 1,所以 i=1<<0, j=3<<1(<< 表示左移的意思),即:i=1, j=6,这没问题,关键在 k 和 l,从输出结果看 k=3<<2,l=3<<3。
简单表述:
-
i=1:左移 0 位,不变仍为 1。
-
j=3:左移 1 位,变为二进制 110,即 6。
-
k=3:左移 2 位,变为二进制 1100,即 12。
-
l=3:左移 3 位,变为二进制 11000,即 24。
注:<<n==*(2^n)。
笔记
在定义常量组时,如果不提供初始值,则表示将使用上行的表达式。
package mainimport "fmt"const (a = 1bcd )func main() {fmt.Println(a)// b、c、d没有初始化,使用上一行(即a)的值fmt.Println(b) // 输出1fmt.Println(c) // 输出1fmt.Println(d) // 输出1 }
iota 只是在同一个 const
常量组内递增,每当有新的 const
关键字时,iota 计数会重新开始
package mainconst (i = iotaj = iotax = iota ) const xx = iota const yy = iota func main(){println(i, j, x, xx, yy) }// 输出是 0 1 2 0 0
左移运算符 << 是双目运算符。左移 n 位就是乘以 2 的 n 次方。 其功能把 << 左边的运算数的各二进位全部左移若干位,由 << 右边的数指定移动的位数,高位丢弃,低位补 0。
右移运算符 >> 是双目运算符。右移 n 位就是除以 2 的 n 次方。 其功能是把 >> 左边的运算数的各二进位全部右移若干位, >> 右边的数指定移动的位数。
在Go语言中,字符串类型是一个结构体,包含一个指向底层数据的指针和一个表示字符串长度的整数。在64位系统上,每个指针占用8字节,整数占用8字节。因此,一个字符串类型在64位系统上总共占用16字节的空间。
对于给定的代码a = "hello"
,虽然a
是一个字符串类型的变量,但在赋值时,实际上是将一个指向字符串"hello"底层数据的指针赋值给了a
。因此,unsafe.Sizeof(a)
返回的是一个指针的大小,即8字节。
因此,代码a = "hello"; unsafe.Sizeof(a)
的运行结果是8。
08_运算符
-
算术运算符
-
关系运算符
-
逻辑运算符
-
位运算符
-
赋值运算符
-
其他运算符
*<!-- *是指针,解引用指针运算符,存储着内存地址 | &是获取变量的地址,取地址符号 地址是一个指针类型 -->
算术运算符
下表列出了所有Go语言的算术运算符。假定 A 值为 10,B 值为 20。
运算符 | 描述 | 实例 |
---|---|---|
+ | 相加 | A + B 输出结果 30 |
- | 相减 | A - B 输出结果 -10 |
* | 相乘 | A * B 输出结果 200 |
/ | 相除 | B / A 输出结果 2 |
% | 求余 | B % A 输出结果 0 |
++ | 自增 | A++ 输出结果 11 |
-- | 自减 | A-- 输出结果 9 |
关系运算符
下表列出了所有Go语言的关系运算符。假定 A 值为 10,B 值为 20。
运算符 | 描述 | 实例 |
---|---|---|
== | 检查两个值是否相等,如果相等返回 True 否则返回 False。 | (A == B) 为 False |
!= | 检查两个值是否不相等,如果不相等返回 True 否则返回 False。 | (A != B) 为 True |
> | 检查左边值是否大于右边值,如果是返回 True 否则返回 False。 | (A > B) 为 False |
< | 检查左边值是否小于右边值,如果是返回 True 否则返回 False。 | (A < B) 为 True |
>= | 检查左边值是否大于等于右边值,如果是返回 True 否则返回 False。 | (A >= B) 为 False |
<= | 检查左边值是否小于等于右边值,如果是返回 True 否则返回 False。 | (A <= B) 为 True |
逻辑运算符
下表列出了所有Go语言的逻辑运算符。假定 A 值为 True,B 值为 False。
运算符 | 描述 | 实例 |
---|---|---|
&& | 逻辑 AND 运算符。 如果两边的操作数都是 True,则条件 True,否则为 False。 | (A && B) 为 False |
|| | 逻辑 OR 运算符。 如果两边的操作数有一个 True,则条件 True,否则为 False。 | (A || B) 为 True |
! | 逻辑 NOT 运算符。 如果条件为 True,则逻辑 NOT 条件 False,否则为 True。 |
位运算符
位运算符对整数在内存中的二进制位进行操作。
下表列出了位运算符 &, |, 和 ^ 的计算:
p | q | p & q | p | q | p ^ q |
---|---|---|---|---|
0 | 0 | 0 | 0 | 0 |
0 | 1 | 0 | 1 | 1 |
1 | 1 | 1 | 1 | 0 |
1 | 0 | 0 | 1 | 1 |
Go 语言支持的位运算符如下表所示。假定 A 为60,B 为13:
运算符 | 描述 | 实例 |
---|---|---|
& | 按位与运算符"&"是双目运算符。 其功能是参与运算的两数各对应的二进位相与。 | (A & B) 结果为 12, 二进制为 0000 1100 |
| | 按位或运算符"|"是双目运算符。 其功能是参与运算的两数各对应的二进位相或 | (A | B) 结果为 61, 二进制为 0011 1101 |
^ | 按位异或运算符"^"是双目运算符。 其功能是参与运算的两数各对应的二进位相异或,当两对应的二进位相异时,结果为1。 | (A ^ B) 结果为 49, 二进制为 0011 0001 |
<< | 左移运算符"<<"是双目运算符。左移n位就是乘以2的n次方。 其功能把"<<"左边的运算数的各二进位全部左移若干位,由"<<"右边的数指定移动的位数,高位丢弃,低位补0。 | A << 2 结果为 240 ,二进制为 1111 0000 |
>> | 右移运算符">>"是双目运算符。右移n位就是除以2的n次方。 其功能是把">>"左边的运算数的各二进位全部右移若干位,">>"右边的数指定移动的位数。 | A >> 2 结果为 15 ,二进制为 0000 1111 |
赋值运算符
下表列出了所有Go语言的赋值运算符。
运算符 | 描述 | 实例 |
---|---|---|
= | 简单的赋值运算符,将一个表达式的值赋给一个左值 | C = A + B 将 A + B 表达式结果赋值给 C |
+= | 相加后再赋值 | C += A 等于 C = C + A |
-= | 相减后再赋值 | C -= A 等于 C = C - A |
*= | 相乘后再赋值 | C *= A 等于 C = C * A |
/= | 相除后再赋值 | C /= A 等于 C = C / A |
%= | 求余后再赋值 | C %= A 等于 C = C % A |
<<= | 左移后赋值 | C <<= 2 等于 C = C << 2 |
>>= | 右移后赋值 | C >>= 2 等于 C = C >> 2 |
&= | 按位与后赋值 | C &= 2 等于 C = C & 2 |
^= | 按位异或后赋值 | C ^= 2 等于 C = C ^ 2 |
|= | 按位或后赋值 | C |= 2 等于 C = C | 2 |
其他运算符
下表列出了Go语言的其他运算符。
运算符 | 描述 | 实例 |
---|---|---|
& | 返回变量存储地址 | &a; 将给出变量的实际地址。 |
* | 指针变量。 | *a; 是一个指针变量 |
运算符优先级
有些运算符拥有较高的优先级,二元运算符的运算方向均是从左至右。下表列出了所有运算符以及它们的优先级,由上至下代表优先级由高到低:
优先级 | 运算符 |
---|---|
5 | * / % << >> & &^ |
4 | + - | ^ |
3 | == != < <= > >= |
2 | && |
1 | || |
笔记
指针变量 * 和地址值 & 的区别:指针变量保存的是一个地址值,会分配独立的内存来存储一个整型数字。当变量前面有 * 标识时,才等同于 & 的用法,否则会直接输出一个整型数字。
func main() {var a int = 4var ptr *int // 定义一个指针变量ptrptr = &a // 把a的地址赋值给ptrprintln("a的值为", a); // 4println("*ptr为", *ptr); // 4println("ptr为", ptr); // 824633794744 }
Go 的自增,自减只能作为表达式使用,而不能用于赋值语句。
a++ // 这是允许的,类似 a = a + 1,结果与 a++ 相同 a-- //与 a++ 相似 a = a++ // 这是不允许的,会出现编译错误 syntax error: unexpected ++ at end of statement
使用指针变量与不使用的区别:
func main(){var a int = 4var ptr intptr = a fmt.Println(ptr)//4a = 15fmt.Println(ptr)//4var b int = 5 var ptr1 *intptr1 = &b fmt.Println(*ptr1)//5b=15 fmt.Println(*ptr1)//15 }
09_条件语句
Go 语言提供了以下几种条件判断语句:
语句 | 描述 |
---|---|
if 语句 | if 语句 由一个布尔表达式后紧跟一个或多个语句组成。 |
if...else 语句 | if 语句 后可以使用可选的 else 语句, else 语句中的表达式在布尔表达式为 false 时执行。 |
if 嵌套语句 | 你可以在 if 或 else if 语句中嵌入一个或多个 if 或 else if 语句。 |
switch 语句 | switch 语句用于基于不同条件执行不同动作。 |
select 语句 | select 语句类似于 switch 语句,但是select会随机执行一个可运行的case。如果没有case可运行,它将阻塞,直到有case可运行。 |
注意:Go 没有三目运算符,所以不支持 ?: 形式的条件判断。
笔记
package mainimport "fmt"func main() {var s int ; // 声明变量 s 是需要判断的数fmt.Println("输入一个数字:")fmt.Scan(&s)if s%2 == 0 { // 取 s 处以 2 的余数是否等于 0fmt.Print("s 是偶数\n") ;//如果成立}else {fmt.Print("s 不是偶数\n") ;//否则}fmt.Print("s 的值是:",s) ; }
在if之后,条件语句之前,可以添加变量初始化语句,使用;进行分隔
有返回值的函数中,最终的return不能在条件语句中
寻找到 100 以内的所有的素数:
package mainimport "fmt" func main(){// var count,c int //定义变量不使用也会报错var count intvar flag boolcount=1//while(count<100) { //go没有whilefor count < 100 {count++flag = true;//注意tmp变量 :=for tmp:=2;tmp<count;tmp++ {if count%tmp==0{flag = false}}// 每一个 if else 都需要加入括号 同时 else 位置不能在新一行if flag == true {fmt.Println(count,"素数")}else{continue}} }
package main import"fmt"func main(){var a int var b intfmt.Printf("请输入密码: \n")fmt.Scan(&a)if a == 5211314 {fmt.Printf("请再次输入密码:")fmt.Scan(&b)if b == 5211314 {fmt.Printf("密码正确,门锁已打开")}else{fmt.Printf("非法入侵,已自动报警")}}else{fmt.Printf("非法入侵,已自动报警")} }
package mainimport "fmt"func main() {/* 定义局部变量 */var grade string = "B"var marks int = 90switch marks {case 90: grade = "A"case 80: grade = "B"case 50,60,70 : grade = "C"default: grade = "D" }switch {case grade == "A" :fmt.Printf("优秀!\n" ) case grade == "B", grade == "C" :fmt.Printf("良好\n" ) case grade == "D" :fmt.Printf("及格\n" ) case grade == "F":fmt.Printf("不及格\n" )default:fmt.Printf("差\n" );}fmt.Printf("你的等级是 %s\n", grade ); }
*Type Switch
switch 语句还可以被用于 type-switch 来判断某个 interface 变量中实际存储的变量类型。
Type Switch 语法格式如下:
switch x.(type){case type:statement(s); case type:statement(s); /* 你可以定义任意个数的case */default: /* 可选 */statement(s); }
package mainimport "fmt"func main() {var x interface{}switch i := x.(type) {case nil: fmt.Printf(" x 的类型 :%T",i) case int: fmt.Printf("x 是 int 型") case float64:fmt.Printf("x 是 float64 型") case func(int) float64:fmt.Printf("x 是 func(int) 型") case bool, string:fmt.Printf("x 是 bool 或 string 型" ) default:fmt.Printf("未知型") } }
*fallthrough
使用 fallthrough
会强制执行后面的 case 语句,fallthrough
不会判断下一条 case 的表达式结果是否为 true
package mainimport "fmt"func main() {switch {case false:fmt.Println("1、case 条件语句为 false")fallthroughcase true:fmt.Println("2、case 条件语句为 true")fallthroughcase false:fmt.Println("3、case 条件语句为 false")fallthroughcase true:fmt.Println("4、case 条件语句为 true")case false:fmt.Println("5、case 条件语句为 false")fallthroughdefault:fmt.Println("6、默认 case")} }
select 语句
select 是 Go 中的一个控制结构,类似于 switch 语句。
select 语句只能用于通道操作,每个 case 必须是一个通道操作,要么是发送要么是接收。
select 语句会监听所有指定的通道上的操作,一旦其中一个通道准备好就会执行相应的代码块。
如果多个通道都准备好,那么 select 语句会随机选择一个通道执行。如果所有通道都没有准备好,那么执行 default 块中的代码。
select语法
Go 编程语言中 select 语句的语法如下:
select {case <- channel1:// 执行的代码case value := <- channel2:// 执行的代码case channel3 <- value:// 执行的代码// 你可以定义任意数量的 casedefault:// 所有通道都没有准备好,执行的代码 }
以下描述了 select 语句的语法:
-
每个 case 都必须是一个通道
-
所有 channel 表达式都会被求值
-
所有被发送的表达式都会被求值
-
如果任意某个通道可以进行,它就执行,其他被忽略。
-
如果有多个 case 都可以运行,select 会随机公平地选出一个执行,其他不会执行。
否则:
-
如果有 default 子句,则执行该语句。
-
如果没有 default 子句,select 将阻塞,直到某个通道可以运行;Go 不会重新对 channel 或值进行求值。
-
package mainimport ("fmt""time" )func main() {c1 := make(chan string)c2 := make(chan string)/* chan 关键字:在Go中,chan 是用于声明通道的关键字string:这表示该通道用于传递字符串类型的数据make 函数:make 函数用于初始化通道使用 <- 运算符来发送(send)和接收(receive)数据发送数据到通道:c2 <- "Hello, World!"从通道接收数据:message := <-c2 */go func() {time.Sleep(1 * time.Second)c1 <- "one"}()go func() {time.Sleep(2 * time.Second)c2 <- "two"}()for i := 0; i < 2; i++ {select {case msg1 := <-c1:fmt.Println("received", msg1)case msg2 := <-c2:fmt.Println("received", msg2)}} }
以上实例中,我们创建了两个通道 c1 和 c2。
select 语句等待两个通道的数据。如果接收到 c1 的数据,就会打印 "received one";如果接收到 c2 的数据,就会打印 "received two"。
以下实例中,我们定义了两个通道,并启动了两个协程(Goroutine)从这两个通道中获取数据。在 main 函数中,我们使用 select 语句在这两个通道中进行非阻塞的选择,如果两个通道都没有可用的数据,就执行 default 子句中的语句。
以下实例执行后会不断地从两个通道中获取到的数据,当两个通道都没有可用的数据时,会输出 "no message received"。
package mainimport "fmt"func main() {// 定义两个通道ch1 := make(chan string)ch2 := make(chan string)// 启动两个 goroutine,分别从两个通道中获取数据go func() {for {ch1 <- "from 1"}}()go func() {for {ch2 <- "from 2"}}()// 使用 select 语句非阻塞地从两个通道中获取数据for {select {case msg1 := <-ch1:fmt.Println(msg1)case msg2 := <-ch2:fmt.Println(msg2)default:// 如果两个通道都没有可用的数据,则执行这里的语句fmt.Println("no message received")}} }
笔记
select 会循环检测条件,如果有满足则执行并退出,否则一直循环检测。
package mainimport ("fmt""time" )func Chann(ch chan int, stopCh chan bool) {var i inti = 10for j := 0; j < 10; j++ {ch <- itime.Sleep(time.Second)}stopCh <- true }func main() {ch := make(chan int)c := 0stopCh := make(chan bool)go Chann(ch, stopCh)for {select {case c = <-ch:fmt.Println("Receive", c)fmt.Println("channel")case s := <-ch:fmt.Println("Receive", s)case _ = <-stopCh:goto end}} end: }
楼上那个兄弟,select 是随机执行的不是循环检测,是为了避免饥饿问题,我给你改了改:
package mainimport ("fmt" "time")func Chann(ch chan int, stopCh chan bool) {for j := 0; j < 10; j++ {ch <- jtime.Sleep(time.Second)}stopCh <- true}func main() {ch := make(chan int)c := 0 stopCh := make(chan bool)go Chann(ch, stopCh)for {select {case c = <-ch:fmt.Println("Receive C", c)case s := <-ch:fmt.Println("Receive S", s)case _ = <-stopCh:goto end}} end: }
10_循环语句
语法
*<!--switch case-->
虽然不是循环语句,有时却比其好用
package mainimport "fmt"func main() {// var num string// _,_=fmt.Scan(&num)num := "-415"var s strings = ""for _, item := range num {switch item {case '0':s += "ling "case '1':s += "yi "case '2':s += "er "case '3':s += "san "case '4':s += "si "case '5':s += "wu "case '6':s += "liu "case '7':s += "qi "case '8':s += "ba "case '9':s += "jiu "default:s += "fu "}}fmt.Printf(s[:len(s)-1]) //注意最后的空格 }
Go 语言的 For 循环有 3 种形式,只有其中的一种使用分号。
和 C 语言的 for 一样:
for init; condition; post { }
和 C 的 while 一样:
for condition { }
和 C 的 for(;;) 一样:
for { }
-
init: 一般为赋值表达式,给控制变量赋初值;
-
condition: 关系表达式或逻辑表达式,循环控制条件;
-
post: 一般为赋值表达式,给控制变量增量或减量。
判别赋值表达式 init 是否满足给定条件,若其值为真,满足循环条件,则执行循环体内语句,然后执行 post,进入第二次循环,再判别 condition;否则判断 condition 的值为假,不满足条件,就终止for循环,执行循环体外语句
for 循环的 range 格式可以对 slice、map、数组、字符串等进行迭代循环。格式如下:
for key, value := range oldMap {newMap[key] = value }
以上代码中的 key 和 value 是可以省略。
如果只想读取 key,格式如下:
for key := range oldMap
或者这样:
for key, _ := range oldMap
如果只想读取 value,格式如下:
for _, value := range oldMap
计算 1 到 10 的数字之和:
package mainimport "fmt"func main() {sum := 0for i := 0; i <= 10; i++ {sum += i}fmt.Println(sum) }
init 和 post 参数是可选的,我们可以直接省略它,类似 While 语句。
以下实例在 sum 小于 10 的时候计算 sum 自相加后的值:
package mainimport "fmt"func main() {sum := 1for ; sum <= 10; {sum += sum}fmt.Println(sum)// 这样写也可以,更像 While 语句形式for sum <= 10{sum += sum}fmt.Println(sum) }
package mainimport "fmt"func main() {sum := 0for {sum++ // 无限循环下去}fmt.Println(sum) // 无法输出 }
要停止无限循环,可以在命令窗口按下ctrl-c
For-each range 循环
这种格式的循环可以对字符串、数组、切片等进行迭代输出元素
package main import "fmt"func main() {strings := []string{"google", "runoob"}for i, s := range strings {fmt.Println(i, s)}numbers := [6]int{1, 2, 3, 5} for i,x:= range numbers {fmt.Printf("第 %d 位 x 的值 = %d\n", i,x)} }
for 循环的 range 格式可以省略 key 和 value,如下实例:
package main import "fmt"func main() {map1 := make(map[int]float32)map1[1] = 1.0map1[2] = 2.0map1[3] = 3.0map1[4] = 4.0// 读取 key 和 valuefor key, value := range map1 {fmt.Printf("key is: %d - value is: %f\n", key, value)}// 读取 keyfor key := range map1 {fmt.Printf("key is: %d\n", key)}// 读取 valuefor _, value := range map1 {fmt.Printf("value is: %f\n", value)} }
以下实例使用循环嵌套来输出 2 到 100 间的素数:
package mainimport "fmt"func main() {/* 定义局部变量 */var i, j intfor i=2; i < 100; i++ {for j=2; j <= (i/j); j++ {if(i%j==0) {break; // 如果发现因子,则不是素数}}if(j > (i/j)) {fmt.Printf("%d 是素数\n", i);}} }
九九乘法表:
package main import "fmt"func main() {for m := 1; m < 10; m++ {/* fmt.Printf("第%d次:\n",m) */for n := 1; n <= m; n++ {fmt.Printf("%dx%d=%d ",n,m,m*n)}fmt.Println("")} }
*goto 语句
package mainimport "fmt"func main() {/* 定义局部变量 */var a int = 10/* 循环 */LOOP: for a < 20 {if a == 15 {/* 跳过迭代 */a = a + 1goto LOOP}fmt.Printf("a的值为 : %d\n", a)a++ } }
打印九九乘法表
package main import "fmt"func main() {//print9x()gotoTag() }//嵌套for循环打印九九乘法表 func print9x() {for m := 1; m < 10; m++ {for n := 1; n <= m; n++ {fmt.Printf("%dx%d=%d ",n,m,m*n)}fmt.Println("")} }//for循环配合goto打印九九乘法表 func gotoTag() {for m := 1; m < 10; m++ {n := 1LOOP: if n <= m {fmt.Printf("%dx%d=%d ",n,m,m*n)n++goto LOOP} else {fmt.Println("")}n++} }
package mainimport "fmt"func main() {var test intfmt.Printf("请输入: \n")fmt.Scan(&test)switch test {case 0:goto endcase 1:println("我是1")default:println("我是default")}println("我是switch中间的内容") end:println("结束咯") }
11_函数
以下实例为 max() 函数的代码,该函数传入两个整型参数 num1 和 num2,并返回这两个参数的最大值:
package mainimport "fmt"func max(i1, i2 int) int {if i1 > i2 {return i1} else {return i2} }func main() {var iii, ooo, e1 intfmt.Scan(&iii, &ooo)e1 = max(iii, ooo)fmt.Println(e1) }
函数返回多个值
package mainimport "fmt"func swap(x, y string) (string, string) {return y, x }func main() {a, b := swap("Google", "Runoob")fmt.Println(a, b) }
函数参数
函数如果使用参数,该变量可称为函数的形参
形参就像定义在函数体内的局部变量
调用函数,可以通过两种方式来传递参数
传递类型 | 描述 |
---|---|
值传递 | 值传递是调用函数时将实际参数复制一份传递到函数中,将不影响实际参数 |
引用传递 | 引用传递是指在调用函数时将实际参数的地址传递到函数中,修改参数就是修改原参数 |
默认情况下,Go 语言使用的是值传递,调用过程中不会影响到实际参数
笔记
a := 100 b := 200 a, b = b, a // a == 200 // b == 100
函数引用传递值
package mainimport "fmt"func main() {/* 定义局部变量 */var a int = 100var b int= 200fmt.Printf("交换前,a 的值 : %d\n", a )fmt.Printf("交换前,b 的值 : %d\n", b )/* 调用 swap() 函数* &a 指向 a 指针,a 变量的地址* &b 指向 b 指针,b 变量的地址*/swap(&a, &b)fmt.Printf("交换后,a 的值 : %d\n", a )fmt.Printf("交换后,b 的值 : %d\n", b ) }func swap(x *int, y *int) {var temp inttemp = *x /* 保存 x 地址上的值 */*x = *y /* 将 y 值赋给 x */*y = temp /* 将 temp 值赋给 y */ }
函数用法
函数用法 | 描述 |
---|---|
函数作为另外一个函数的实参 | 函数定义后可作为另外一个函数的实参数传入 |
闭包 | 闭包是匿名函数,可在动态编程中使用 |
方法 | 方法就是一个包含了接受者的函数 |
函数作为实参
package mainimport ("fmt""math" )func main(){/* 声明函数变量 */getSquareRoot := func(x float64) float64 {return math.Sqrt(x)}/* 使用函数 */fmt.Println(getSquareRoot(9))}
笔记
// 函数作为参数传递,实现回调 package main import "fmt"// 声明一个函数类型 type cb func(int) intfunc main() {testCallBack(1, callBack)testCallBack(2, func(x int) int {fmt.Printf("我是回调,x:%d\n", x)return x}) }func testCallBack(x int, f cb) {f(x) }func callBack(x int) int {fmt.Printf("我是回调,x:%d\n", x)return x }
把上面的简化,实际就是把函数作为参数传递进去了
package main import "fmt" // 声明一个函数类型 type cb func(int) intfunc main() { testCallBack(1, callBack)//执行函数---testCallBack } func testCallBack(x int, f cb) { //定义了一个函数 testCallBackf(x) //由于传进来的是callBack函数,该函数执行需要传入一个int类型参数,因此传入x } func callBack(x int) int { fmt.Printf("我是回调,x:%d\n", x) return x }
匿名函数
package mainimport "fmt"var (myRes = func (a int, b int) int {return a - b} )func main(){//匿名函数,只调用一次,定义时直接调用res1 := func (a int, b int) int {return a + b}(10,25)fmt.Printf("res1 =%d\n", res1)//匿名函数赋给变量用变量来调用,可多次使用,但作用域有限res2 := func (a int, b int) int {return a * b}res3 := res2(10,25)fmt.Printf("res3 =%d\n", res3)//将匿名函数用全局变量接收,则该函数为全局匿名函数res4 := myRes(10,25)fmt.Printf("res4 =%d\n", res4)}
函数闭包(匿名函数)
package mainimport "fmt"func getSequence() func() int {i:=0return func() int {i+=1return i } }func main(){/* nextNumber 为一个函数,函数 i 为 0 */nextNumber := getSequence() /* 调用 nextNumber 函数,i 变量自增 1 并返回 */fmt.Println(nextNumber())fmt.Println(nextNumber())fmt.Println(nextNumber())/* 创建新的函数 nextNumber1,并查看结果 */nextNumber1 := getSequence() fmt.Println(nextNumber1())fmt.Println(nextNumber1()) }
package mainimport "fmt"func main() {// 定义一个匿名函数并将其赋值给变量addadd := func(a, b int) int {return a + b}// 调用匿名函数result := add(3, 5)fmt.Println("3 + 5 =", result)// 在函数内部使用匿名函数multiply := func(x, y int) int {return x * y}product := multiply(4, 6)fmt.Println("4 * 6 =", product)// 将匿名函数作为参数传递给其他函数calculate := func(operation func(int, int) int, x, y int) int {return operation(x, y)}sum := calculate(add, 2, 8)fmt.Println("2 + 8 =", sum)// 也可以直接在函数调用中定义匿名函数difference := calculate(func(a, b int) int {return a - b}, 10, 4)fmt.Println("10 - 4 =", difference) }
笔记
带参数的闭包函数调用:
package mainimport "fmt" func main() {add_func := add(1,2)fmt.Println(add_func())fmt.Println(add_func())fmt.Println(add_func()) }// 闭包使用方法 func add(x1, x2 int) func()(int,int) {i := 0return func() (int,int){i++return i,x1+x2} }
闭包带参数补充:
package main import "fmt" func main() {add_func := add(1,2)fmt.Println(add_func(1,1))fmt.Println(add_func(0,0))fmt.Println(add_func(2,2)) } // 闭包使用方法 func add(x1, x2 int) func(x3 int,x4 int)(int,int,int) {i := 0return func(x3 int,x4 int) (int,int,int){ i++return i,x1+x2,x3+x4} }
闭包带参数继续补充:
package mainimport "fmt"// 闭包使用方法,函数声明中的返回值(闭包函数)不用写具体的形参名称 func add(x1, x2 int) func(int, int) (int, int, int) {i := 0return func(x3, x4 int) (int, int, int) {i += 1return i, x1 + x2, x3 + x4} }func main() {add_func := add(1, 2)fmt.Println(add_func(4, 5))fmt.Println(add_func(1, 3))fmt.Println(add_func(2, 2)) }
fmt.Println(nextNumber()) 这一章没有讲清楚啊 ,例子也少了一行代码,补充上去。
package mainimport "fmt"func getSequence() func() int {i := 0return func() int {i += 1return i} }func main() {/* nextNumber 为一个函数,函数 i 为 0 */nextNumber := getSequence()/* 调用 nextNumber 函数,i 变量自增 1 并返回 */fmt.Println(nextNumber()) //这个执行结果是1fmt.Println(nextNumber()) //这个执行结果是2fmt.Println(nextNumber()) //这个执行结果是3/* 创建新的函数 nextNumber1,并查看结果 */nextNumber1 := getSequence() //当getSequence()被重新赋值之后,nextNumber的值应该销毁丢失的,但并没有fmt.Println(nextNumber1()) //这儿因为是新赋值的,所以是1fmt.Println(nextNumber()) //这一行代码是补充上例子的。这儿可不是新赋的值,重点说明这一个,这儿执行居然是4,这个值并没有被销毁,原因就是闭包导致的,尽管外面的函数销毁了,但是内部函数仍然存在,还可以继续走。这个就是闭包fmt.Println(nextNumber1()) //新赋值的,继续执行是2 }
*函数方法
Go 语言中同时有函数和方法。一个方法就是一个包含了接受者的函数,接受者可以是命名类型或者结构体类型的一个值或者是一个指针。所有给定类型的方法属于该类型的方法集。语法格式如下:
func (variable_name variable_data_type) function_name() [return_type]{/* 函数体*/ }
下面定义一个结构体类型和该类型的一个方法:
package mainimport ("fmt" )/* 定义结构体 */ type Circle struct {radius float64 }func main() {var c1 Circlec1.radius = 10.00fmt.Println("圆的面积 = ", c1.getArea()) }//该 method 属于 Circle 类型对象中的方法 func (c Circle) getArea() float64 {//c.radius 即为 Circle 类型对象中的属性return 3.14 * c.radius * c.radius }
*高级函数
package mainimport "fmt"func main() {fmt.Println("请输入要执行的操作:")fmt.Println(`1:登录 2:个人中心 3:注销`)var num intfmt.Scan(&num)var funcMap = map[int]func(){1: func() {fmt.Println("登录")},2: func() {fmt.Println("个人中心")},3: func() {fmt.Println("注销")},}funcMap[num]() }
package mainimport "fmt"func login() {fmt.Println("登录") }func userCenter() {fmt.Println("个人中心") }func logout() {fmt.Println("注销") }func main() {fmt.Println("请输入要执行的操作:")fmt.Println(`1:登录 2:个人中心 3:注销`)var num intfmt.Scan(&num)var funcMap = map[int]func(){1: login,2: userCenter,3: logout,}funcMap[num]() }
笔记
Go 没有面向对象,而我们知道常见的 Java。
C++ 等语言中,实现类的方法做法都是编译器隐式的给函数加一个 this 指针,而在 Go 里,这个 this 指针需要明确的申明出来,其实和其它 OO 语言并没有很大的区别。
在 C++ 中是这样的:
class Circle {public:float getArea() {return 3.14 * radius * radius;}private:float radius; }// 其中 getArea 经过编译器处理大致变为 float getArea(Circle *const c) {... }
在 Go 中则是如下:
func (c Circle) getArea() float64 {//c.radius 即为 Circle 类型对象中的属性return 3.14 * c.radius * c.radius }
关于值和指针,如果想在方法中改变结构体类型的属性,需要对方法传递指针,体会如下对结构体类型改变的方法 changRadis() 和普通的函数 change() 中的指针操作:
package mainimport ("fmt" )/* 定义结构体 */ type Circle struct {radius float64 }func main() { var c Circlefmt.Println(c.radius)c.radius = 10.00fmt.Println(c.getArea())c.changeRadius(20)fmt.Println(c.radius)change(&c, 30)fmt.Println(c.radius) } func (c Circle) getArea() float64 {return c.radius * c.radius } // 注意如果想要更改成功c的值,这里需要传指针 func (c *Circle) changeRadius(radius float64) {c.radius = radius }// 以下操作将不生效 //func (c Circle) changeRadius(radius float64) { // c.radius = radius //} // 引用类型要想改变值需要传指针 func change(c *Circle, radius float64) {c.radius = radius }
package mainimport ( "fmt" )/* 定义结构体 理解为一个内部类,这个内部类,只定义了一个变量radius, * 然后能过getArea()的这种形式,给它实现一个方法,这样别的地方就可以调用这个方法了。 * 有点绕,但是这样可以助于理解 */ type Circle struct {radius float64 }func main() {var c1 Circlec1.radius = 10.00fmt.Println("圆的面积 = ", c1.getArea())var c2 Circlec2 = getArea2(c1)fmt.Println(c2.radius)c3 := getArea3()fmt.Println(c3.radius) } /****************************先这么理解************************************/ // 这种相当于是给【Circle类】定义了一个方法 func (c Circle) getArea() float64 {//c.radius 即为 Circle 类型对象中的属性return 3.14 * c.radius * c.radius }// 这种是把【Circle类】作 为参数传递,并返回Circle类对象 func getArea2(c Circle) Circle {var temp Circletemp.radius = c.radius * 12return temp }// 这种是返回Circle类对象 func getArea3() Circle {var temp Circletemp.radius = 0.999return temp }
笔记
杨辉三角
package mainimport "fmt"//行数 const LINES int = 10// 杨辉三角 func ShowYangHuiTriangle() {nums := []int{}for i := 0; i < LINES; i++ {//补空白for j := 0; j < (LINES - i); j++ {fmt.Print(" ")}for j := 0; j < (i + 1); j++ {var length = len(nums)var value intif j == 0 || j == i {value = 1} else {value = nums[length-i] + nums[length-i-1]}nums = append(nums, value)fmt.Print(value, " ")}fmt.Println("")} }func main() {ShowYangHuiTriangle() }
九九乘法表
package mainimport ("fmt""strconv" )func add(a, b int) int {return a + b }func multiplicationTable() {for i := 1; i <= 9; i++ {for j := 1; j <= i; j++ {var ret stringif i*j < 10 && j != 1 {ret = " " + strconv.Itoa(i*j)} else {ret = strconv.Itoa(i * j)}fmt.Print(j, " * ", i, " = ", ret, " ")}fmt.Print("\n")} }func main() {multiplicationTable() }
菱形
package mainimport "fmt"func main() {// 长x := 9// 宽y := 9// 行数row := 1for row <= y {// 计算每行得星星数count := 0if row <= (y/2 + 1) {count = (2 * row - 1)} else {count = 2 * (y - row) + 1 }row++text := ""// 算出显示星星的范围star_min := ((x - count) / 2) + 1star_max := ((x - count) / 2) + countfor index := 1;index <= x;index++ {if index >= star_min && index <= star_max {text += "*"} else {text += " "}} fmt.Println(text)} }
所有参数都是值传递:slice,map,channel 会有传引用的错觉(比如切片,他背后对应的是一个数组,切片本身是一个数据结构,在这个数据结构中包含了指向了这个数组的指针。所以说,即便是在传值的情况下这个结构被复制到函数里了,在通过指针去操作这个数组的值的时候,其实是操作的是同一块空间,实际上是结构被复制了,但是结构里包含的指针指向的是同一个数组,所以才有这个错觉)
package main import "fmt" func main() {println("切片解决九九乘法表")var num []int //定义切片for i:=1;i<10;i++{num = append(num, i) //将数据添加到切片中去}for i:=1;i<10 ;i++ {for j:=1;j<i+1 ;j++ {value:=num[j-1]*i //计算 fmt.Printf("%d*%d=%d\t",j,i,value,)}fmt.Println()} }
map例子
package main import "fmt" func main() { // 创建一个空的map m := make(map[string]int) // 向map中添加键值对 m["apple"] = 5 m["banana"] = 10 m["orange"] = 7 // 检索map中的值 value, ok := m["apple"] if ok { fmt.Println("The value of apple is:", value) } else { fmt.Println("The key apple does not exist in the map.") } // 删除map中的键值对 delete(m, "banana") // 遍历map for key, value := range m { fmt.Println("Key:", key, "Value:", value) } }
12_变量作用域
Go 语言中变量可以在三个地方声明:
-
函数内定义的变量称为局部变量
-
函数外定义的变量称为全局变量
-
函数定义中的变量称为形式参数
局部变量
package mainimport "fmt"func main() {/* 声明局部变量 */var a, b, c int /* 初始化参数 */a = 10b = 20c = a + bfmt.Printf ("结果: a = %d, b = %d and c = %d\n", a, b, c) }
全局变量
package mainimport "fmt"/* 声明全局变量 */ var g intfunc main() {/* 声明局部变量 */var a, b int/* 初始化参数 */a = 10b = 20g = a + bfmt.Printf("结果: a = %d, b = %d and g = %d\n", a, b, g) }
形式参数
package mainimport "fmt"/* 声明全局变量 */ var a int = 20;func main() {/* main 函数中声明局部变量 */var a int = 10var b int = 20var c int = 0fmt.Printf("main()函数中 a = %d\n", a);c = sum( a, b);fmt.Printf("main()函数中 c = %d\n", c); }/* 函数定义-两数相加 */ func sum(a, b int) int {fmt.Printf("sum() 函数中 a = %d\n", a);fmt.Printf("sum() 函数中 b = %d\n", b);return a + b; }
笔记
package mainimport "fmt"/* 声明全局变量 */ var a int = 20func main() {/* main 函数中声明局部变量 */var a int = 10var b int = 20var c int = 0fmt.Printf("main()函数中 a = %d\n", a)c = sum(a, b)fmt.Printf("main()函数中 a = %d\n", a)fmt.Printf("main()函数中 c = %d\n", c) }/* 函数定义-两数相加 */ func sum(a, b int) int {a = a + 1fmt.Printf("sum() 函数中 a = %d\n", a)fmt.Printf("sum() 函数中 b = %d\n", b)return a + b }
main()函数中 a = 10 sum() 函数中 a = 11 sum() 函数中 b = 20 main()函数中 a = 10 main()函数中 c = 31
package mainimport "fmt"func main(){var a int = 0fmt.Println("for start")for a:=0; a < 10; a++ {fmt.Println(a)}fmt.Println("for end")fmt.Println(a) }
package mainimport "fmt"func main(){var a int = 0fmt.Println("for start")for a = 0; a < 10; a++ {fmt.Println(a)}fmt.Println("for end")fmt.Println(a) }
全局变量可以在整个包甚至外部包(被导出后)使用。
下述代码在 test.go 中定义了全局变量 Total_sum,然后在 hello.go 中引用。
test.go:
package main import "fmt" var Total_sum int = 0 func Sum_test(a int, b int) int {fmt.Printf("%d + %d = %d\n", a, b, a+b)Total_sum += (a + b)fmt.Printf("Total_sum: %d\n", Total_sum)return a+b }
hello.go:
package mainimport ("fmt" )func main() {var sum intsum = Sum_test(2, 3)fmt.Printf("sum: %d; Total_sum: %d\n", sum, Total_sum) }
13_数组
声明数组
var arrayName [size]dataType
例:
var balance [10]float32
初始化数组
var numbers [5]int
var numbers = [5]int{1, 2, 3, 4, 5}
numbers := [5]int{1, 2, 3, 4, 5}
<!-- 数组的类型也会区分大小 [5]int 和 [10]int 是不同的类型 -->
-
数组长度不确定,可用 ... 代替数组的长度
// 数组长度不确定,可用 ... 代替数组的长度 var balance = [...]float32{1000.0, 2.0, 3.4, 7.0, 50.0} // 或 balance := [...]float32{1000.0, 2.0, 3.4, 7.0, 50.0}
-
根据索引初始化数组
// 将索引为 1 和 3 的元素初始化 balance := [5]float32{1:2.0,3:7.0}
访问数组元素
数组元素可以通过索引(位置)来读取
var salary float32 = balance[9]
package mainimport "fmt"func main() {var n [10]int /* n 是一个长度为 10 的数组 */var i,j int/* 为数组 n 初始化元素 */ for i = 0; i < 10; i++ {n[i] = i + 100 /* 设置元素为 i + 100 */}/* 输出每个数组元素的值 */for j = 0; j < 10; j++ {fmt.Printf("Element[%d] = %d\n", j, n[j] )} }
package mainimport "fmt"func main() {var i,j,k int// 声明数组的同时快速初始化数组balance := [5]float32{1000.0, 2.0, 3.4, 7.0, 50.0}/* 输出数组元素 */ ...for i = 0; i < 5; i++ {fmt.Printf("balance[%d] = %f\n", i, balance[i] )}balance2 := [...]float32{1000.0, 2.0, 3.4, 7.0, 50.0}/* 输出每个数组元素的值 */for j = 0; j < 5; j++ {fmt.Printf("balance2[%d] = %f\n", j, balance2[j] )}// 将索引为 1 和 3 的元素初始化balance3 := [5]float32{1:2.0,3:7.0} for k = 0; k < 5; k++ {fmt.Printf("balance3[%d] = %f\n", k, balance3[k] )} }
更多内容
数组对 Go 语言来说是非常重要的,以下我们将介绍数组更多的内容:
内容 | 描述 |
---|---|
多维数组 | Go 语言支持多维数组,最简单的多维数组是二维数组 |
向函数传递数组 | 你可以向函数传递数组参数 |
多维数组
以下实例声明了三维的整型数组:
var threedim [5][10][4]int
二维数组
var arrayName [ x ][ y ] variable_type
二维数组中的元素可通过 a[ i ][ j ]
来访问
package mainimport "fmt"func main() {// Step 1: 创建数组values := [][]int{}// Step 2: 使用 append() 函数向空的二维数组添加两行一维数组row1 := []int{1, 2, 3}row2 := []int{4, 5, 6}values = append(values, row1)values = append(values, row2)// Step 3: 显示两行数据fmt.Println("Row 1")fmt.Println(values[0])fmt.Println("Row 2")fmt.Println(values[1])// Step 4: 访问第一个元素fmt.Println("第一个元素为:")fmt.Println(values[0][0]) }
初始化二维数组
a := [3][4]int{ {0, 1, 2, 3} , /* 第一行索引为 0 */{4, 5, 6, 7} , /* 第二行索引为 1 */{8, 9, 10, 11}, /* 第三行索引为 2 */ }a := [3][4]int{ {0, 1, 2, 3} , /* 第一行索引为 0 */{4, 5, 6, 7} , /* 第二行索引为 1 */{8, 9, 10, 11}} /* 第三行索引为 2 */
package mainimport "fmt"func main() {// 创建二维数组sites := [2][2]string{}// 向二维数组添加元素sites[0][0] = "Google"sites[0][1] = "Runoob"sites[1][0] = "Taobao"sites[1][1] = "Weibo"// 显示结果fmt.Println(sites) }
访问二维数组
val := a[2][3] // 或 var value int = a[2][3]
package mainimport "fmt"func main() {/* 数组 - 5 行 2 列*/var a = [5][2]int{ {0,0}, {1,2}, {2,4}, {3,6},{4,8}}var i, j int/* 输出数组元素 */for i = 0; i < 5; i++ {for j = 0; j < 2; j++ {fmt.Printf("a[%d][%d] = %d\n", i,j, a[i][j] )}} }
-
创建各个维度元素数量不一致的多维数组
package mainimport "fmt"func main() {// 创建空的二维数组animals := [][]string{}// 创建三一维数组,各数组长度不同row1 := []string{"fish", "shark", "eel"}row2 := []string{"bird"}row3 := []string{"lizard", "salamander"}// 使用 append() 函数将一维数组添加到二维数组中animals = append(animals, row1)animals = append(animals, row2)animals = append(animals, row3)// 循环输出for i := range animals {// fmt.Printf("Row: %v\n", i)fmt.Println(animals[i])} }
向函数传递数组
func myFunction(param [10]int) {.... } // 形参设定数组大小
func myFunction(param []int) {.... } // 形参未设定数组大小
func getAverage(arr []int, size int) float32 {var i intvar avg, sum float32 for i = 0; i < size; ++i {sum += arr[i]}avg = sum / sizereturn avg; }
package mainimport "fmt"func main() {/* 数组长度为 5 */var balance = [5]int {1000, 2, 3, 17, 50}var avg float32/* 数组作为参数传递给函数 */avg = getAverage( balance, 5 ) ;/* 输出返回的平均值 */fmt.Printf( "平均值为: %f ", avg ); } func getAverage(arr [5]int, size int) float32 {var i,sum intvar avg float32 for i = 0; i < size;i++ {sum += arr[i]}avg = float32(sum) / float32(size)return avg; }
package main import ("fmt" ) func main() {a := 1.69b := 1.7c := a * b // 结果应该是2.873fmt.Println(c) // 输出的是2.8729999999999998 }
package main import ("fmt" ) func main() {a := 1690 // 表示1.69b := 1700 // 表示1.70c := a * b // 结果应该是2873000表示 2.873fmt.Println(c) // 内部编码fmt.Println(float64(c) / 1000000) // 显示 }
package mainimport "fmt"// 函数接受一个数组作为参数 func modifyArray(arr [5]int) {for i := 0; i < len(arr); i++ {arr[i] = arr[i] * 2} }// 函数接受一个数组的指针作为参数 func modifyArrayWithPointer(arr *[5]int) {for i := 0; i < len(*arr); i++ {(*arr)[i] = (*arr)[i] * 2} }func main() {// 创建一个包含5个元素的整数数组myArray := [5]int{1, 2, 3, 4, 5}fmt.Println("Original Array:", myArray)// 传递数组给函数,但不会修改原始数组的值modifyArray(myArray)fmt.Println("Array after modifyArray:", myArray)// 传递数组的指针给函数,可以修改原始数组的值modifyArrayWithPointer(&myArray)fmt.Println("Array after modifyArrayWithPointer:", myArray) }/* Original Array: [1 2 3 4 5] Array after modifyArray: [1 2 3 4 5] Array after modifyArrayWithPointer: [2 4 6 8 10] */
笔记
func main() {var array = []int{1, 2, 3, 4, 5}/* 未定义长度的数组只能传给不限制数组长度的函数 */setArray(array)/* 定义了长度的数组只能传给限制了相同数组长度的函数 */var array2 = [5]int{1, 2, 3, 4, 5}setArray2(array2) }func setArray(params []int) {fmt.Println("params array length of setArray is : ", len(params)) }func setArray2(params [5]int) {fmt.Println("params array length of setArray2 is : ", len(params)) }
// 多维数组传参 package mainfunc prt(arr [][] float32){for i:= 0;i < 3;i++{println(arr[i][0])} }func main(){var arr = [][]float32 {{-1,-2},{-3,-4},{-5}}prt(arr) }
声明数组:
nums := [3]int{1,2,3,}
声明切片:
nums := []int{1,2,3}
没有所谓没有声明长度的数组存在
与 c 语言不同,go 的数组作为函数参数传递的是副本,函数内修改数组并不改变原来的数组。
package main import "fmt" func change(nums[3] int){nums[0]=100 } func main() {var nums=[3]int{1,2,3} change(nums) //nums并未被改变 fmt.Println(nums[0]) return }
这里没有区分 数组 与 切片
-
Go 语言的数组是值,其长度是其类型的一部分,作为函数参数时,是 值传递,函数中的修改对调用者不可见
-
Go 语言中对数组的处理,一般采用 切片 的方式,切片包含对底层数组内容的引用,作为函数参数时,类似于 指针传递,函数中的修改对调用者可见
// 数组 b := [...]int{2, 3, 5, 7, 11, 13}func boo(tt [6]int) {tt[0], tt[len(tt)-1] = tt[len(tt)-1], tt[0] }boo(b) fmt.Println(b) // [2 3 5 7 11 13] // 切片 p := []int{2, 3, 5, 7, 11, 13}func poo(tt []int) {tt[0], tt[len(tt)-1] = tt[len(tt)-1], tt[0] } poo(p) fmt.Println(p) // [13 3 5 7 11 2]
函数里面的数组改动对定义类型为切片的数组外部数据也会有影响:
func main(){var slice1 = []int{1,2,3,4,5,6,7,8}var array = [8]int{1,2,3,4,5,6,7,8}change_arr1(slice1)change_arr2(array)fmt.Println("slice1 = ", slice1) //[10,2,3,4,5,6,7,8]fmt.Println("array = ", array) //[1,2,3,4,5,6,7,8] }func change_arr1(arr []int) {arr[0] = 10fmt.Println("arr = ", arr) //[10,2,3,4,5,6,7,8] } func change_arr2(arr [8]int) {arr[0] = 10fmt.Println("arr = ", arr) //[10,2,3,4,5,6,7,8] }
看了评论,总结下几位老哥的结论,外加自己想到的引用传递。
package main import "fmt" // Go 语言的数组是值,其长度是其类型的一部分,作为函数参数时,是 值传递,函数中的修改对调用者不可见 func change1(nums [3]int) { nums[0] = 4 } // 传递进来数组的内存地址,然后定义指针变量指向该地址,则会改变数组的值 func change2(nums *[3]int) { nums[0] = 5 } // Go 语言中对数组的处理,一般采用 切片 的方式,切片包含对底层数组内容的引用,作为函数参数时,类似于 指针传递,函数中的修改对调用者可见 func change3(nums []int) { nums[0] = 6 } func main() { var nums1 = [3]int{1, 2, 3} var nums2 = []int{1, 2, 3} change1(nums1) fmt.Println(nums1) // [1 2 3] change2(&nums1) fmt.Println(nums1) // [5 2 3] change3(nums2) fmt.Println(nums2) // [6 2 3] }
笔记
package main import "fmt"func main() {var a = [3][5]int {{1, 2, 3, 4, 5}, {0, 9, 8, 7, 6}, {3, 4, 5, 6, 7}}var i, j intfor i = 0; i < 3; i++ {for j = 0; j < 5; j++ {fmt.Printf("a[%d][%d] = %d\n", i,j, a[i][j])}} }
将二维数组按行输出:
package mainimport "fmt" func main() {/* 数组 - 5 行 2 列 */var a = [5][2]int{{0,0}, {1,2}, {2,4}, {3,6}, {4,8}}var i, j int /* 输出数组元素 */for i =0; i < 5; i++ {fmt.Printf("第 %d 行:", i)for j = 0; j < 2; j++ {fmt.Printf("%d, ", a[i][j])}// 换行fmt.Println()} }
range 方式循环二维数组
package mainimport "fmt"func main() {arr := [...][]int{{1, 2, 3, 4},{10, 20, 30, 40},}for i := range arr {for j := range arr[i] {fmt.Println(arr[i][j])}} }
14_指针
*为什么使用指针
-
避免数据拷贝:当函数接收一个大的结构体作为参数时,如果传递的是值(而不是指针),Go会创建这个结构体的一个副本,并将副本传递给函数。这可能会消耗额外的内存和计算资源,尤其是当结构体很大时。通过传递指针,我们直接操作原始变量,避免了不必要的拷贝
-
能够修改原始数据:如果
printBook
函数需要修改Books
结构体的字段,那么它必须接收一个指向该结构体的指针。传递值(而不是指针)意味着函数只能修改副本,而不能修改原始数据。在这个例子中,虽然printBook
函数只是打印信息,没有修改任何数据,但使用指针作为参数仍然是一个好习惯,尤其是当函数可能会修改数据时 -
灵活性:通过指针,函数内部可以检查指针是否为
nil
(即是否指向有效的内存地址),这增加了代码的健壮性。此外,指针还可以用于动态分配内存(尽管在Go中通常使用new
函数或字面量初始化来分配内存)
*项目理解
type UserDao struct { *gorm.DB } /* *gorm.DB 表示 UserDao 结构体中嵌入了一个指向 gorm.DB 类型的指针,这样做的好处是,你可以直接通过 UserDao 的实例来调用 gorm.DB 的方法,而不需要使用额外的字段名来访问它 通过嵌入指针而不是值,`UserDao` 可以引用一个现有的 `gorm.DB` 实例,而不是拥有一个自己的副本 */func NewUserDao(cxt context.Context) *UserDao { return &UserDao{NewDBClient(cxt)} } /* & 符号获取这个 UserDao 实例的地址,并返回一个指向 UserDao 的指针 */
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {/* ... */} /* *Engine作为接收器类型,不需要每次调用方法时复制整个结构体,实现了接口,并且可以修改Engine实例的状态(字段) */
变量是一种使用方便的占位符,用于引用计算机内存地址
Go 语言的取地址符是 &,放到一个变量前使用就会返回相应变量的内存地址
package mainimport "fmt"func main() {var a int = 10 fmt.Printf("变量的地址: %x\n", &a ) }
什么是指针
一个指针变量指向了一个值的内存地址
类似于变量和常量,在使用指针前你需要声明指针
var var_name *var-type
var-type 为指针类型,var_name 为指针变量名,* 号用于指定变量是作为一个指针
var ip *int /* 指向整型*/ var fp *float32 /* 指向浮点型 */
如何使用指针
指针使用流程:
-
定义指针变量
-
为指针变量赋值
-
访问指针变量中指向地址的值
在指针类型前面加上 * 号(前缀)来获取指针所指向的内容
package mainimport "fmt"func main() {var a int= 20 /* 声明实际变量 */var ip *int /* 声明指针变量 */ip = &a /* 指针变量的存储地址 */fmt.Printf("a 变量的地址是: %x\n", &a )/* 指针变量的存储地址 */fmt.Printf("ip 变量储存的指针地址: %x\n", ip )/* 使用指针访问值 */fmt.Printf("*ip 变量的值: %d\n", *ip ) }/* a 变量的地址是: 20818a220 ip 变量储存的指针地址: 20818a220 *ip 变量的值: 20 */
空指针
当一个指针被定义后没有分配到任何变量时,它的值为 nil
一个指针变量通常缩写为 ptr
package mainimport "fmt"func main() {var ptr *intfmt.Printf("ptr 的值为 : %x\n", ptr ) }
空指针判断:
if(ptr != nil) /* ptr 不是空指针 */ if(ptr == nil) /* ptr 是空指针 */
指针更多内容
内容 | 描述 |
---|---|
Go 指针数组 | 你可以定义一个指针数组来存储地址 |
Go 指向指针的指针 | Go 支持指向指针的指针 |
Go 向函数传递指针参数 | 通过引用或地址传参,在函数调用时可以改变其值 |
指针数组
定义了长度为 3 的整型数组:
package main import "fmt"const MAX int = 3func main() {a := []int{10,100,200}var i intfor i = 0; i < MAX; i++ {fmt.Printf("a[%d] = %d\n", i, a[i] )} }
有一种情况,我们可能需要保存数组,这样我们就需要使用到指针
以下声明了整型指针数组:
var ptr [MAX]*int;
ptr 为整型指针数组。因此每个元素都指向了一个值。以下实例的三个整数将存储在指针数组中
package mainimport "fmt"const MAX int = 3func main() {a := []int{10,100,200}var i intvar ptr [MAX]*int;for i = 0; i < MAX; i++ {ptr[i] = &a[i] /* 整数地址赋值给指针数组 */}for i = 0; i < MAX; i++ {fmt.Printf("a[%d] = %d\n", i,*ptr[i] )} }
指向指针的指针
如果一个指针变量存放的又是另一个指针变量的地址,则称这个指针变量为指向指针的指针变量
当定义一个指向指针的指针变量时,第一个指针存放第二个指针的地址,第二个指针存放变量的地址:
指向指针的指针变量声明格式如下:
var ptr **int;
以上指向指针的指针变量为整型
访问指向指针的指针变量值需要使用两个 * 号,如下所示
package mainimport "fmt"func main() {var a intvar ptr *intvar pptr **inta = 3000/* 指针 ptr 地址 */ptr = &a/* 指向指针 ptr 地址 */pptr = &ptr/* 获取 pptr 的值 */fmt.Printf("变量 a = %d\n", a )fmt.Printf("指针变量 *ptr = %d\n", *ptr )fmt.Printf("指向指针的指针变量 **pptr = %d\n", **pptr) }
指针作为函数参数
Go 语言允许向函数传递指针,只需要在函数定义的参数上设置为指针类型即可
以下实例演示了如何向函数传递指针,并在函数调用后修改函数内的值
package mainimport "fmt"func main() {/* 定义局部变量 */var a int = 100var b int = 200fmt.Printf("交换前 a 的值 : %d\n", a )fmt.Printf("交换前 b 的值 : %d\n", b )/* 调用函数用于交换值* &a 指向 a 变量的地址* &b 指向 b 变量的地址*/swap(&a, &b);fmt.Printf("交换后 a 的值 : %d\n", a )fmt.Printf("交换后 b 的值 : %d\n", b ) }func swap(x *int, y *int) {var temp inttemp = *x /* 保存 x 地址的值 */*x = *y /* 将 y 赋值给 x */*y = temp /* 将 temp 赋值给 y */ }
笔记
以下是一个更简洁的变量交换实例:
package mainimport "fmt"func main() {/* 定义局部变量 */var a int = 100var b int= 200swap(&a, &b);fmt.Printf("交换后 a 的值 : %d\n", a )fmt.Printf("交换后 b 的值 : %d\n", b ) }/* 交换函数这样写更加简洁,也是 go 语言的特性,可以用下,c++ 和 c# 是不能这么干的 */func swap(x *int, y *int){*x, *y = *y, *x }
以下是一个更更简洁的变量交换实例:
package mainimport "fmt"func main() {/* 定义局部变量 */var a int = 100var b int= 200a, b = b, afmt.Printf("交换后 a 的值 : %d\n", a )fmt.Printf("交换后 b 的值 : %d\n", b ) }
15_结构体
定义结构体
type struct_variable_type struct {member definitionmember definition...member definition }
variable_name := structure_variable_type {value1, value2...valuen} // 或 variable_name := structure_variable_type { key1: value1, key2: value2..., keyn: valuen}
package mainimport "fmt"type Books struct {title stringauthor stringsubject stringbook_id int }func main() {// 创建一个新的结构体fmt.Println(Books{"Go 语言", "www.runoob.com", "Go 语言教程", 6495407})// 也可以使用 key => value 格式fmt.Println(Books{title: "Go 语言", author: "www.runoob.com", subject: "Go 语言教程", book_id: 6495407})// 忽略的字段为 0 或 空fmt.Println(Books{title: "Go 语言", author: "www.runoob.com"}) }
访问结构体成员
// 结构体.成员名"
package mainimport "fmt"type Books struct {title stringauthor stringsubject stringbook_id int }func main() {var Book1 Books /* 声明 Book1 为 Books 类型 */var Book2 Books /* 声明 Book2 为 Books 类型 *//* book 1 描述 */Book1.title = "Go 语言"Book1.author = "www.runoob.com"Book1.subject = "Go 语言教程"Book1.book_id = 6495407/* book 2 描述 */Book2.title = "Python 教程"Book2.author = "www.runoob.com"Book2.subject = "Python 语言教程"Book2.book_id = 6495700/* 打印 Book1 信息 */fmt.Printf( "Book 1 title : %s\n", Book1.title)fmt.Printf( "Book 1 author : %s\n", Book1.author)fmt.Printf( "Book 1 subject : %s\n", Book1.subject)fmt.Printf( "Book 1 book_id : %d\n", Book1.book_id)/* 打印 Book2 信息 */fmt.Printf( "Book 2 title : %s\n", Book2.title)fmt.Printf( "Book 2 author : %s\n", Book2.author)fmt.Printf( "Book 2 subject : %s\n", Book2.subject)fmt.Printf( "Book 2 book_id : %d\n", Book2.book_id) }
结构体作为函数参数
package mainimport "fmt"type Books struct {title stringauthor stringsubject stringbook_id int }func main() {var Book1 Books /* 声明 Book1 为 Books 类型 */var Book2 Books /* 声明 Book2 为 Books 类型 *//* book 1 描述 */Book1.title = "Go 语言"Book1.author = "www.runoob.com"Book1.subject = "Go 语言教程"Book1.book_id = 6495407/* book 2 描述 */Book2.title = "Python 教程"Book2.author = "www.runoob.com"Book2.subject = "Python 语言教程"Book2.book_id = 6495700/* 打印 Book1 信息 */printBook(Book1)/* 打印 Book2 信息 */printBook(Book2) }func printBook( book Books ) {fmt.Printf( "Book title : %s\n", book.title)fmt.Printf( "Book author : %s\n", book.author)fmt.Printf( "Book subject : %s\n", book.subject)fmt.Printf( "Book book_id : %d\n", book.book_id) }
结构体指针
var struct_pointer *Books struct_pointer = &Book1 struct_pointer.title
package mainimport "fmt"type Books struct {title stringauthor stringsubject stringbook_id int }func main() {var Book1 Books /* 声明 Book1 为 Books 类型 */var Book2 Books /* 声明 Book2 为 Books 类型 *//* book 1 描述 */Book1.title = "Go 语言"Book1.author = "www.runoob.com"Book1.subject = "Go 语言教程"Book1.book_id = 6495407/* book 2 描述 */Book2.title = "Python 教程"Book2.author = "www.runoob.com"Book2.subject = "Python 语言教程"Book2.book_id = 6495700/* 打印 Book1 信息 */printBook(&Book1)/* 打印 Book2 信息 */printBook(&Book2) } func printBook( book *Books ) {fmt.Printf( "Book title : %s\n", book.title)fmt.Printf( "Book author : %s\n", book.author)fmt.Printf( "Book subject : %s\n", book.subject)fmt.Printf( "Book book_id : %d\n", book.book_id) }
笔记
package mainimport "fmt"type Books struct {title stringauthor stringsubject stringbook_id int }func changeBook(book Books) {book.title = "book1_change" }func main() {var book1 Booksbook1.title = "book1"book1.author = "zuozhe"book1.book_id = 1changeBook(book1)fmt.Println(book1) }
如果想在函数里面改变结果体数据内容,需要传入指针:
package mainimport "fmt"type Books struct {title stringauthor stringsubject stringbook_id int }func changeBook(book *Books) {book.title = "book1_change" }func main() {var book1 Booksbook1.title = "book1"book1.author = "zuozhe"book1.book_id = 1changeBook(&book1)fmt.Println(book1) }
struct 类似于 java 中的类,可以在 struct 中定义成员变量
要访问成员变量,可以有两种方式:
-
1.通过 struct 变量.成员 变量来访问
-
2.通过 struct 指针.成员 变量来访问
不需要通过 getter, setter 来设置访问权限
type Rect struct{ //定义矩形类x,y float64 //类型只包含属性,并没有方法width,height float64 } func (r *Rect) Area() float64{ //为Rect类型绑定Area的方法,*Rect为指针引用可以修改传入参数的值return r.width*r.height //方法归属于类型,不归属于具体的对象,声明该类型的对象即可调用该类型的方法 }
利用指针改变结构体对应的值:
package mainimport ("fmt""strconv" )type Books struct {title stringauthor stringsubject stringbook_id int }func printBook(book Books) {/*打印函数,没有返回值,传入结构体*//*结构体只作为临时参数*/fmt.Printf("Book title: %s\n", book.title)fmt.Printf("Book author: %s\n", book.author)fmt.Printf("Book subject: %s\n", book.subject)fmt.Printf("Book id: %d\n", book.book_id) }func changeBook(book *Books, new_info_type string, new_info string) {if new_info_type == "title" {book.title = new_info} else {if new_info_type == "author" {book.author = new_info} else {if new_info_type == "subject" {book.subject = new_info} else {int, err := strconv.Atoi(new_info) // 将字符串转换为整数if err == nil {book.book_id = int}}}} }func main() {book1 := Books{"Go Language", "www.golang.com", "Go语言基础", 6495407}book2 := Books{"Harry Porter", "www.youku.com", "Deathly Hallows", 6448722}fmt.Println("------原始信息------")printBook(book1)printBook(book2)fmt.Println("------新的信息-------")changeBook(&book1, "title", "Summer")changeBook(&book2, "book_id", "1111111")printBook(book1)printBook(book2) }
结构体中属性的首字母大小写问题
-
首字母大写相当于 public
-
首字母小写相当于 private
注意: 这个 public 和 private 是相对于包(go 文件首行的 package 后面跟的包名)来说的
敲黑板,划重点
当要将结构体对象转换为 JSON 时,对象中的属性首字母必须是大写,才能正常转换为 JSON
type Person struct {Name string //Name字段首字母大写age int //age字段首字母小写 }func main() {person:=Person{"小明",18}if result,err:=json.Marshal(&person);err==nil{ //json.Marshal 将对象转换为json字符串fmt.Println(string(result))} } // {"Name":"小明"} //只有Name,没有age
type Person struct{Name string //都是大写Age int } // {"Name":"小明","Age":18} //两个字段都有
那这样 JSON 字符串以后就只能是大写了么? 当然不是,可以使用 tag 标记要返回的字段名
type Person struct{Name string `json:"name"` //标记json名字为name Age int `json:"age"`Time int64 `json:"-"` // 标记忽略该字段}func main(){person:=Person{"小明",18, time.Now().Unix()} // 获取当前时间的Unix时间戳(从1970年1月1日以来的秒数)if result,err:=json.Marshal(&person);err==nil{fmt.Println(string(result)) // 将字节切片转换为字符串} } // {"name":"小明","age":18}
-
定义的结构体如果只在当前包内使用,结构体的属性不用区分大小写
-
如果想要被其他的包引用,那么结构体的属性的首字母需要大写
package mode//结构体小写开头的属性只能包内调用 type Books struct { Title string Author string Subject string book_id int }
import ( "fmt" "src/mode" )func main() {var Book1 mode.Books /* 声明 Book1 为 Books 类型 *//* book 1 描述 */Book1.Title = "Go 语言"Book1.Author = "www.runoob.com"Book1.Subject = "Go 语言教程"// 如果进行了如下调用,则会报错// Book1.book_id = 6495407/* 打印 Book1 信息 */printBook(Book1)}func printBook( book Books ) {fmt.Printf( "Book title : %s\n", book.Title)fmt.Printf( "Book author : %s\n", book.Author)fmt.Printf( "Book subject : %s\n", book.Subject)// 无法调用// fmt.Printf( "Book book_id : %d\n", book.book_id) }
package mainimport "fmt"type Books struct {title stringauthor stringsubject stringbook_id int }var book1 = Books{"Go 入门到放弃", "yuantiankai", "go系列教程", 012231}func main() {var b *Booksb = &book1fmt.Println(b) //&{Go 语言 www.runoob.com Go 语言教程 6495407}fmt.Println(*b) //{Go 语言 www.runoob.com Go 语言教程 6495407}fmt.Println(&b) //0xc000082018fmt.Println(book1) //{Go 语言 www.runoob.com Go 语言教程 6495407} }
var b *Books //就是说b这个指针是Books类型的。 b = &Book1 //Book1是Books的一个实例化的结构,&Book1就是把这个结构体的内存地址赋给了b, *b //那么在使用的时候,只要在b的前面加个*号,就可以把b这个内存地址对应的值给取出来了 &b // 就是取了b这个指针的内存地址,也就是b这个指针是放在内存空间的什么地方的。 Book1 // 就是Book1这个结构体,打印出来就是它自己。也就是指针b前面带了*号的效果。
只有一个特殊的地方,尽管 b 所表示的是 Book1 对象的内存地址,但是,在从 b 对应的内存地址取属性值的时候,就不是 *b.title 了。而是直接使用b.title,这点很特殊,它的效果就相当于 Book1.title:
fmt.Println(b.title) //Go 入门到放弃 fmt.Println(Book1.title) //Go 入门到放弃 fmt.Println(b.author) //yuantiankai fmt.Println(Book1.author) //yuantiankai
具体区别:
比如我们要写一个函数修改结构体里的一个值,那么我们需要将修改过后的值返回出来,然后再重新赋值,比如这样:
package mainimport "fmt"type Books struct {title stringauthor stringsubject stringbook_id int }func changeBook(book Books) string { //把book对象传进来,返回的值是string类型的,也就是将被修改的值返回出来。book.title = "book1_change"return book.title }func main() {var book1 Books;book1.title = "book1"book1.author = "zuozhe"book1.book_id = 1var res = changeBook(book1) //然后在外面拿到被修改的值book1.title = res // 再重新赋值fmt.Println(book1) }
如果我们这样做,是行不通的,看如下代码:
package mainimport "fmt"type Books struct {title stringauthor stringsubject stringbook_id int }func changeBook(book Books) { //这个函数没有返回值book.title = "book1_change" //仅仅是修改了一下 }func main() {var book1 Books;book1.title = "book1"book1.author = "zuozhe"book1.book_id = 1changeBook(book1) //将book1传进去,本意是想修改book1里面的值fmt.Println(book1) }
但是有了结构体指针,就不是值传递了,而是引用传递(传递的是地址)了。就可以这么写了
package mainimport "fmt"type Books struct {title stringauthor stringsubject stringbook_id int }func changeBook(book *Books) { // 这个方法传入的参数一个Books类型的指针book.title = "book1_change" //直接用指针.属性的方式,修改原地址的值。 }func main() {var book1 Books;book1.title = "book1"book1.author = "zuozhe"book1.book_id = 1changeBook(&book1) //将book1这个对象的内存地址传进去,fmt.Println(book1) }
/* GO语言结构体指针1. 没有特别,与一般指针一致。2. 调用成员变量可以使用 变量名.成员名、指针名.成员名 都可以,相当于自动解引用,不需要c语言的 -> 符号。3. GO语言的自动解引用只支持到一级指针,多级指针就要至少手动解引用至一级指针。看来GO是做了,但是没完全做。 */ package mainimport "fmt"func t02_test1() {// Books 在包内定义过,可以直接使用var book Booksbook.title = "Go 语言"book.author = "www.runoob.com"book.subject = "Go 语言教程"book.book_id = 6495407printBook(book)printBookByPoniter(&book)// GO语言不支持形如 &&book 这样直接构造多级地址,需要使用变量来构造p1 := &bookp2 := &p1printBookByPoniter2(p2)} func printBook(book Books) {// 传参的本质是为局部变量赋值,此book为本函数的局部变量,和外部的book是两个独立的变量。fmt.Printf("Book title : %s\n", book.title)fmt.Printf("Book author : %s\n", book.author)fmt.Printf("Book subject : %s\n", book.subject)fmt.Printf("Book book_id : %d\n", book.book_id) } func printBookByPoniter(book *Books) {// 仍然遵循传参的本质是为局部变量赋值。此book的值是外部book的内存地址,可以由此间接操作外部变量。不能说是将外部变量传递了进来,模糊化复杂化的概念不可取。// 在GO语言中,可以直接使用 指针.成员名 ,无需像C语言那样解引用。类似Rust的自动解引用。fmt.Printf("Book title : %s\n", book.title)fmt.Printf("Book author : %s\n", book.author)fmt.Printf("Book subject : %s\n", book.subject)fmt.Printf("Book book_id : %d\n", book.book_id) } func printBookByPoniter2(book **Books) {// GO语言的自动解引用只支持到一级指针,多级指针就要至少手动解引用至一级指针,否则报错。看来GO是做了,但是没完全做。fmt.Printf("Book title : %s\n", (*book).title)fmt.Printf("Book author : %s\n", (*book).author)fmt.Printf("Book subject : %s\n", (*book).subject)fmt.Printf("Book book_id : %d\n", (*book).book_id) }
16_切片(Slice)
Go 语言切片是对数组的抽象
Go 数组的长度不可改变,在特定场景中这样的集合就不太适用
Go 中提供了一种灵活,功能强悍的内置类型切片("动态数组"),与数组相比切片的长度是不固定的,可以追加元素,在追加时可能使切片的容量增大
定义切片
可以声明一个未指定大小的数组来定义切片:
var identifier []type
切片不需要说明长度
或使用 make() 函数来创建切片:
var slice1 []type = make([]type, len) // 也可以简写为 slice1 := make([]type, len)
也可以指定容量,其中 capacity 为可选参数
make([]T, length, capacity)
这里 len 是数组的长度并且也是切片的初始长度
切片初始化
s :=[] int {1,2,3 }
接初始化切片,[] 表示是切片类型,{1,2,3} 初始化值依次是 1,2,3,其 cap=len=3
s := arr[:]
初始化切片 s,是数组 arr 的引用
s := arr[startIndex:endIndex]
将 arr 中从下标 startIndex 到 endIndex-1 下的元素创建为一个新的切片
s := arr[startIndex:]
默认 endIndex 时将表示一直到arr的最后一个元素
s := arr[:endIndex]
默认 startIndex 时将表示从 arr 的第一个元素开始
s1 := s[startIndex:endIndex]
通过切片 s 初始化切片 s1
s :=make([]int,len,cap) var numbers = make([]int,3,5)
通过内置函数 make() 初始化切片s,[]int 标识为其元素类型为 int 的切片
*len() 和 cap() 函数
切片是可索引的,并且可以由 len() 方法获取长度
切片提供了计算容量的方法 cap() 可以测量切片最长可以达到多少
package mainimport "fmt"func main() {var numbers = make([]int,3,5)printSlice(numbers) }func printSlice(x []int){fmt.Printf("len=%d cap=%d slice=%v\n",len(x),cap(x),x) }
空(nil)切片
一个切片在未初始化之前默认为 nil,长度为 0
package mainimport "fmt"func main() {var numbers []intprintSlice(numbers)if(numbers == nil){fmt.Printf("切片是空的")} }func printSlice(x []int){fmt.Printf("len=%d cap=%d slice=%v\n",len(x),cap(x),x) }
切片截取
可以通过设置下限及上限来设置截取切片[lower-bound:upper-bound]
,实例如下:
package mainimport "fmt"func main() {/* 创建切片 */numbers := []int{0,1,2,3,4,5,6,7,8} printSlice(numbers)/* 打印原始切片 */fmt.Println("numbers ==", numbers)/* 打印子切片从索引1(包含) 到索引4(不包含)*/fmt.Println("numbers[1:4] ==", numbers[1:4])/* 默认下限为 0*/fmt.Println("numbers[:3] ==", numbers[:3])/* 默认上限为 len(s)*/fmt.Println("numbers[4:] ==", numbers[4:])numbers1 := make([]int,0,5)printSlice(numbers1)/* 打印子切片从索引 0(包含) 到索引 2(不包含) */number2 := numbers[:2]printSlice(number2)/* 打印子切片从索引 2(包含) 到索引 5(不包含) */number3 := numbers[2:5]printSlice(number3)}func printSlice(x []int){fmt.Printf("len=%d cap=%d slice=%v\n",len(x),cap(x),x) }
切片排序
var list = []int{4, 5, 3, 2, 7} fmt.Println("排序前:", list) sort.Ints(list) fmt.Println("升序:", list) sort.Sort(sort.Reverse(sort.IntSlice(list))) fmt.Println("降序:", list)
*append() 和 copy() 函数
如果想增加切片的容量,我们必须创建一个新的更大的切片并把原分片的内容都拷贝过来
下面的代码描述了从拷贝切片的 copy 方法和向切片追加新元素的 append 方法
package mainimport "fmt"func main() {var numbers []intprintSlice(numbers)/* 允许追加空切片 */numbers = append(numbers, 0)printSlice(numbers)/* 向切片添加一个元素 */numbers = append(numbers, 1)printSlice(numbers)/* 同时添加多个元素 */numbers = append(numbers, 2,3,4)printSlice(numbers)/* 创建切片 numbers1 是之前切片的两倍容量*/numbers1 := make([]int, len(numbers), (cap(numbers))*2)/* 拷贝 numbers 的内容到 numbers1 */copy(numbers1,numbers)printSlice(numbers1) }func printSlice(x []int){fmt.Printf("len=%d cap=%d slice=%v\n",len(x),cap(x),x) }
笔记
切片实际的是获取数组的某一部分,len切片<=cap切片<=len数组
切片由三部分组成:指向底层数组的指针、len、cap
package mainimport "fmt"func main() {sliceTest()twoDimensionArray() }func sliceTest() {arr := []int{1, 2, 3, 4, 5}s := arr[:]for e := range s {fmt.Println(s[e])}s1 := make([]int, 3)for e := range s1 {fmt.Println(s1[e])} }func twoDimensionArray() {/* 数组 - 5 行 2 列*/var a = [][]int{{0, 0}, {1, 2}, {2}, {3, 6}, {4, 8}}var i, j int/* 输出数组元素 */for i = 0; i < len(a); i++ {for j = 0; j < len(a[i]); j++ {fmt.Printf("a[%d][%d] = %d\n", i, j, a[i][j])}} }
sliceTest 函数是切片测试代码,简单的两种初始化方式。
twoDimensionsArray 函数是二维数组测试函数。
测试结果:
1.二维数组中的元素(一位数组)个数 > 限制的列数,异常;
2.二维数组中的元素(一位数组)个数 <= 限制的列数,正常;
假设列数为 3, 某个一位数组 {1}, 则后两个元素,默认为 0
合并多个数组
package main import "fmt"func main() {var arr1 = []int{1,2,3}var arr2 = []int{4,5,6}var arr3 = []int{7,8,9}var s1 = append(append(arr1, arr2...), arr3...)fmt.Printf("s1: %v\n", s1) }
在做函数调用时,slice 按引用传递,array 按值传递:
package mainimport "fmt"func main(){changeSliceTest() }func changeSliceTest() {arr1 := []int{1, 2, 3}arr2 := [3]int{1, 2, 3}arr3 := [3]int{1, 2, 3}fmt.Println("before change arr1, ", arr1)changeSlice(arr1) // slice 按引用传递fmt.Println("after change arr1, ", arr1)fmt.Println("before change arr2, ", arr2)changeArray(arr2) // array 按值传递fmt.Println("after change arr2, ", arr2)fmt.Println("before change arr3, ", arr3)changeArrayByPointer(&arr3) // 可以显式取array的 指针fmt.Println("after change arr3, ", arr3) }func changeSlice(arr []int) {arr[0] = 9999 }func changeArray(arr [3]int) {arr[0] = 6666 }func changeArrayByPointer(arr *[3]int) {arr[0] = 6666 }
append() 和 copy() 部分,貌似有没说明白的地方
当 numbers = [0, 1] 时,append(numbers, 2, 3, 4) 为什么 cap 从 2 变成 6 ?
经过实践得知,append(list, [params]),先判断 list 的 cap 长度是否大于等于 len(list) + len([params]),如果大于那么 cap 不变,否则 cap 等于 max{cap(list), cap[params]},所以当 append(numbers, 2, 3, 4) cap 从 2 变成 6
使用 copy 函数要注意对于 copy(dst, src),要初始化 dst 的 size,否则无法复制。
错误示例:
dst := make([]int, 0) src := []int{1, 2, 3} copy(dst, src) printSlice(src) printSlice(dst)func printSlice(x []int){fmt.Printf("len=%d cap=%d slice=%v\n",len(x),cap(x),x) } /* len=3 cap=3 slice=[1 2 3] len=0 cap=0 slice=[] */
正确示例:
dst := make([]int, 3) // 令size=3 src := []int{1, 2, 3} copy(dst, src) printSlice(src) printSlice(dst)/* len=3 cap=3 slice=[1 2 3] len=3 cap=3 slice=[1 2 3] */
实例:
package mainimport "fmt"func main() {var array = []int{1, 2, 3, 4, 5}printSlice(array)slice := array[1:] // slice指向 array 的地址printSlice(slice)array[1] = 100printSlice(slice) }func printSlice(x []int){fmt.Printf("len=%d cap=%d slice=%v\n", len(x), cap(x), x) } /* len=5 cap=5 slice=[1 2 3 4 5] len=4 cap=4 slice=[2 3 4 5] len=4 cap=4 slice=[100 3 4 5] */
切片内部结构:
struct Slice { byte* array; // actual datauintgo len; // number of elementsuintgo cap; // allocated number of elements};
第一个字段表示 array 的指针,是真实数据的指针第二个是表示 slice 的长度,第三个是表示 slice 的容量
所以 unsafe.Sizeof(切片)永远都是 24
当把 slice 作为参数,本身传递的是值,但其内容就 byte* array,实际传递的是引用,所以可以在函数内部修改
若对 slice 做 append,导致 slice 进行了扩容,扩容的切片是复制的,不对原切片进行修改
package mainimport ( "fmt" "unsafe" )func main() {slice_test := []int{1, 2, 3, 4, 5}fmt.Println(unsafe.Sizeof(slice_test))fmt.Printf("main:%#v,%#v,%#v\n", slice_test, len(slice_test), cap(slice_test))slice_value(slice_test)fmt.Printf("main:%#v,%#v,%#v\n", slice_test, len(slice_test), cap(slice_test))slice_ptr(&slice_test)fmt.Printf("main:%#v,%#v,%#v\n", slice_test, len(slice_test), cap(slice_test))fmt.Println(unsafe.Sizeof(slice_test)) }func slice_value(slice_test []int) {slice_test[1] = 100 // 函数外的slice确实有被修改slice_test = append(slice_test, 6) // 函数外的不变fmt.Printf("slice_value:%#v,%#v,%#v\n", slice_test, len(slice_test), cap(slice_test)) }func slice_ptr(slice_test *[]int) { // 这样才能修改函数外的slice*slice_test = append(*slice_test, 7)fmt.Printf("slice_ptr:%#v,%#v,%#v\n", *slice_test, len(*slice_test), cap(*slice_test)) }/* 24 main:[]int{1, 2, 3, 4, 5},5,5 slice_value:[]int{1, 100, 3, 4, 5, 6},6,10 main:[]int{1, 100, 3, 4, 5},5,5 slice_ptr:[]int{1, 100, 3, 4, 5, 7},6,10 main:[]int{1, 100, 3, 4, 5, 7},6,10 24 */
slice 的底层是数组指针,所以 slice a 和 s 指向的是同一个底层数组,所以当修改 s[0] 时,a 也会被修改。
package main import "fmt" func main() {s := []int{1, 2, 3} // len=3, cap=3a := ss[0] = 888s = append(s, 4)fmt.Println(a, len(a), cap(a)) // 输出:[888 2 3] 3 3fmt.Println(s, len(s), cap(s)) // 输出:[888 2 3 4] 4 6 }
关于 cap 为何变为 6 的问题
1、当同时添加多个元素时:
len(list)+len([params]) 为偶数:cap=len(list)+len([params])len(list)+len([params]) 为奇数:cap=len(list)+len([params])+1即 cap 始终为偶数。
2、当一个一个添加元素时:
len(list)+1<=cap: cap=caplen(list)+1>cap: cap=2*cap即 cap 总是呈 2 倍的增加(也是偶数)
关于切片容量 cap,当切片容量不够时,cap 会自动扩容到 2 倍:
package main import "fmt" func main() {var numbers []intprintSlice(numbers)/* 允许追加空切片 */numbers = append(numbers, 0)printSlice(numbers)/* 向切片添加一个元素 */numbers = append(numbers, 1)printSlice(numbers)/* 注意cap容量的变化 */numbers = append(numbers, 2)printSlice(numbers)numbers = append(numbers, 3)printSlice(numbers)numbers = append(numbers, 4) // 可以看出,容量不够时,cap会自动扩容到2倍printSlice(numbers)}func printSlice(x []int){fmt.Printf("len=%d cap=%d slice=%v\n",len(x),cap(x),x)}
当一个一个元素追加到切片中,切片容量变化过程为:
0 -> 1 1 -> 2 2 -> 4 4 -> 8
关于楼上所讨论的 cap 由 2-> 6,根据查阅文档,可以得出一个结论:通过 append() 函数向数组中添加元素,首先 cap 会以二倍的速度增长,如果发现增长 2 倍以上的容量可以满足扩容后的需求,那么 cap*2,否则就会看扩容后数组的 length 是多少 cap=length+1。
以 s := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9} 为例:
建议:做 slice 截取时建议用两个参数,尤其是从底层数组进行切片操作时,因为这样在进行第一次 append 操作时,会给切片重新分配空间,这样减少切片对数组的影响。
结论:s = s[low : high : max] 切片的三个参数的切片截取的意义为 low 为截取的起始下标(含), high 为窃取的结束下标(不含 high),max 为切片保留的原切片的最大下标(不含 max);即新切片从老切片的 low 下标元素开始,len = high - low, cap = max - low;high 和 max 一旦超出在老切片中越界,就会发生 runtime err,slice out of range。另外如果省略第三个参数的时候,第三个参数默认和第二个参数相同,即 len = cap。
package mainimport "fmt"func main(){s := []int {0, 1, 2, 3, 4, 5, 6,7, 8, 9}s = s[1:9:10]fmt.Println(s)fmt.Println(len(s))fmt.Println(cap(s)) } /* [1 2 3 4 5 6 7 8] 8 9 */
修改 max 值,发生越界错误:
package mainimport "fmt"func main(){s := []int {0, 1, 2, 3, 4, 5, 6,7, 8, 9}s = s[1:9:13] // 修改 max 值为 13fmt.Println(s)fmt.Println(len(s))fmt.Println(cap(s)) }
执行后,错误信息如下:
panic: runtime error: slice bounds out of rangegoroutine 1 [running]:
append 会让切片和与他相关的切片脱钩,但地址不变:
package mainimport ("fmt" )func main() {a := []int{1, 2, 3, 4}b := aprintSlice(a, "part1 a")printSlice(b, "part1 b")fmt.Printf("\n")a[0] = 9printSlice(a, "part2 a")printSlice(b, "part2 b")fmt.Printf("\n")a = append(a, 5)a[0] = 1printSlice(a, "part3 a")printSlice(b, "part3 b")fmt.Printf("\n")c := ad := &aprintSlice(a, "part4 a")printSlice(c, "part4 c")printSlice(c, "part4 d")fmt.Printf("\n")a = append(a, 6)printSlice(a, "part5 a")printSlice(c, "part5 c")printSlice(*d, "part5 *d")} func printSlice(x []int, y string) {fmt.Printf("%v len=%d cap=%d slice=%v\n", y, len(x), cap(x), x) } /* part1 a len=4 cap=4 slice=[1 2 3 4] part1 b len=4 cap=4 slice=[1 2 3 4]part2 a len=4 cap=4 slice=[9 2 3 4] part2 b len=4 cap=4 slice=[9 2 3 4]part3 a len=5 cap=8 slice=[1 2 3 4 5] part3 b len=4 cap=4 slice=[9 2 3 4]part4 a len=5 cap=8 slice=[1 2 3 4 5] part4 c len=5 cap=8 slice=[1 2 3 4 5] part4 d len=5 cap=8 slice=[1 2 3 4 5]part5 a len=6 cap=8 slice=[1 2 3 4 5 6] part5 c len=5 cap=8 slice=[1 2 3 4 5] part5 *d len=6 cap=8 slice=[1 2 3 4 5 6] */
len=5 cap=6 slice=[0 1 2 3 4]
为什么这里的 cap 不是 5 呢?
要弄清楚这个问题,先要理解如下代码:
// 每次cap改变,指向array的ptr就会变化一次 s := make([]int, 1) fmt.Printf("len:%d cap: %d array ptr: %v \n", len(s), cap(s), *(*unsafe.Pointer)(unsafe.Pointer(&s)))/* unsafe.Pointer: unsafe.Pointer 是一个特殊的指针类型,它可以转换为任何类型的指针。这是 unsafe 包提供的主要功能之一 */for i := 0; i < 5; i++ {s = append(s, i)fmt.Printf("len:%d cap: %d array ptr: %v \n", len(s), cap(s), *(*unsafe.Pointer)(unsafe.Pointer(&s))) } fmt.Println("Array:", s)
以上代码执行输出结果为:
len:1 cap: 1 array ptr: 0xc8200640f0 len:2 cap: 2 array ptr: 0xc820064110 len:3 cap: 4 array ptr: 0xc8200680c0 len:4 cap: 4 array ptr: 0xc8200680c0 len:5 cap: 8 array ptr: 0xc82006c080 len:6 cap: 8 array ptr: 0xc82006c080 Array: [0 0 1 2 3 4]
解释:每次cap改变的时候指向array内存的指针都在变化。当在使用 append 的时候,如果 cap==len 了这个时候就会新开辟一块更大内存,然后把之前的数据复制过去(实际go在append的时候放大cap是有规律的。在 cap 小于1024的情况下是每次扩大到 2 * cap ,当大于1024之后就每次扩大到 1.25 * cap 。所以上面的测试中cap变化是 1, 2, 4, 8)。
可以通过查看$GOROOT/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}} }
从上面的代码可以看出以下内容:
-
首先判断,如果新申请容量(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
类型的处理方式就不一样。
楼上对于扩容后的容量说明都在发生扩容的代码逻辑中,没有说明这段代码中的参数 cap(申请容量) 是怎么计算的 。
经过多次的测试,我这边猜测是在 append 发生扩容时,申请容量是 原切片容量 + 追加的长度,如果是单数则 +1,再将这个容量传入扩容的方法进行判断,以讨论的 cap 变为6的代码为例:
原切片为 []int{1, 2} ,长度为2(记为 len1 = 2),容量为2(记为 cap1 =2)。
追加的长度为 {3, 4, 5},长度为3(记为 len2= 3),此时进行扩容,申请容量为6(记为 cap2 = cap1+len2,cap2 为单数,则+1,cap2 = 6),将 cap2 带入楼上的扩容判断逻辑。
申请容量 cap2 大于2倍的旧容量(cap1 = 2),则newcap = cap2 = 6
部分测试打印如下:
len=0 cap=0 slice=[] len=2 cap=2 slice=[0 1] // len1 = 2,cap1 = 2 len=5 cap=6 slice=[0 1 2 3 4] // len2 = 3,cap2 = cap1+len2 = 5,单数+1,cap2 = 6 len=0 cap=0 slice=[] len=2 cap=2 slice=[0 1] // len1 = 2,cap1 = 2 len=6 cap=6 slice=[0 1 2 3 4 5] // len2 = 3,cap2 = cap1+len2 = 6
关于 append 会让切片与其他相关切片脱钩的问题:
package main import "fmt"func main() {x := make([]int, 4)a := x[:2]a[0] = 1 fmt.Println(a)fmt.Println(x)a = append(a, 2)a[0] = 0 fmt.Println(a)fmt.Println(x)fmt.Printf("切片a的地址:%p\n", a)fmt.Printf("切片x的地址:%p\n", x)fmt.Println()y := make([]int, 4)b := yb[0] = 1 fmt.Println(b)fmt.Println(y)b = append(b, 2)b[0] = 0 fmt.Println(b)fmt.Println(y)fmt.Printf("切片b的地址:%p\n", b)fmt.Printf("切片y的地址:%p\n", y)fmt.Println() }
输出结果:
[1 0] [1 0 0 0] [0 0 2] [0 0 2 0] 切片a的地址:0x14000130000 切片x的地址:0x14000130000[1 0 0 0] [1 0 0 0] [0 0 0 0 2] [1 0 0 0] 切片b的地址:0x1400012e080 切片y的地址:0x14000130040
猜测脱钩的情况是由于切片底层数组扩张(创建了新数组替换旧数组)导致。
@da蘑菇大
提供了一个很好的例子,但是这个例子只在 cap(b)≤len(a) 的情况下成立;但是在 cap(b)>len(a) 时,append 并不能使切片脱钩。
先看看@da蘑菇大原始代码:
package mainimport ("fmt" )func main() {a := []int{1, 2, 3, 4}b := aprintSlice(a, "part1 a")printSlice(b, "part1 b")fmt.Printf("\n")a[0] = 9printSlice(a, "part2 a")printSlice(b, "part2 b")fmt.Printf("\n")a = append(a, 5)a[0] = 1printSlice(a, "part3 a")printSlice(b, "part3 b")fmt.Printf("\n")c := ad := &aprintSlice(a, "part4 a")printSlice(c, "part4 c")printSlice(*d, "part4 d") //这是更改后的,原始代码:printSlice(c, "part4 d") fmt.Printf("\n")a = append(a, 6)printSlice(a, "part5 a")printSlice(c, "part5 c")printSlice(*d, "part5 *d")} func printSlice(x []int, y string) {fmt.Printf("%v len=%d cap=%d slice=%v\n", y, len(x), cap(x), x) }
运行结果为:
part1 a len=4 cap=4 slice=[1 2 3 4] part1 b len=4 cap=4 slice=[1 2 3 4]part2 a len=4 cap=4 slice=[9 2 3 4] part2 b len=4 cap=4 slice=[9 2 3 4]part3 a len=5 cap=8 slice=[1 2 3 4 5] part3 b len=4 cap=4 slice=[9 2 3 4]part4 a len=5 cap=8 slice=[1 2 3 4 5] part4 c len=5 cap=8 slice=[1 2 3 4 5] part4 d len=5 cap=8 slice=[1 2 3 4 5]part5 a len=6 cap=8 slice=[1 2 3 4 5 6] part5 c len=5 cap=8 slice=[1 2 3 4 5] part5 *d len=6 cap=8 slice=[1 2 3 4 5 6]
在 part3 中,通过 a[0]=1 修改了 a[0] 的值,但是 b[0] 的值并没有改变;通过 a=append(a,5) 增加了一个数据,b 切片没有增加数据;这只是在 b 的 cap 比较小的情况下才会出现的情况;如果 b 的 cap 足够大呢?
将代码修改成:
package mainimport ("fmt" )func main() {a := make([]int, 4, 10)printSlice(a, "part1 a")a[0] = 1a[1] = 2a[2] = 3a[3] = 4b := aprintSlice(a, "part1 a")printSlice(b, "part1 b")fmt.Printf("\n")a[0] = 99printSlice(a, "part2 a")printSlice(b, "part2 b")fmt.Printf("\n")a = append(a, 5)a[0] = 100printSlice(a, "part3 a")printSlice(b, "part3 b")fmt.Printf("\n")c := ad := &aprintSlice(a, "part4 a")printSlice(c, "part4 c")printSlice(*d, "part4 d")fmt.Printf("\n")a = append(a, 6)printSlice(a, "part5 a")printSlice(c, "part5 c")printSlice(*d, "part5 *d")} func printSlice(x []int, y string) {fmt.Printf("%v len=%d cap=%d slice=%v\n", y, len(x), cap(x), x) }
运行结果:
part1 a len=4 cap=10 slice=[0 0 0 0] part1 a len=4 cap=10 slice=[1 2 3 4] part1 b len=4 cap=10 slice=[1 2 3 4] part2 a len=4 cap=10 slice=[99 2 3 4] part2 b len=4 cap=10 slice=[99 2 3 4]part3 a len=5 cap=10 slice=[100 2 3 4 5] part3 b len=4 cap=10 slice=[100 2 3 4]part4 a len=5 cap=10 slice=[100 2 3 4 5] part4 c len=5 cap=10 slice=[100 2 3 4 5] part4 d len=5 cap=10 slice=[100 2 3 4 5]part5 a len=6 cap=10 slice=[100 2 3 4 5 6] part5 c len=5 cap=10 slice=[100 2 3 4 5] part5 *d len=6 cap=10 slice=[100 2 3 4 5 6]
修改后的代码中,将 a, b 的 cap 都设置成 10;
在 part3 部分,通过 a[0] 修改 a[0] 值,b[0] 值也会跟着修改;通过 a=append(a, 5) 可以给 a 增加数据项,但是 b 的数据项并没有增加;
通过上面的试验可以发现,通过 b:=a 引用的方式,当 cap(b) 小于 len(a),修改 a[i] 的值并不会改变 b[i] 的值;但是如果 cap(b)≥len(a) 时,修改 a(i) 的值就会改变 b(i) 的值;
要想使两个切片同步改变,最好的方式是使用切片指针来实现,也就是上面的 *d。
*17_语言范围(Range)
Go 语言中 range 关键字用于 for 循环中迭代数组(array)、切片(slice)、通道(channel)或集合(map)的元素。在数组和切片中它返回元素的索引和索引对应的值,在集合中返回 key-value 对。
for 循环的 range 格式可以对 slice、map、数组、字符串等进行迭代循环。格式如下:
for key, value := range oldMap {newMap[key] = value }
以上代码中的 key 和 value 是可以省略。
如果只想读取 key,格式如下:
for key := range oldMap
或者这样:
for key, _ := range oldMap
如果只想读取 value,格式如下:
for _, value := range oldMap
遍历简单的数组,2**%d 的结果为 2 对应的次方数:
package mainimport "fmt"var pow = []int{1, 2, 4, 8, 16, 32, 64, 128}func main() {for i, v := range pow {fmt.Printf("2**%d = %d\n", i, v)} }
for 循环的 range 格式可以省略 key 和 value,如下实例:
package main import "fmt"func main() {map1 := make(map[int]float32)map1[1] = 1.0map1[2] = 2.0map1[3] = 3.0map1[4] = 4.0// 读取 key 和 valuefor key, value := range map1 {fmt.Printf("key is: %d - value is: %f\n", key, value)}// 读取 keyfor key := range map1 {fmt.Printf("key is: %d\n", key)}// 读取 valuefor _, value := range map1 {fmt.Printf("value is: %f\n", value)} }
range 遍历其他数据结构:
package main import "fmt" func main() {//这是我们使用 range 去求一个 slice 的和。使用数组跟这个很类似nums := []int{2, 3, 4}sum := 0for _, num := range nums {sum += num}fmt.Println("sum:", sum)//在数组上使用 range 将传入索引和值两个变量。上面那个例子我们不需要使用该元素的序号,所以我们使用空白符"_"省略了。有时侯我们确实需要知道它的索引。for i, num := range nums {if num == 3 {fmt.Println("index:", i)}}//range 也可以用在 map 的键值对上。kvs := map[string]string{"a": "apple", "b": "banana"}for k, v := range kvs {fmt.Printf("%s -> %s\n", k, v)}//range也可以用来枚举 Unicode 字符串。第一个参数是字符的索引,第二个是字符(Unicode的值)本身。for i, c := range "go" {fmt.Println(i, c)} }
笔记
Go Range 简单循环:
package mainimport "fmt"func main(){nums := []int{1,2,3,4};length := 0;for range nums { length++;}fmt.Println( length); }
循环键值对
package main import "fmt" func main(){nums := []int{1,2,3,4}for i,num := range nums {fmt.Printf("索引是%d,长度是%d\n",i, num)} } /* 索引是0,长度是1 索引是1,长度是2 索引是2,长度是3 索引是3,长度是4 */
通过 range 获取参数列表:
package main // 声明这个Go源文件属于main包,它是程序的入口点。 import ( "fmt" // 导入fmt包,用于格式化输出。 "os" // 导入os包,它包含了与操作系统交互的功能,包括命令行参数。 ) func main() { // main函数是Go程序的入口点。 fmt.Println(len(os.Args)) // 打印os.Args切片的长度,即命令行参数的数量。 // 使用for循环遍历os.Args切片,并打印每个参数。 for _, arg := range os.Args { fmt.Println(arg) } }
Go 中的中文采用 UTF-8 编码,因此逐个遍历字符时必须采用 for-each 形式:
package mainimport "fmt"func main() { printStr("hello") fmt.Println() fmt.Println() printStr("中国人") }func printStr(s string) { fmt.Println("str: " + s) for _, v := range s { fmt.Printf("0x%x %c, ", v, v) } fmt.Println() for i := 0; i < len(s); i++ { fmt.Printf("0x%x, ", s[i]) } } /* str: hello 0x68 h, 0x65 e, 0x6c l, 0x6c l, 0x6f o, 0x68, 0x65, 0x6c, 0x6c, 0x6f, str: 中国人 0x4e2d 中, 0x56fd 国, 0x4eba 人, 0xe4, 0xb8, 0xad, 0xe5, 0x9b, 0xbd, 0xe4, 0xba, 0xba, */
涉及指针时需要注意,v 是个单独的地址。
func main() { nums := [3]int{5, 6, 7}for k , v := range nums {fmt.Println("源值地址:",&nums[k]," \t value的地址:",&v)} } /* 源值地址: 0xc000012138 value的地址: 0xc00000e0a8 源值地址: 0xc000012140 value的地址: 0xc00000e0a8 源值地址: 0xc000012148 value的地址: 0xc00000e0a8 */
18_Map(集合)
Map 是一种无序的键值对的集合
Map 最重要的一点是通过 key 来快速检索数据,key 类似于索引,指向数据的值
Map 是一种集合,所以我们可以像迭代数组和切片那样迭代它。不过,Map 是无序的,遍历 Map 时返回的键值对的顺序是不确定的
在获取 Map 的值时,如果键不存在,返回该类型的零值,例如 int 类型的零值是 0,string 类型的零值是 ""
Map 是引用类型,如果将一个 Map 传递给一个函数或赋值给另一个变量,它们都指向同一个底层数据结构,因此对 Map 的修改会影响到所有引用它的变量
定义 Map
可以使用内建函数 make 或使用 map 关键字来定义 Map:
/* 使用 make 函数 */ map_variable := make(map[KeyType]ValueType, initialCapacity)
其中 KeyType 是键的类型,ValueType 是值的类型,initialCapacity 是可选的参数,用于指定 Map 的初始容量。Map 的容量是指 Map 中可以保存的键值对的数量,当 Map 中的键值对数量达到容量时,Map 会自动扩容。如果不指定 initialCapacity,Go 语言会根据实际情况选择一个合适的值
// 创建一个空的 Map m := make(map[string]int)// 创建一个初始容量为 10 的 Map m := make(map[string]int, 10)
也可以使用字面量创建 Map:
// 使用字面量创建 Map m := map[string]int{"apple": 1,"banana": 2,"orange": 3, }
获取元素:
// 获取键值对 v1 := m["apple"] v2, ok := m["pear"] // 如果键不存在,ok 的值为 false,v2 的值为该类型的零值
修改元素:
// 修改键值对 m["apple"] = 5
获取 Map 的长度:
// 获取 Map 的长度 len := len(m)
遍历 Map:
// 遍历 Map for k, v := range m {fmt.Printf("key=%s, value=%d\n", k, v) }
删除元素:
// 删除键值对 delete(m, "banana")
下面实例演示了创建和使用map:
package mainimport "fmt"func main() {var siteMap map[string]string /*创建集合 */siteMap = make(map[string]string)/* map 插入 key - value 对,各个国家对应的首都 */siteMap [ "Google" ] = "谷歌"siteMap [ "Runoob" ] = "菜鸟教程"siteMap [ "Baidu" ] = "百度"siteMap [ "Wiki" ] = "维基百科"/*使用键输出地图值 */ for site := range siteMap {fmt.Println(site, "首都是", siteMap [site])}/*查看元素在集合中是否存在 */name, ok := siteMap [ "Facebook" ] /*如果确定是真实的,则存在,否则不存在 *//*fmt.Println(capital) *//*fmt.Println(ok) */if (ok) {fmt.Println("Facebook 的 站点是", name)} else {fmt.Println("Facebook 站点不存在")} } /* Wiki 首都是 维基百科 Google 首都是 谷歌 Runoob 首都是 菜鸟教程 Baidu 首都是 百度 Facebook 站点不存在 */
*delete() 函数
delete() 函数用于删除集合的元素, 参数为 map 和其对应的 key。实例如下:
package mainimport "fmt"func main() {/* 创建map */countryCapitalMap := map[string]string{"France": "Paris", "Italy": "Rome", "Japan": "Tokyo", "India": "New delhi"}fmt.Println("原始地图")/* 打印地图 */for country := range countryCapitalMap {fmt.Println(country, "首都是", countryCapitalMap [ country ])}/*删除元素*/ delete(countryCapitalMap, "France")fmt.Println("法国条目被删除")fmt.Println("删除元素后地图")/*打印地图*/for country := range countryCapitalMap {fmt.Println(country, "首都是", countryCapitalMap [ country ])} } /* 原始地图 India 首都是 New delhi France 首都是 Paris Italy 首都是 Rome Japan 首都是 Tokyo 法国条目被删除 删除元素后地图 Italy 首都是 Rome Japan 首都是 Tokyo India 首都是 New delhi */
笔记
基于 go 实现简单 HashMap,暂未做 key 值的校验
package mainimport ("fmt" )type HashMap struct {key stringvalue stringhashCode intnext *HashMap }var table [16](*HashMap)func initTable() {for i := range table{table[i] = &HashMap{"","",i,nil}} }func getInstance() [16](*HashMap){if(table[0] == nil){initTable()}return table }func genHashCode(k string) int{// 生成哈希码if len(k) == 0{return 0}var hashCode int = 0var lastIndex int = len(k) - 1for i := range k {if i == lastIndex {hashCode += int(k[i])break}hashCode += (hashCode + int(k[i]))*31}return hashCode }func indexTable(hashCode int) int{// 计算桶索引和节点索引return hashCode%16 }func indexNode(hashCode int) int {// 计算桶索引和节点索引return hashCode>>4 }func put(k string, v string) string {// 插入操作var hashCode = genHashCode(k)var thisNode = HashMap{k,v,hashCode,nil}var tableIndex = indexTable(hashCode)var nodeIndex = indexNode(hashCode)var headPtr [16](*HashMap) = getInstance()var headNode = headPtr[tableIndex]if (*headNode).key == "" {*headNode = thisNodereturn ""}var lastNode *HashMap = headNodevar nextNode *HashMap = (*headNode).nextfor nextNode != nil && (indexNode((*nextNode).hashCode) < nodeIndex){lastNode = nextNodenextNode = (*nextNode).next}if (*lastNode).hashCode == thisNode.hashCode {var oldValue string = lastNode.valuelastNode.value = thisNode.valuereturn oldValue}if lastNode.hashCode < thisNode.hashCode {lastNode.next = &thisNode}if nextNode != nil {thisNode.next = nextNode}return "" }func get(k string) string {// 获取操作var hashCode = genHashCode(k)var tableIndex = indexTable(hashCode)var headPtr [16](*HashMap) = getInstance()var node *HashMap = headPtr[tableIndex]if (*node).key == k{return (*node).value}for (*node).next != nil {if k == (*node).key {return (*node).value}node = (*node).next}return "" }//examples func main() {getInstance()put("a","a_put")put("b","b_put")fmt.Println(get("a"))fmt.Println(get("b"))put("p","p_put")fmt.Println(get("p")) }
*19_递归函数
递归,就是在运行的过程中调用自己
语法格式如下:
func recursion() {recursion() /* 函数调用自身 */ }func main() {recursion() }
Go 语言支持递归。但我们在使用递归时,开发者需要设置退出条件,否则递归将陷入无限循环中
递归函数对于解决数学上的问题是非常有用的,就像计算阶乘,生成斐波那契数列等
阶乘
以下实例通过 Go 语言的递归函数实例阶乘:
package mainimport "fmt"func Factorial(n uint64)(result uint64) {if (n > 0) {result = n * Factorial(n-1)return result}return 1 }func main() { var i int = 15fmt.Printf("%d 的阶乘是 %d\n", i, Factorial(uint64(i))) } // 15 的阶乘是 1307674368000
斐波那契数列
以下实例通过 Go 语言的递归函数实现斐波那契数列:
package mainimport "fmt"func fibonacci(n int) int {if n < 2 {return n}return fibonacci(n-2) + fibonacci(n-1) }func main() {var i intfor i = 0; i < 10; i++ {fmt.Printf("%d\t", fibonacci(i))} } // 0 1 1 2 3 5 8 13 21 34
求平方根
以下实例通过 Go 语言使用递归方法实现求平方根的代码:
package mainimport ("fmt" )func sqrtRecursive(x, guess, prevGuess, epsilon float64) float64 {/* x表示待求平方根的数 guess表示当前猜测的平方根值 prevGuess 表示上一次的猜测值 epsilon 表示精度要求(即接近平方根的程度) */if diff := guess*guess - x; diff < epsilon && -diff < epsilon {return guess}newGuess := (guess + x/guess) / 2if newGuess == prevGuess {return guess}return sqrtRecursive(x, newGuess, guess, epsilon) }func sqrt(x float64) float64 {return sqrtRecursive(x, 1.0, 0.0, 1e-9) }func main() {x := 25.0result := sqrt(x)fmt.Printf("%.2f 的平方根为 %.6f\n", x, result) }
递归的终止条件是当前猜测的平方根与上一次猜测的平方根非常接近,差值小于给定的精度 epsilon
在 sqrt 函数中,我们调用 sqrtRecursive 来计算平方根,并传入初始值和精度要求,然后在 main 函数中,我们调用 sqrt 函数来求解平方根,并将结果打印出来
笔记
斐波纳契数列以如下被以递归的方法定义:F0=0,F1=1,Fn=F(n-1)+F(n-2)(n>=2,n∈N*)
在这里的:
return fibonacci(n-2) + fibonacci(n-1)
是指的 n-2 是 n 前面第二项的值,而不是 n-2=x 的值,那么 n-1 也与此同理
更好的一种 fibonacci 实现,用到多返回值特性,降低复杂度:
func fibonacci2(n int) (int,int) {if n < 2 {return 0,n}a,b := fibonacci2(n-1)return b,a+b }func fibonacci(n int) int {a,b := fibonacci2(n)return b }
求平方根
原理: 计算机通常使用循环来计算 x 的平方根。从某个猜测的值 z 开始,我们可以根据 z² 与 x 的近似度来调整 z,产生一个更好的猜测:
z -= (z*z - x) / (2*z)
重复调整的过程,猜测的结果会越来越精确,得到的答案也会尽可能接近实际的平方根。
package main import "fmt"func sqrt(x float64,i float64) (float64,float64){remain:=(i*i-x)/(2*i);i=i-remainif(remain>0){return sqrt(x,i);}else{ return i,remain } } func get_sqrt(x float64) float64{ i,_ :=sqrt(x,x); return i; } func main(){ fmt.Println(get_sqrt(2))fmt.Println(get_sqrt(3)) }
求平方根算法复杂度改成 O(1):
package main import "fmt" import "unsafe"func get_sqrt(x float32) float32{ xhalf := 0.5*x;var i int32 = *(*int32)(unsafe.Pointer(&x));i = 0x5f375a86 - (i>>1);x = *(*float32)(unsafe.Pointer(&i));x = x * (1.5 - xhalf*x*x);x = x * (1.5 - xhalf*x*x);x = x * (1.5 - xhalf*x*x);return 1/x; }func main(){ fmt.Println(get_sqrt(4)) }
*20_类型转换
<!-- 类型(表达式) -->
数值类型转换
将整型转换为浮点型:
var a int = 10 var b float64 = float64(a)
以下实例中将整型转化为浮点型,并计算结果,将结果赋值给浮点型变量:
package mainimport "fmt"func main() {var sum int = 17var count int = 5var mean float32mean = float32(sum)/float32(count)fmt.Printf("mean 的值为: %f\n",mean) } // mean 的值为: 3.400000
字符串类型转换
注意,strconv.Atoi
函数返回两个值,第一个是转换后的整型值,第二个是可能发生的错误,我们可以使用空白标识符 _ 来忽略这个错误
字符串转化为整数
package mainimport ("fmt""strconv" )func main() {str := "123"num, err := strconv.Atoi(str) // 字符串转化为整数if err != nil {fmt.Println("转换错误:", err)} else {fmt.Printf("字符串 '%s' 转换为整数为:%d\n", str, num)} } // 字符串 '123' 转换为整数为:123
整数转换为字符串:
package mainimport ("fmt""strconv" )func main() {num := 123str := strconv.Itoa(num) // 整数转换字符串fmt.Printf("整数 %d 转换为字符串为:'%s'\n", num, str) } // 整数 123 转换为字符串为:'123'
字符串转换为浮点数:
package mainimport ("fmt""strconv" )func main() {str := "3.14"num, err := strconv.ParseFloat(str, 64) // 字符串转换为浮点数if err != nil {fmt.Println("转换错误:", err)} else {fmt.Printf("字符串 '%s' 转为浮点型为:%f\n", str, num)} } // 字符串 '3.14' 转为浮点型为:3.140000
浮点数转换为字符串
package mainimport ("fmt""strconv" )func main() {num := 3.14str := strconv.FormatFloat(num, 'f', 2, 64) // 浮点数转换为字符串fmt.Printf("浮点数 %f 转为字符串为:'%s'\n", num, str) } // 浮点数 3.140000 转为字符串为:'3.14'
接口类型转换
接口类型转换有两种情况:类型断言和类型转换
类型断言用于将接口类型转换为指定类型,其语法为:
接口类型的变量.(要转换成的类型)
如果类型断言成功,它将返回转换后的值和一个布尔值,表示转换是否成功
package mainimport "fmt"func main() {var i interface{} = "Hello, World"str, ok := i.(string)if ok {fmt.Printf("'%s' is a string\n", str)} else {fmt.Println("conversion failed")} }
以上实例中,我们定义了一个接口类型变量 i,并将它赋值为字符串 "Hello, World"
然后,我们使用类型断言将 i 转换为字符串类型,并将转换后的值赋值给变量 str。最后,我们使用 ok 变量检查类型转换是否成功,如果成功,我们打印转换后的字符串;否则,我们打印转换失败的消息
类型转换用于将一个接口类型的值转换为另一个接口类型,其语法为:
目标接口类型(要转换的值)
在类型转换中,我们必须保证要转换的值和目标接口类型之间是兼容的,否则编译器会报错
package mainimport "fmt"type Writer interface {Write([]byte) (int, error) }type StringWriter struct {str string }func (sw *StringWriter) Write(data []byte) (int, error) {sw.str += string(data)return len(data), nil }func main() {var w Writer = &StringWriter{}sw := w.(*StringWriter)sw.str = "Hello, World"fmt.Println(sw.str) } /* 定义了一个 Writer 接口和一个实现了该接口的结构体 StringWriter 然后,将 StringWriter 类型的指针赋值给 Writer 接口类型的变量 w 接着,使用类型转换将 w 转换为 StringWriter 类型,并将转换后的值赋值给变量 sw 最后,使用 sw 访问 StringWriter 结构体中的字段 str,并打印出它的值 */
笔记
go 不支持隐式转换类型,比如 :
package main import "fmt"func main() { var a int64 = 3var b int32b = afmt.Printf("b 为 : %d", b) } // 会报错
package main import "fmt"func main() { var a int64 = 3var b int32b = int32(a)fmt.Printf("b 为 : %d", b) } // 不会报错
怎么能缺少了string转int、int转string呢~
package main import ("fmt""strconv" )func main() {// string to intaStr := "100"bInt, err := strconv.Atoi(aStr)if err == nil {fmt.Printf("aStr:%T %s,bInt:%T %d", aStr, aStr, bInt, bInt)} else {fmt.Printf("err:%s", err)}// int to stringcInt := 200dStr := strconv.Itoa(cInt)fmt.Printf("cInt:%T %d,dStr:%T %s", cInt, cInt, dStr, dStr) }
*21_接口
接口把所有的具有共性的方法定义在一起,任何其他类型只要实现了接口内的所有方法就是实现了这个接口
<!-- 任何其他类型只要实现了接口内所有的方法就是实现了这个接口 -->
接口可以让我们将不同的类型绑定到一组公共的方法上,从而实现多态和灵活的设计
Go 语言中的接口是隐式实现的,也就是说,如果一个类型实现了一个接口定义的所有方法,那么它就自动地实现了该接口。因此,我们可以通过将接口作为参数来实现对不同类型的调用,从而实现多态
/* 定义接口 */ type interface_name interface {method_name1 [return_type]method_name2 [return_type]method_name3 [return_type]...method_namen [return_type] }/* 定义结构体 */ type struct_name struct {/* variables */ }/* 实现接口方法 */ func (struct_name_variable struct_name) method_name1() [return_type] {/* 方法实现 */ } ... func (struct_name_variable struct_name) method_namen() [return_type] {/* 方法实现*/ }
package mainimport ("fmt" )type Phone interface {call() }type NokiaPhone struct { }func (nokiaPhone NokiaPhone) call() {fmt.Println("I am Nokia, I can call you!") }type IPhone struct { }func (iPhone IPhone) call() {fmt.Println("I am iPhone, I can call you!") }func main() {var phone Phonephone = new(NokiaPhone)phone.call()phone = new(IPhone)phone.call() } /* I am Nokia, I can call you! I am iPhone, I can call you! */
在上面的例子中,我们定义了一个接口 Phone,接口里面有一个方法 call() 然后我们在 main 函数里面定义了一个 Phone 类型变量,并分别为之赋值为 NokiaPhone 和 IPhone 然后调用 call() 方法
package main import "fmt" // 定义一个接口 type Speaker interface { Speak() string } // 定义一个结构体,实现了Speaker接口 type Dog struct { Name string } func (d Dog) Speak() string { return d.Name + " says: Woof!" } // 定义一个函数,接受一个Speaker接口类型的参数 func LetItSpeak(s Speaker) { fmt.Println(s.Speak()) } func main() { // 初始化Dog结构体 dog := Dog{Name: "Buddy"} // 将Dog实例赋值给Speaker接口变量 // 注意:这里并没有“初始化接口”,只是将实现了接口的类型实例赋值给接口变量 var speaker Speaker = dog // 调用接口方法 LetItSpeak(speaker) // 输出: Buddy says: Woof! // 或者可以直接将实现了接口的类型实例作为参数传递给接受接口类型的函数 LetItSpeak(Dog{Name: "Rover"}) // 输出: Rover says: Woof! }
package mainimport "fmt"type Shape interface {area() float64 }type Rectangle struct {width float64height float64 }func (r Rectangle) area() float64 {return r.width * r.height }type Circle struct {radius float64 }func (c Circle) area() float64 {return 3.14 * c.radius * c.radius }func main() {var s Shapes = Rectangle{width: 10, height: 5}fmt.Printf("矩形面积: %f\n", s.area())s = Circle{radius: 3}fmt.Printf("圆形面积: %f\n", s.area()) } /* 矩形面积: 50.000000 圆形面积: 28.260000 */
以上实例中,我们定义了一个 Shape 接口,它定义了一个方法 area(),该方法返回一个 float64 类型的面积值。然后,我们定义了两个结构体 Rectangle 和 Circle,它们分别实现了 Shape 接口的 area() 方法。在 main() 函数中,我们首先定义了一个 Shape 类型的变量 s,然后分别将 Rectangle 和 Circle 类型的实例赋值给它,并通过 area() 方法计算它们的面积并打印出来
需要注意的是,接口类型变量可以存储任何实现了该接口的类型的值。在示例中,我们将 Rectangle 和 Circle 类型的实例都赋值给了 Shape 类型的变量 s,并通过 area() 方法调用它们的面积计算方法
笔记
给接口增加参数:
package main import ("fmt" ) type Man interface {name() string;age() int; }type Woman struct { }func (woman Woman) name() string {return "Jin Yawei" } func (woman Woman) age() int {return 23; }type Men struct { }func ( men Men) name() string {return "liweibin"; } func ( men Men) age() int {return 27; }func main(){var man Man;man = new(Woman);fmt.Println( man.name());fmt.Println( man.age());man = new(Men);fmt.Println( man.name());fmt.Println( man.age()); }
func (name string) imp() string{print("这是实现方法的写法") }func sum(x int,y int) int{print("这是正常写法") }
接口方法传参,以及返回结果:
package mainimport "fmt"type Phone interface {call(param int) stringtakephoto() }type Huawei struct { }func (huawei Huawei) call(param int) string{fmt.Println("i am Huawei, i can call you!", param)return "damon" }func (huawei Huawei) takephoto() {fmt.Println("i can take a photo for you") }func main(){var phone Phonephone = new(Huawei)phone.takephoto()r := phone.call(50)fmt.Println(r) }
接口案例:
package main import ("fmt" )//定义接口 type Phone interface {call()call2() }//一直都搞不懂这是干啥的 //原来是用来定义结构体内的数据类型的type Phone1 struct {id intname stringcategory_id intcategory_name string }//第一个类的第一个回调函数 func (test Phone1) call() {fmt.Println("这是第一个类的第一个接口回调函数 结构体数据:", Phone1{id: 1, name: "浅笑"}) }//第一个类的第二个回调函数 func (test Phone1) call2() {fmt.Println("这是一个类的第二个接口回调函数call2", Phone1{id: 1, name: "浅笑", category_id: 4, category_name: "分类名称"}) }//第二个结构体的数据类型 type Phone2 struct {member_id intmember_balance float32member_sex boolmember_nickname string }//第二个类的第一个回调函数 func (test2 Phone2) call() {fmt.Println("这是第二个类的第一个接口回调函数call", Phone2{member_id: 22, member_balance: 15.23, member_sex: false, member_nickname: "浅笑18"}) }//第二个类的第二个回调函数 func (test2 Phone2) call2() {fmt.Println("这是第二个类的第二个接口回调函数call2", Phone2{member_id: 44, member_balance: 100, member_sex: true, member_nickname: "陈超"}) }//开始运行 func main() {var phone Phone//先实例化第一个接口phone = new(Phone1)phone.call()phone.call2()//实例化第二个接口phone = new(Phone2)phone.call()phone.call2() }
将接口做为参数
package mainimport ("fmt" )type Phone interface {call() string }type Android struct {brand string }type IPhone struct {version string }func (android Android) call() string {return "I am Android " + android.brand }func (iPhone IPhone) call() string {return "I am iPhone " + iPhone.version }func printCall(p Phone) {fmt.Println(p.call() + ", I can call you!") }func main() {var vivo = Android{brand:"Vivo"}var hw = Android{"HuaWei"}i7 := IPhone{"7 Plus"}ix := IPhone{"X"}printCall(vivo)printCall(hw)printCall(i7)printCall(ix) } /* I am Android Vivo, I can call you! I am Android HuaWei, I can call you! I am iPhone 7 Plus, I can call you! I am iPhone X, I can call you! */
如果想要通过接口方法修改属性,需要在传入指针的结构体才行,具体代码入下的 1 处:
type fruit interface{getName() string setName(name string) } type apple struct{ name string } //[1] func (a *apple) getName() string{ return a.name } //[2] func (a *apple) setName(name string) {a.name = name } func testInterface(){ a:=apple{"红富士"} fmt.Print(a.getName()) a.setName("树顶红") fmt.Print(a.getName()) }
带参数的实现:
package mainimport "fmt" type Animal interface { eat() }type Cat struct { name string } func (cat Cat) eat(){ fmt.Println(cat.name +"猫吃东西" ) } type Dog struct{}func (dog Dog) eat(){ fmt.Println("狗吃东西") } func main() { var animal1 Animal=Cat{"maomao"} var animal2 Animal=Dog{} animal1.eat() animal2.eat() }
组合接口:
package mainimport "fmt"type reader interface {read() string }type writer interface {write() string }type rw interface {readerwriter }type mouse struct{}func (m mouse) read() string {return "mouse reading..." }func (m *mouse) write() string {return "mouse writing..." }func main() {var rw1 rw// 只要有一个指针实现,则此处必须是指针rw1 = &mouse{}// rw1 = new(mouse)fmt.Println(rw1.read())fmt.Println(rw1.write()) }
interface 本质是一个指针:
package mainimport "fmt"type Reader interface { // interface本质就是一个指针ReadBook() }type Writer interface {WriteBook() }type Book struct{ }func (t *Book) ReadBook() {fmt.Println("read a book") }func (t *Book) WriteBook() {fmt.Println("write a book") }func main() {// b : pair<type:Book, value:book{}地址>b := &Book{}// r: pair<type, value>var r Reader// r : pair<type:Book, value:book{}地址> pair是不变的r = b // interface r 本质就是一个指针,所以b也要是指针类型r.ReadBook()var w Writer// w : pair<type:Book, value:book{}地址> pair是不变的w = r.(Writer) // w和r的type一致,所以断言成功(断言就是强转?)w.WriteBook() }
<!-- 接口实现 -->
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {/*...*/} /* 实现接口,并且可以修改Engine实例的状态(字段),在调用方法函数时不需要复制结构体 */
*22_文件操作
读取
byteData, _ := os.ReadFile("go_study/hello.txt") fmt.Println(string(byteData))
// GetCurrentFilePath 获取当前文件路径 func GetCurrentFilePath() string { _, file, _, _ := runtime.Caller(1) return file }
// 分片读 file, _ := os.Open("go_study/hello.txt") defer file.Close() for {buf := make([]byte, 1)_, err := file.Read(buf)if err == io.EOF {break}fmt.Printf("%s", buf) }
// 带缓存读取 // 按行读 file, _ := os.Open("go_study/hello.txt") buf := bufio.NewReader(file) for {line, _, err := buf.ReadLine()fmt.Println(string(line))if err != nil {break} } // 指定分隔符 file, _ := os.Open("go_study/hello.txt") scanner := bufio.NewScanner(file) scanner.Split(bufio.ScanWords) // 按照单词读 //scanner.Split(bufio.ScanLines) // 按照行读 //scanner.Split(bufio.ScanRunes) // 按照中文字符读 //scanner.Split(bufio.ScanBytes) // 按照字节读读,中文会乱码for scanner.Scan() {fmt.Println(scanner.Text()) }
写入
err := os.WriteFile("go_study/file1.txt", []byte("内容"), os.ModePerm) fmt.Println(err)
// 常规写 func writeFile() {file, err := os.OpenFile("abc.txt", os.O_CREATE|os.O_APPEND|os.O_RDWR, 0644)if err != nil {panic(err)}defer file.Close()byteSlice := []byte("hello world!")bytesWritten, err := file.Write(byteSlice)if err != nil {panic(err)}fmt.Printf("Wrote %d bytes\n", bytesWritten) }
// 快速写 func main() {err := ioutil.WriteFile("abc.txt", []byte("add a new line"), 0644)if err != nil {panic(err)} }
// 缓冲写 func main() {file, err := os.OpenFile("abc.txt", os.O_CREATE|os.O_WRONLY, 0600)if err != nil {panic(err)}defer file.Close()msg := "Hello World!\n"writer := bufio.NewWriter(file)for i := 0; i < 5; i++ {writer.Write([]byte(msg))}writer.Flush() }
打开
// 如果文件不存在就创建 os.O_CREATE|os.O_WRONLY // 追加写 os.O_APPEND|os.O_WRONLY // 可读可写 os.O_RDWR
const ( O_RDONLY int = syscall.O_RDONLY // 只读 O_WRONLY int = syscall.O_WRONLY // 只写 O_RDWR int = syscall.O_RDWR // 读写O_APPEND int = syscall.O_APPEND // 追加 O_CREATE int = syscall.O_CREAT // 如果不存在就创建 O_EXCL int = syscall.O_EXCL // 文件必须不存在 O_SYNC int = syscall.O_SYNC // 同步打开 O_TRUNC int = syscall.O_TRUNC // 打开时清空文件 )
复制
io.Copy(dst Writer, src Reader) (written int64, err error)
read, _ := os.Open("go_study/file1.txt") write, _ := os.Create("go_study/file3.txt") // 默认是 可读可写,不存在就创建,清空文件 n, err := io.Copy(write, read) fmt.Println(n, err)
目录
dir, _ := os.ReadDir("go_study") for _, entry := range dir {info, _ := entry.Info()fmt.Println(entry.Name(), info.Size()) // 文件名,文件大小,单位比特 }
*23_错误处理
Go 语言通过内置的错误接口提供了非常简单的错误处理机制
error 类型是一个接口类型,这是它的定义:
type error interface {Error() string }
我们可以在编码中通过实现 error 接口类型来生成错误信息
函数通常在最后的返回值中返回错误信息。使用 errors.New 可返回一个错误信息:
func Sqrt(f float64) (float64, error) {if f < 0 {return 0, errors.New("math: square root of negative number")}// 实现 }
在下面的例子中,我们在调用 Sqrt 的时候传递的一个负数,然后就得到了 non-nil 的 error 对象,将此对象与 nil 比较,结果为 true,所以 fmt.Println(fmt 包在处理 error 时会调用 Error 方法)被调用,以输出错误,请看下面调用的示例代码:
result, err:= Sqrt(-1)if err != nil {fmt.Println(err) }
package mainimport ("fmt" )// 定义一个 DivideError 结构 type DivideError struct {dividee intdivider int }// 实现 `error` 接口 func (de *DivideError) Error() string {strFormat := `Cannot proceed, the divider is zero.dividee: %ddivider: 0 `return fmt.Sprintf(strFormat, de.dividee) }// 定义 `int` 类型除法运算的函数 func Divide(varDividee int, varDivider int) (result int, errorMsg string) {if varDivider == 0 {dData := DivideError{dividee: varDividee,divider: varDivider,}errorMsg = dData.Error()// errorMsg := dData.Error()return} else {return varDividee / varDivider, ""}}func main() {// 正常情况if result, errorMsg := Divide(100, 10); errorMsg == "" {fmt.Println("100/10 = ", result)}// 当除数为零的时候会返回错误信息if _, errorMsg := Divide(100, 0); errorMsg != "" {fmt.Println("errorMsg is: ", errorMsg)}} /* 100/10 = 10 errorMsg is: Cannot proceed, the divider is zero.dividee: 100divider: 0 */
笔记
这里应该介绍一下 panic 与 recover,一个用于主动抛出错误,一个用于捕获panic抛出的错误
概念
panic 与 recover 是 Go 的两个内置函数,这两个内置函数用于处理 Go 运行时的错误,panic 用于主动抛出错误,recover 用来捕获 panic 抛出的错误
-
引发
panic
有两种情况,一是程序主动调用,二是程序产生运行时错误,由运行时检测并退出。 -
发生
panic
后,程序会从调用panic
的函数位置或发生panic
的地方立即返回,逐层向上执行函数的defer
语句,然后逐层打印函数调用堆栈,直到被recover
捕获或运行到最外层函数。 -
panic
不但可以在函数正常流程中抛出,在defer
逻辑里也可以再次调用panic
或抛出panic
。defer
里面的panic
能够被后续执行的defer
捕获。 -
recover
用来捕获panic
,阻止panic
继续向上传递。recover()
和defer
一起使用,但是defer
只有在后面的函数体内直接被掉用才能捕获panic
来终止异常,否则返回nil
,异常继续向外传递。
/* defer 语句用于确保在函数返回之前执行一些操作,比如关闭文件、解锁互斥锁、释放资源等。 defer 是压入栈的,先进后出 recover 函数则用于在发生panic时恢复正常的执行流程,并且只有在defer函数中调用recover才能捕获到panic */ package main import "fmt" func main(){ defer recover() // 这里尝试捕获panic,但它在main函数的最外层defer中,因此实际上无法捕获到panic defer fmt.Println(recover) // recover是一个函数,而不是一个变量,不能这样直接调用 defer func(){ func(){ recover() // 无效,嵌套两层,而且recover()的调用并不在defer函数中 }() }() }//以下捕获有效 defer func(){ recover() }() func except(){ recover() } func test(){ defer except() // 这里尝试捕获panic,但except函数中只是调用了recover而没有panic,因此这里不会捕获到任何东西 panic("runtime error") // 这里触发panic }
// 正确代码 package main import "fmt" func except(){ if r := recover(); r != nil { fmt.Println("Recovered in except:", r) } } func test(){ defer except() // 在这里捕获panic panic("runtime error") // 这里触发panic } func main(){ test() // 调用test函数,它会在内部触发panic fmt.Println("After test()") // 如果except函数成功捕获了panic,则这行会被执行 }
多个panic只会捕捉最后一个:
package main import "fmt" func main(){defer func(){if err := recover() ; err != nil {fmt.Println(err)}}()defer func(){panic("three")}()defer func(){panic("two")}()panic("one") }
使用场景
一般情况下有两种情况用到:
-
程序遇到无法执行下去的错误时,抛出错误,主动结束运行
-
在调试程序时,通过 panic 来打印堆栈,方便定位错误
if result, errorMsg := Divide(100, 10); errorMsg == "" {fmt.Println("100/10 = ", result) }if _, errorMsg := Divide(100, 0); errorMsg != "" {fmt.Println("errorMsg is: ", errorMsg) }
等价于:
result, errorMsg := Divide(100,10) if errorMsg == ""{fmt.Println("100/10 = ", result) }result, errorMsg = Divide(100,0) if errorMsg != ""{fmt.Println("errorMsg is: ", errorMsg) }
fmt.Println 打印结构体的时候,会把其中的 error 的返回的信息打印出来。
type User struct {username stringpassword string }func (p *User) init(username string ,password string) (*User,string) {if ""==username || ""==password {return p,p.Error()}p.username = usernamep.password = passwordreturn p,""}func (p *User) Error() string {return "Usernam or password shouldn't be empty!"} }func main() {var user Useruser1, _ :=user.init("","");fmt.Println(user1) } // Usernam or password shouldn't be empty!
个人多次试验,总结几点 panic,defer 和 recover
-
panic 在没有用 recover 前以及在 recover 捕获那一级函数栈,panic 之后的代码均不会执行;一旦被 recover 捕获后,外层的函数栈代码恢复正常,所有代码均会得到执行;
-
panic 后,不再执行后面的代码,立即按照逆序执行 defer,并逐级往外层函数栈扩散;defer 就类似 finally;
-
利用 recover 捕获 panic 时,defer 需要再 panic 之前声明,否则由于 panic 之后的代码得不到执行,因此也无法 recover;
<!-- 用 recover 捕获 panic 时,defer 需要再 panic 之前声明 -->
// <!-- 用 recover 捕获 panic 时,defer 需要再 panic 之前声明 --> package mainimport ( "fmt" )func main() {fmt.Println("外层开始")defer func() {fmt.Println("外层准备recover")if err := recover(); err != nil {fmt.Printf("%#v-%#v\n", "外层", err) // err已经在上一级的函数中捕获了,这里没有异常,只是例行先执行defer,然后执行后面的代码} else {fmt.Println("外层没做啥事")}fmt.Println("外层完成recover")}()fmt.Println("外层即将异常")f()fmt.Println("外层异常后")defer func() {fmt.Println("外层异常后defer")}() }func f() {fmt.Println("内层开始")defer func() {fmt.Println("内层recover前的defer")}()defer func() {fmt.Println("内层准备recover")if err := recover(); err != nil {fmt.Printf("%#v-%#v\n", "内层", err) // 这里err就是panic传入的内容}fmt.Println("内层完成recover")}()defer func() {fmt.Println("内层异常前recover后的defer")}()panic("异常信息")defer func() {fmt.Println("内层异常后的defer")}()fmt.Println("内层异常后语句") //recover捕获的一级或者完全不捕获这里开始下面代码不会再执行 } /* 外层开始 外层即将异常 内层开始 内层异常前recover后的defer 内层准备recover "内层"-"异常信息" 内层完成recover 内层recover前的defer 外层异常后 外层异常后defer 外层准备recover 外层没做啥事 外层完成recover */
这个例子不给力,我重写了一个:
package mainimport ("fmt" )// 自定义错误信息结构 type DIV_ERR struct { etype int // 错误类型 v1 int // 记录下出错时的除数、被除数 v2 int } // 实现接口方法 error.Error() func (div_err DIV_ERR) Error() string { if 0==div_err.etype { return "除零错误" }else{ return "其他未知错误" } } // 除法 func div(a int, b int) (int,*DIV_ERR) { if b == 0 { // 返回错误信息 return 0,&DIV_ERR{0,a,b} } else { // 返回正确的商 return a / b, nil } } func main() { // 正确调用 v,r :=div(100,2) if nil!=r{ fmt.Println("(1)fail:",r) }else{ fmt.Println("(1)succeed:",v) } // 错误调用v,r =div(100,0) if nil!=r{ fmt.Println("(2)fail:",r) }else{ fmt.Println("(2)succeed:",v) } }
最后一段代码有点问题,需要修改为:
// 定义 `int` 类型除法运算的函数 func Divide(varDividee int, varDivider int) (result int, errorMsg string){ if varDivider == 0{ dData := DivideError{ dividee: varDividee, divider: varDivider, } errorMsg := dData.Error() return 0, errorMsg } else { return varDividee / varDivider, "" } }
补 defer
语句
-
defer 语句用于确保在函数返回之前执行一些操作,比如关闭文件、解锁互斥锁、释放资源等。
-
defer 是把数据压入栈的,先进后出
package main import "fmt"func main(){fmt.Println(add(30,60)) }func add(num1 int, num2 int) int {// go中程序遇到defer关键字不会立即执行defer后的语句,而是将defer后的语句压入一个栈中,然后执行完该函数后在执行栈中语句,先进后出// defer压入栈==>相当于copy了一份出来defer fmt.Println("num1=",num1)defer fmt.Println("num2=",num2)num1+=90num2+=50var sum int = num1+num2fmt.Println("sum=",sum)return sum } // 如果想关闭某个使用的资源时,在使用时直接defer,因为defer的延迟执行的机制
24_并发
*<!--go 函数名(参数列表)-->
多线程、协程
package mainimport "fmt"func main(){go sayfor i:=0; i<100; i++{fmt.Println("main=>", 1)} }func say(){for i:=0; i<100; i++{fmt.Println("say==========>", 1)} }
Go 语言支持并发,我们只需要通过 go 关键字来开启 goroutine 即可
goroutine
是轻量级线程,goroutine
的调度是由 Golang
运行时进行管理的
goroutine
语法格式:
go 函数名( 参数列表 )
例如:
go f(x, y, z)
开启一个新的 goroutine(协程):
f(x, y, z)
Go 允许使用 go 语句开启一个新的运行期线程, 即 goroutine(协程),以一个不同的、新创建的 goroutine 来执行一个函数。 同一个程序中的所有 goroutine 共享同一个地址空间
package mainimport ("fmt""time" )func say(s string) {for i := 0; i < 5; i++ {time.Sleep(100 * time.Millisecond) // 100毫秒fmt.Println(s)} }func main() {go say("world")say("hello") } /* 执行以上代码,你会看到输出的 hello 和 world 是没有固定先后顺序。因为它们是两个 goroutine 在执行: world hello hello world world hello hello world world hello */
*通道(channel)
通道(channel)是用来传递数据的一个数据结构
通道可用于两个 goroutine 之间通过传递一个指定类型的值来同步运行和通讯。操作符 <-
用于指定通道的方向,发送或接收 如果未指定方向,则为双向通道
通过 channel 可以实现两个 goroutine 之间的通信
关闭通道并不会丢失里面的数据,只是让读取通道数据的时候不会读完之后一直阻塞等待新数据写入
通道遵循先进先出原则(队列)
go func(c chan int) { //读写均可的channel c } (a) go func(c <- chan int) { //只读的Channel } (a) go func(c chan <- int) { //只写的Channel } (a)
ch <- v // 把 v 发送到通道 ch v := <-ch /* 从 ch 接收数据并把值赋给 v */
声明一个通道很简单,我们使用chan关键字即可,通道在使用前必须先创建:
ch := make(chan int)
注意:默认情况下,通道是不带缓冲区的 发送端发送数据,同时必须有接收端相应的接收数据
以下实例通过两个 goroutine 来计算数字之和,在 goroutine 完成计算后,它会计算两个结果的和:
package mainimport "fmt"func sum(s []int, c chan int) {sum := 0for _, v := range s {sum += v}c <- sum // 把 sum 发送到通道 c }func main() {s := []int{7, 2, 8, -9, 4, 0}c := make(chan int)go sum(s[:len(s)/2], c)go sum(s[len(s)/2:], c)x, y := <-c, <-c // 从通道 c 中接收fmt.Println(x, y, x+y) } // -5 17 12
*通道缓冲区
通道可以设置缓冲区,通过 make 的第二个参数指定缓冲区大小:
ch := make(chan int, 100)
带缓冲区的通道允许发送端的数据发送和接收端的数据获取处于异步状态,就是说发送端发送的数据可以放在缓冲区里面,可以等待接收端去获取数据,而不是立刻需要接收端去获取数据
不过由于缓冲区的大小是有限的,所以还是必须有接收端来接收数据的,否则缓冲区一满,数据发送端就无法再发送数据了
注意:如果通道不带缓冲,发送方会阻塞直到接收方从通道中接收了值。如果通道带缓冲,发送方则会阻塞直到发送的值被拷贝到缓冲区内;如果缓冲区已满,则意味着需要等待直到某个接收方获取到一个值。接收方在有值可以接收之前会一直阻塞
package mainimport "fmt"func main() {// 这里我们定义了一个可以存储整数类型的带缓冲通道// 缓冲区大小为2ch := make(chan int, 2)// 因为 ch 是带缓冲的通道,我们可以同时发送两个数据// 而不用立刻需要去同步读取数据ch <- 1ch <- 2// 获取这两个数据fmt.Println(<-ch)fmt.Println(<-ch) } /* 1 2 */
*Go 遍历通道与关闭通道
<!-- v,ok:=<-ch close() -->
Go 通过 range 关键字来实现遍历读取到的数据,类似于与数组或切片。格式如下:
v, ok := <-ch
如果通道接收不到数据后 ok 就为 false,这时通道就可以使用 close() 函数来关闭
-
放完了之后虽然缓冲区关闭了,但是缓冲区的内容还保留,所以还能继续取出
package mainimport ("fmt" )func fibonacci(n int, c chan int) {x, y := 0, 1for i := 0; i < n; i++ {c <- xx, y = y, x+y}close(c) }func main() {c := make(chan int,10)go fibonacci(cap(c), c)/* range 函数遍历每个从通道接收到的数据,因为 c 在发送完 10 个数据之后就关闭了通道,所以这里我们 range 函数在接收到 10 个数据之后就结束了 如果上面的 c 通道不关闭,那么 range 函数就不会结束,从而在接收第 11 个数据的时候就阻塞了 */for i := range c {fmt.Println(i)} } /* 0 1 1 2 3 5 8 13 21 34 */
* 通知子goroutine
(协程)退出的两种方式
context
package mainimport ("fmt""sync""time" )var wg sync.WaitGroup// 初始的例子 func worker() {defer wg.Done()for {fmt.Println("worker")time.Sleep(time.Second)}// 如何接收外部命令实现退出 }func main() {wg.Add(1)go worker()// 如何结束goroutinewg.Wait()fmt.Println("over") }
全局变量
// 全局变量 package mainimport ("fmt""sync""time" )var wg sync.WaitGroupvar exit bool// 初始的例子 func worker() {defer wg.Done()for {fmt.Println("worker")time.Sleep(time.Second)// 如何接收外部命令实现退出if exit {break}} }func main() {wg.Add(1)go worker()// 如何结束goroutinetime.Sleep(time.Second * 5)exit = truewg.Wait()fmt.Println("over") }
channel
// make与new的区别 /* 都用来初始化内存 new返回指针类型 make返回的是对应的类型*/
package mainimport ("fmt""sync""time" )var wg sync.WaitGroupvar exit bool// 初始的例子 func worker(ch <-chan bool) {defer wg.Done() LABEL:for {select {case <-ch:break LABELdefault:fmt.Println("worker")time.Sleep(time.Second)// 如何接收外部命令实现退出}} }func main() {var exitChan = make(chan bool, 1)wg.Add(1)go worker(exitChan)// 如何结束goroutinetime.Sleep(time.Second * 5)exitChan <- truewg.Wait()fmt.Println("over") }
使用sync.WaitGroup
var wg sync.WaitGroupfunc hello_wg(i int) {defer wg.Done() // goroutine结束就登记-1fmt.Println("hello_wg!", i) }func main() {for i := 0; i < 10; i++ {wg.Add(1) // 启动一个goroutine就登记+1go hello_wg(i)time.Sleep(time.Second)}wg.Wait() // 等待所有登记的goroutine都结束 }
select
func main() {var c1 = make(chan int)go func() {time.Sleep(time.Second * 10)c1 <- 1}()// 此处会一直等到10S到期,通道里有值才会继续往下走。// 如果增加了 time.After(time.Second * 3) ,则最多3秒则结束// 如果这2个case都不行,会走default,也可以不设置defaultselect {case i, ok := <-c1:if ok {fmt.Println("取值", i)}case <-time.After(time.Second * 3):fmt.Println("request time out")default:fmt.Println("无数据")} }
互斥锁
func add() {for i := 0; i < 5000; i++ {// 如果不加锁,此处会有并发问题lock.Lock() // 加锁x = x + 1lock.Unlock() // 解锁}wg.Done() }func main() {wg.Add(2)go add()go add()wg.Wait()fmt.Println(x) }
笔记
goroutine 是 golang 中在语言级别实现的轻量级线程,仅仅利用 go 就能立刻起一个新线程。多线程会引入线程之间的同步问题,在 golang 中可以使用 channel 作为同步的工具
通过 channel 可以实现两个 goroutine 之间的通信
创建一个 channel, make(chan TYPE {, NUM}) TYPE 指的是 channel 中传输的数据类型,第二个参数是可选的,指的是 channel 的容量大小
向 channel 传入数据, CHAN <- DATA , CHAN 指的是目的 channel 即收集数据的一方, DATA 则是要传的数据
从 channel 读取数据, DATA := <-CHAN ,和向 channel 传入数据相反,在数据输送箭头的右侧的是 channel,形象地展现了数据从隧道流出到变量里
我们单独写一个 say2 函数来跑 goroutine,并且 Sleep 时间设置长一点,150 毫秒,看看会发生什么:
package main import ("fmt""time" ) func say(s string) {for i := 0; i < 5; i++ {time.Sleep(100 * time.Millisecond)fmt.Println(s, (i+1)*100)} } func say2(s string) {for i := 0; i < 5; i++ {time.Sleep(150 * time.Millisecond)fmt.Println(s, (i+1)*150)} } func main() {go say2("world")say("hello") } /* hello 100 world 150 hello 200 hello 300 world 300 hello 400 world 450 hello 500[Done] exited with code=0 in 2.066 seconds */
问题来了,say2 只执行了 3 次,而不是设想的 5 次,为什么呢?
原来,在 goroutine 还没来得及跑完 5 次的时候,主函数已经退出了。
我们要想办法阻止主函数的结束,要等待 goroutine 执行完成之后,再退出主函数:
package mainimport ("fmt""time" )func say(s string) {for i := 0; i < 5; i++ {time.Sleep(100 * time.Millisecond)fmt.Println(s, (i+1)*100)} } func say2(s string, ch chan int) {for i := 0; i < 5; i++ {time.Sleep(150 * time.Millisecond)fmt.Println(s, (i+1)*150)}ch <- 0close(ch) }func main() {ch := make(chan int)go say2("world", ch)say("hello")fmt.Println(<-ch) }
我们引入一个信道,默认的,信道的存消息和取消息都是阻塞的,在 goroutine 中执行完成后给信道一个值 0,则主函数会一直等待信道中的值,一旦信道有值,主函数才会结束
更好的展示边入边出概念:
package mainimport ("fmt""time" )func main() {c := make(chan int, 10)go fibonacci(cap(c), c)for v := range c {fmt.Println("out:", time.Now())fmt.Println(v)} }func fibonacci(n int, c chan int) {x, y := 0, 1for i :=0; i < n; i++ {c <- xfmt.Println("in:",time.Now())time.Sleep(100)x, y = y, x+y}close(c) }
关闭通道并不会丢失里面的数据,只是让读取通道数据的时候不会读完之后一直阻塞等待新数据写入
Channel 是可以控制读写权限的 具体如下:
go func(c chan int) { //读写均可的channel c } (a) go func(c <- chan int) { //只读的Channel } (a) go func(c chan <- int) { //只写的Channel } (a)
形象说明一下无缓冲和有缓冲的区别:
无缓冲是同步的,例如 make(chan int),就是一个送信人去你家门口送信,你不在家他不走,你一定要接下信,他才会走,无缓冲保证信能到你手上
有缓冲是异步的,例如 make(chan int, 1),就是一个送信人去你家仍到你家的信箱,转身就走,除非你的信箱满了,他必须等信箱空下来,有缓冲的保证信能进你家的邮箱
修改一下上面笔记中的程序如下:
package main import ("fmt""time" )func sum(s []int, c chan int) {sum := 0for _, v := range s {sum += v}fmt.Printf("sum:")fmt.Printf("%#v\n", sum)c <- sum // 把 sum 发送到通道 cfmt.Println("after channel pro") }// 通道不带缓冲,表示是同步的,只能向通道 c 发送一个数据,只要这个数据没被接收然后所有的发送就被阻塞 func main() {s := []int{7, 2, 8, -9, 4, 0}c := make(chan int)fmt.Println("go [0,3]")go sum(s[:len(s)/2], c) //a//这里开启一个新的运行期线程,这个是需要时间的,本程序继续往下走fmt.Println("go [3,6]")go sum(s[len(s)/2:], c) //bfmt.Println("go2 [0,3]")go sum(s[:len(s)/2], c) //cfmt.Println("go2 [3,6]")go sum(s[len(s)/2:], c) //d/*a b c d和main一起争夺cpu的,他们的执行顺序完全无序,甚至里面不同的语句都相互穿插但无缓冲的等待是同步的,所以接下来a b c d都会执行,一直执行到c <- sum后,开始同步阻塞因此after channel pro是打印不出来的, 等要打印after channel pro的时候,main就结束了*/fmt.Println("go3 start waiting...")time.Sleep(1000 * time.Millisecond)fmt.Println("go3 waited 1000 ms")//因为a b c d都在管道门口等着,这里度一个,a b c d就继续一个,这个结果的顺序可能是acbdaa := <-cbb := <-cfmt.Println(aa)fmt.Println(bb)x, y := <-c, <-cfmt.Println(x, y, x+y) } /* go [0,3] go [3,6] go2 [0,3] go2 [3,6] sum:sum:sum:17 go3 start waiting... 17 -5 sum:-5 go3 waited 1000 ms 17 17 -5 -5 -10 */
修改成 make(chan int, 2),同时合并:
fmt.Printf("sum:") fmt.Printf("%#v\n", sum)
为:
fmt.Printf("sum:%#v\n", sum)
可以看到 after channel pro 没有被阻塞了
结果:
go [0,3] go [3,6] go2 [0,3] go2 [3,6] go3 start waiting... sum:-5 sum:17 after channel pro after channel pro sum:17 sum:-5 go3 waited 1000 ms -5 17 17 -5 12
package mainimport "fmt"func main() {ch := make(chan int, 2)ch <- 1a := <-chch <- 2ch <- 3fmt.Println(<-ch)fmt.Println(<-ch)fmt.Println(a) } /* 2 3 1 */
通道遵循先进先出原则(队列)
不带缓冲区的通道在向通道发送值时,必须及时接收,且必须一次接收完成
而带缓冲区的通道则会以缓冲区满而阻塞,直到先塞发送到通道的值被从通道中接收才可以继续往通道传值。就像往水管里推小钢珠一样,如果钢珠塞满没有从另一头放出,那么这一头就没法再往里塞,是一个道理。例如上面的例子,最多只能让同时在通道中停放2个值,想多传值,就需要把前面的值提前从通道中接收出去
Go 遍历通道与关闭通道的例子这样改一下,会更清楚:
package mainimport ("fmt""time" )func fibonacci(n int, c chan int) {x, y := 0, 1for i := 0; i < n; i++ {c <- xx, y = y, x+ytime.Sleep(1000 * time.Millisecond)}close(c) } func main() {c := make(chan int, 10)go fibonacci(cap(c), c)// range 函数遍历每个从通道接收到的数据,因为 c 在发送完 10 个// 数据之后就关闭了通道,所以这里我们 range 函数在接收到 10 个数据// 之后就结束了。如果上面的 c 通道不关闭,那么 range 函数就不// 会结束,从而在接收第 11 个数据的时候就阻塞了。for i := range c {fmt.Println(i)} }
演示带缓冲区的 channel 实现异步存取
实例代码:
package main import ( "fmt" "time" ) func put(c chan int) { for i := 0; i < 10; i++ { c <- i time.Sleep(100 * time.Millisecond) fmt.Println("->放入", i) } fmt.Println("=所有的都放进去了!关闭缓冲区,但是里面的数据不会丢失,还能取出。") close(c) } func main() { ch := make(chan int, 5) go put(ch) for { time.Sleep(1000 * time.Millisecond) data, ok := <-ch if ok == true { fmt.Println("<-取出", data) } else { break } } } /* ->放入 0 ->放入 1 ->放入 2 ->放入 3 ->放入 4 <-取出 0 ->放入 5 <-取出 1 ->放入 6 <-取出 2 ->放入 7 <-取出 3 ->放入 8 <-取出 4 ->放入 9 =所有的都放进去了!关闭缓冲区,但是里面的数据不会丢失,还能取出。 <-取出 5 <-取出 6 <-取出 7 <-取出 8 <-取出 9 */
放的速度较快,先放满了 4 个,阻塞住
取的速度较慢,放了4个才开始取,由于缓冲区已经满了,所以取出一个之后,才能再次放入
放完了之后虽然缓冲区关闭了,但是缓冲区的内容还保留,所以还能继续取出
并发进阶
协程与通道实现
-
通道接收数据
// 阻塞接收 data := <-ch 12 // 非阻塞接收 data, ok := <-ch 12 // 忽略接收数据 <-ch 12 // 循环接收数据 for data := range ch {// }
package mainimport ("fmt" )func Sum(s []int, ch chan int) {sum := 0for _, v := range s {sum += v}ch <- sum }func main() {s := []int{6, 7, 8, -9, 1, 8}ch := make(chan int)go Sum(s[:len(s)/2], ch)go Sum(s[len(s)/2:], ch)a, b := <-ch, <-chfmt.Println(a, b, a+b) }
-
通道缓冲区
ch := make(chan int, 6)
只要缓冲区未满,发送方和接收方可以处于异步状态,例如如下程序:
package mainimport ("fmt" )func main() {ch := make(chan int, 3)ch <- 6ch <- 7ch <- 8fmt.Println(<-ch)fmt.Println(<-ch)fmt.Println(<-ch) }
-
select 多路复用
select { default:// case <- ch1:// case v2 := <- ch2:// }
实现例如如下程序部分:
timeout := make(chan interface{}, 1)go func() {time.Sleep(6)timeout <- interface{}{} }select {case <- ch://case <- timeout:// }
-
遍历通道与关闭通道
v, ok := <-ch
例如如下程序:
package mainimport ("fmt" )func fibonacci(n int, ch chan int) {a, b := 0, 1for i := 0; i < n; i++ {ch <- aa, b = b, a+b}close(ch) }func main() {ch := make(chan int, 6)go fibonacci(cap(ch), ch)for j := range ch {fmt.Println(j)} }
sync 实现并发
竞态
使用并发,可能产生数据争用的竞态问题,例如如下的程序部分:
// 输出 0 或 6 func main() {fmt.Println(getNumber()) }func getNumber() int {var i intgo func() {i = 6}()return i }
互斥锁
sync.Mutex
用于实现互斥锁,用于读写不确定的场景,全局锁,其结构和方法如下:
type Mutex struct {state int32 //当前互斥锁的状态sema uint32 //控制锁状态的信号量 }func (m *Mutex) Lock() func (m *Mutex) Unlock()
-
必须先 Lock() ,然后 Unlock() 。
-
连续 Lock() ,死锁。
-
先 Unlock() ,后 Lock() ,panic 。
-
可以一个 goroutine 先 Lock() ,其他 goroutine 后 Unlock()。
例如如下的程序:
package mainimport ("fmt""sync""time" )func main() {var mutex sync.Mutexwait := sync.WaitGroup{}fmt.Println("Locked")mutex.Lock()// 在主goroutine中,首先锁定互斥锁for i := 1; i <= 5; i++ {wait.Add(1)go func(i int) {defer wait.Done() //使用defer关键字确保goroutine完成后通知WaitGroupfmt.Println("Not lock:", i)mutex.Lock()fmt.Println("Locked:", i)time.Sleep(time.Second)fmt.Println("Unlocked:", i)mutex.Unlock()}(i)}time.Sleep(time.Second)fmt.Println("Unlocked")mutex.Unlock()wait.Wait()// wait.Wait(): 等待所有通过wait.Add(1)添加到WaitGroup的goroutine完成 }
package mainimport ("fmt""sync" )var num int var wait sync.WaitGroup var lock sync.Mutexfunc add() {// 谁先抢到了这把锁,谁就把它锁上,一旦锁上,其他的线程就只能等着lock.Lock()for i := 0; i < 1000000; i++ {num++}lock.Unlock()wait.Done() } func reduce() {lock.Lock()for i := 0; i < 1000000; i++ {num--}lock.Unlock()wait.Done() }func main() {wait.Add(2)go add()go reduce()wait.Wait()fmt.Println(num) }
读写互斥锁
sync.RWMutex
可以多个读锁或者一个写锁,用于读次数远远多于写次数的场景,其结构和方法的声明如下:
type RWMutex struct {w MutexwriterSem uint32readerSem uint32readerCount int32readerWait int32 } //写操作 func (*RWMutex) Lock() func (*RWMutex) Unlock()//读操作 func (*RWMutex) RLock() func (*RWMutex) RUnlock()
例如如下的程序:
package mainimport ("fmt""sync""math/rand" )var count int var rw sync.RWMutexfunc main() {ch := make(char struct{}, 6)for i := 0; i < 3; i++ {go ReadCount(i, ch)}for i := 0; i < 3; i++ {go WriteCount(i, ch)}for i := 0; i < 6; i++ {<-ch} }func ReadCount(n int, ch chan struct{}) {rw.RLock()fmt.Printf("goroutine %d 进入读操作...\n", n)v := countfmt.Printf("goroutine %d 读取结束,值为:%d\n", n, v)rw.RUnlock()ch <- struct{}{} }func WriteCount(n int, ch chan struct{}) {rw.Lock()fmt.Printf("goroutine %d 进入写操作...\n", n)v := rand.Intn(10)count = vfmt.Printf("goroutine %d 写入结束,值为:%d\n", n, v)rw.Unlock()ch <- struct{}{} }
sync.Once
结构体
多次调用 sync.Once.Do(f func())
,只执行第一次调用的函数,其结构和方法的声明如下:
type Once struct {done uint32m Mutex }func (o *Once) Do(f func())
例如如下的程序:
package mainimport ("fmt""sync" )func main() {var once sync.OnceonceBody := func() {fmt.Println("test only once,这里只打印一次!")//打印}done := make(chan bool)for i := 0; i < 6; i++ {go func() {once.Do(onceBody)//确保只执行一次done <- true}()}for i := 0; i < 6; i++ {<-done} }
例如如下程序:
package mainimport ("fmt""sync" )var wg sync.WaitGroup var once sync.Oncefunc func1(ch1 chan<- int) {defer wg.Done()//使用defer关键字确保goroutine完成后通知WaitGroupfor i := 0; i < 10; i++ {ch1 <- i}close(ch1) }func func2(ch1 <-chan int, ch2 chan<- int) {defer wg.Done()for {x, ok := <-ch1if !ok {break}ch2 <- 2 * x}once.Do(func() { close(ch2) }) // 确保某个操作只执行一次 }func main() {ch1 := make(chan int, 10)ch2 := make(chan int, 10)wg.Add(3)go func1(ch1)go func2(ch1, ch2)go func2(ch1, ch2)wg.Wait()for ret := range ch2 {fmt.Println(ret)} }
同步等待组 sync.WaitGroup
用于等待一组线程的结束,其结构和方法的声明如下:
func (*WaitGroup) Add(int) func (w *WaitGroup) Done() {w.Add(-1) } func (*WaitGroup) Wait()
例如如下的程序:
package mainimport ("fmt""sync""time" )func main() {var wg sync.WaitGroupwg.Add(1)go func() {defer wg.Done()fmt.Println("1 goroutine sleep ...")time.Sleep(2)fmt.Println("1 goroutine exit ...")}()wg.Add(1)go func() {defer wg.Done()fmt.Println("2 goroutine sleep ...")time.Sleep(4)fmt.Println("2 goroutine exit ...")}()fmt.Println("Waiting for all goroutine ")wg.Wait()fmt.Println("All goroutines finished!") }
又例如如下的程序:
package mainimport ("fmt""sync""time" )func main() {testFunc := func(wg *sync.WaitGroup, id int) {defer wg.Done()fmt.Printf("%v goroutine start ...\n", id)time.Sleep(2)fmt.Printf("%v goroutine exit ...\n", id)}var wg sync.WaitGroupconst N = 3wg.Add(N)for i := 0; i < N; i++ {go testFunc(&wg, i)}fmt.Println("Waiting for all goroutine")wg.Wait()fmt.Println("All goroutines finished!") }
package mainimport ("fmt""sync""time" )var (wait = sync.WaitGroup{} )func sing() {fmt.Println("唱歌")time.Sleep(1 * time.Second)fmt.Println("唱歌结束")wait.Done() }func main() {wait.Add(4)go sing()go sing()go sing()go sing()wait.Wait()fmt.Println("主线程结束") }
竞态检测器
竞态分析工具,例如:
go build -race main.go go run -race main.go go test -race test_main.go
例如如下的程序:
package mainimport "fmt"func main() {c := make(chan bool)m := make(map[string]string)go func() {m["a"] = "one" // 第一个冲突访问.c <- true}()m["b"] = "two" // 第一个冲突访问<-cfor k, v := range m {fmt.Println(k, v)} }
线程安全的map
package mainimport ("fmt""sync""time" )var wait sync.WaitGroup var mp = map[string]string{} var lock sync.Mutexfunc reader() {for {lock.Lock()fmt.Println(mp["time"])lock.Unlock()}wait.Done() } func writer() {for {lock.Lock()mp["time"] = time.Now().Format("15:04:05")lock.Unlock()}wait.Done() }func main() {wait.Add(2)go reader()go writer()wait.Wait() }
package mainimport ("fmt""sync""time" )var wait sync.WaitGroup var mp = sync.Map{}func reader() {for {fmt.Println(mp.Load("time"))}wait.Done() } func writer() {for {mp.Store("time", time.Now().Format("15:04:05"))}wait.Done() }func main() {wait.Add(2)go reader()go writer()wait.Wait() }
Go 并发的 Web 应用
自增整数生成器
package mainimport "fmt"func IntegerGenerator() chan int {var ch chan int = make(chan int)go func() {for i := 0; ; i++ {ch <- i}}()return ch }func main() {generator := IntegerGenerator()for i := 0; i < 100; i++ {fmt.Println(<-generator)} }
并发的消息发送器
package mainimport "fmt"func SendNotification(user string) <-chan string {notifications := make(char string, 1)go func() {defer close(notifications)notifications <- fmt.Sprintf("Hi %s, welcome!", user)}()return notifications }func main() {barry := SendNotification("barry")shirdon := SendNotification("shirdon")fmt.Println(<-barry)fmt.Println(<-shirdon ) }
多路复合计算器
package mainimport ("fmt""math/rand""time" )func doCompute(x int) int {time.Sleep(time.Duration(rand.Intn(10)) * time.Millisecond)return 1+x }func branch(x int) chan int {ch := make(chan int, 1)go func() {defer close(ch)ch <- doCompute(x)}()return ch } /* func Recombination(chs... chan int) chan int {ch := make(chan int)for _, c := range chs {go func(c chan int) {ch <- <-c}(c)}return ch } */func Recombination(chs... chan int) chan int {num := len(chs)ch := make(chan int, num)go func() {defer close(ch)for i := 0; i < num; i++ {/*select {case v := <-chs[i]:ch <- v}*/ch <- <-chs[i]}}()return ch }func main() {result := Recombination(branch(10), branch(20), branch(30))/*for i := 0; i < 3; i++ {fmt.Println(<-result)}*/for v := range result {fmt.Println(v)} }
select 创建多通道监听器
package mainimport ("fmt""time" )func foo(x int) <-chan int {ch := make(chan int, 1)go func() {defer close(ch)ch <- x}()return ch }func main() {ch1, ch2, ch3 := foo(3), foo(6), foo(9)ch := make(chan int, 3)go func() {defer close(ch)timeout := time.After(1*time.Second)for isTimeout := false; !isTimeout; {select {case v1, ok := <-ch1:if ok {ch <- v1}case v2, ok := <-ch2:if ok {ch <- v2}case v3, ok := <-ch3:if ok {ch <- v3}case <-timeout:isTimeout = true}}}()for v := range ch {fmt.Println(v)} }
无缓冲通道阻塞主线
package mainimport ("fmt" )func main() {ch, quit := make(chan int), make(chan int)go func() {ch <- 8quit <- 1}()for isQuit:= false; !isQuit; {select {case v := <-ch:fmt.Printf("received %d from ch", v)case <-quit:isQuit= true}} }
筛选法求素数
package mainimport ("fmt" )func IntegerGenerator() chan int {var ch chan int = make(chan int)go func() {for i := 2; ; i++ {ch <- i}}()return ch }func Filter(in chan int, number int) chan int {out := make(chan int)go func() {for {i := <-inif i%number != 0 {out <- i}}}()return out }func main() {const max = 100//产生所有的输入流//2 3 4 5 6 7 8 9 10numbers := IntegerGenerator()//取第一个数2number := <-numbersfor number <= max {fmt.Println(number)//过滤输入流,产生新的输入流//第一次过滤//3 5 7 9//第二次过滤//5 7//第三次过滤//7numbers = Filter(numbers, number)//第一次过滤后取第一个数3//第二次过滤后取第一个数5//第三次过滤后取第一个数7number = <-numbers} }
随机数生成器
package mainimport ("fmt" )func randGenerator() chan int {var ch chan int = make(chan int)go func() {for {select {case ch <- 0:case ch <- 1:}}}()return ch }func main() {generator := IntegerGenerator()for i := 0; i < 10; i++ {fmt.Println(<-generator)} }
定时器
package mainimport ("fmt""time" )func Timer(duration time.Duration) chan bool {var ch chan bool = make(chan bool)go func() {time.Sleep(duration)ch <- true}()return ch }func main() {timeout := Timer(5*time.Second)for {select {case <-timeout:fmt.Println("already 5s!")return}} }
25_单元测试
-
文件以
_test.go
结尾,方法以Test
开头,方法入参t *testing.T
func TestProgram(t *testing.T) {split := strings.Split("a,b,c", ",")defer func() {if err := recover(); err != nil {fmt.Println("异常:", err)}}()findElement(split, "a") }// 查找元素 func findElement(split []string, target string) {flag := falsefor _, e := range split {if e == target {flag = truebreak}}if flag {fmt.Println("已经找到")} else {panic("没找到")} }
<!-- go安装 -->
国内镜像网站
Go下载 - Go语言中文网 - Golang中文社区
NJU Mirror
环境变量配置
1、
-
如果
Path
中有你下载的Go
语言的工作目录+\bin
,就不需要加GOROOT
-
如果没有,则添加变量
GOROOT
path
为D:\***\Go
(Go语言工作目录) -
最后在Path变量下添加
%GOROOT%\bin
2、
-
创建一个文件夹(尽量不要再
C
盘)命名为gopath
,在其下面创建三个文件夹-
bin
-
pkg
-
src
-
-
添加环境变量
GOPATH
path
为你的gopath
路径
在cmd
中输入 go version
查看go的版本信息
输入go env
查看go
的信息
go env
配置
GO111MODULE
:包管理模式,auto即可,可以同时使用MODULE
和GOPATH
模式(非mod项目也可以build 解决 go: go.mod file not found in current directory or any parent directory
)
在cmd
中的命令
# cmd go env -w GO111MODULE=auto# 配置包代理镜像 go env -w GOPROXY=https://goproxy.cn,direct #公司内根据需要配置私有模块 go env -w GONOPROXY=xxx.xxx go env -w GONOSUMDB=xxx.xxx <!-- `go env` 可查看目前的`env`变量配置 -->
创建项目
创建GoDemo
文件夹,执行 go mod init GoDemo
命令,初始化工程
创建main.go
文件,输入如下代码,并保存
package main import "fmt"func main() {fmt.Println("Hello World") }
cmd
,执行go run main.go
断点调试
选择vscode
开发和调试Go项目
首先安装go插件,插件市场直接搜索go并安装
然后vscode
命令面板输入 go install -v
,安装必要的go开发工具
打开创建的GoDemo
项目,F5
启动调试(还报错的话vscode
会有个提示然后点击安装即可)
如果断点调试提示go版本过旧(1.16.9) ,可以安装旧版dlv
go get -u github.com/go-delve/delve/cmd/dlv@v1.6.1
go install github.com/go-delve/delve/cmd/dlv@v1.6.1
dlv.exe
会在GOPATH
目录下生成
<!-- go mod -->
管理项目依赖的包
go mod init go get go get -u // 更新 go mod tidy go list go mod vendor // 依赖项复制到项目的vendor目录以便离线构建
// gin框架下载 go get -u github.com/gin-gonic/gin
发布go mod 到github
中
-
go mod init 包名
-
这个包名是
github.com/用户名/你的包名
-