- 正文
- 环境安装
- 结构
- 基础语法
- 数据类型
- 变量
- 常量
- 运算符
- 条件语句
- 循环语句
- 函数
- 变量作用域
- 数组
- 指针
- 结构体
- 切片(Slice)
- 范围(Range)
- Map(集合)
- 递归函数
- 类型转换
- 接口
- 参考资料
正文
环境安装
结构
package main
import "fmt"
func main() {
/* 这是我的第一个简单的程序 */
fmt.Println("Hello, World!")
}
需要注意的是 {
不能单独放在一行。
关于包,注意以下几点:
- 文件名与包名没有直接关系,不一定要将文件名与包名定成同一个。
- 文件夹名与包名没有直接关系,并非需要一致。
- 同一个文件夹下的文件只能有一个包名,否则编译报错。
基础语法
Go 程序的一般结构:
// 当前程序的包名
package main
// 导入其他包
import . "fmt"
// 常量定义
const PI = 3.14
// 全局变量的声明和赋值
var name = "gopher"
// 一般类型声明
type newType int
// 结构的声明
type gopher struct{}
// 接口的声明
type golang interface{}
// 由main函数作为程序入口点启动
func main() {
Println("Hello World!")
}
Go 程序是通过 package 来组织的。
只有 package 名称为 main 的源码文件可以包含 main 函数。
一个可执行程序有且仅有一个 main 包。
通过 import 关键字来导入其他非 main 包。
可以通过 import 关键字单个导入:
import "fmt"
import "io"
也可以同时导入多个:
import (
"fmt"
"math"
)
func main() {
fmt.Println(math.Exp2(10)) // 1024
}
导入包取个别名:
package 包名:
// 为fmt起别名为fmt2
import fmt2 "fmt"
一般使用 <PackageName>.<FunctionName>
调用,可以省略调用(不建议使用),前面加个点表示省略调用,
那么调用该模块里面的函数,可以不用写模块名称了:
// 调用的时候只需要Println(),而不需要fmt.Println()
import . "fmt"
func main (){
Println("hello,world")
}
通过 const 关键字来进行常量的定义。
通过在函数体外部使用 var 关键字来进行全局变量的声明和赋值。
通过 type 关键字来进行结构(struct)和接口(interface)的声明。
通过 func 关键字来进行函数的声明。
可见性规则:
Go语言中,使用大小写来决定该常量、变量、类型、接口、结构或函数是否可以被外部包所调用。
函数名首字母小写即为 private ,函数名首字母大写即为 public :
func getId() {}
func Printf() {}
fmt 包
Print() 函数将参数列表 a 中的各个参数转换为字符串并写入到标准输出中。
非字符串参数之间会添加空格,返回写入的字节数。
func Print(a ...interface{}) (n int, err error)
Println() 函数功能类似 Print,只不过最后会添加一个换行符。
所有参数之间会添加空格,返回写入的字节数。
func Println(a ...interface{}) (n int, err error)
Printf() 函数将参数列表 a 填写到格式字符串 format 的占位符中。
填写后的结果写入到标准输出中,返回写入的字节数。
func Printf(format string, a ...interface{}) (n int, err error)
以下三个函数功能同上面三个函数(Print()、Println()、Printf()),只不过将转换结果写入到 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)
以下三个函数功能同上面三个函数(Print()、Println()、Printf()),只不过将转换结果以字符串形式返回。
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 3 cd
// a b 1 2 3 c d
// ab 1 2 3 cd
if 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 {
if n > 100 {
return fmt.Errorf("数值 %d 超出范围(100)", n)
}
fmt.Print(n, "%\n")
}
return nil
}
Formatter 由自定义类型实现,用于实现该类型的自定义格式化过程。
当格式化器需要格式化该类型的变量时,会调用其 Format 方法。
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
}
实例:
type Ustr string
func (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 函数,则必须自己处理所有格式,包括 %#v
if 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 | 精度:5
fmt.Printf("%+ 0#8.5M\n", u) // 旗标:[+ 0#] | 宽度:8 | 精度:5
fmt.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, false
fmt.Scan(&a, &b, &c)
fmt.Println(a, b, c)
// 在终端执行后,输入 abc 1 回车 true 回车
// 结果 abc 1 true
}
// 对于 Scanln 而言,回车结束扫描
func main() {
a, b, c := "", 0, false
fmt.Scanln(&a, &b, &c)
fmt.Println(a, b, c)
// 在终端执行后,输入 abc 1 true 回车
// 结果 abc 1 true
}
// 格式字符串可以指定宽度
func main() {
a, b, c := "", 0, false
fmt.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 string
func (u *Ustr) Scan(state fmt.ScanState, verb rune) (err error) {
var s []byte
switch 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 Ustr
n, 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
}
数据类型
变量
常量
运算符
条件语句
switch
switch 语句还可以被用于 type-switch 来判断某个 interface 变量中实际存储的变量类型。
package main
import "fmt"
func main() {
var x interface{}
x = 1
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("未知型")
}
}
输出:
x 是 int 型
使用 fallthrough 会强制执行后面的 case 语句,fallthrough 不会判断下一条 case 的表达式结果是否为 true。
package main
import "fmt"
func main() {
switch {
case false:
fmt.Println("1、case 条件语句为 false")
fallthrough
case false:
fmt.Println("2、case 条件语句为 false")
fallthrough
case true:
fmt.Println("3、case 条件语句为 true")
fallthrough
case false:
fmt.Println("4、case 条件语句为 false")
fallthrough
case false:
fmt.Println("5、case 条件语句为 false")
case false:
fmt.Println("6、case 条件语句为 false")
fallthrough
case true:
fmt.Println("7、case 条件语句为 true")
fallthrough
case false:
fmt.Println("8、case 条件语句为 false")
fallthrough
default:
fmt.Println("9、默认 case")
}
}
输出:
3、case 条件语句为 true
4、case 条件语句为 false
5、case 条件语句为 false
如果把 7 上面部分删去,输出:
7、case 条件语句为 true
8、case 条件语句为 false
9、默认 case
从以上代码输出的结果可以看出:switch 从第一个判断表达式为 true 的 case 开始执行, 如果 case 带有 fallthrough,程序会继续执行下一条 case,且它不会去判断下一个 case 的表达式是否为 true,执行完毕后停止。
select
select 是 Go 中的一个控制结构,类似于 switch 语句。
select 语句只能用于通道操作,每个 case 必须是一个通道操作,要么是发送要么是接收。
select 语句会监听所有指定的通道上的操作,一旦其中一个通道准备好就会执行相应的代码块。
如果多个通道都准备好,那么 select 语句会随机选择一个通道执行。如果所有通道都没有准备好,那么执行 default 块中的代码。
示例一:
package main
import "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")
}
}
}
无休止输出:
from 1
from 2
from 1
from 1
from 1
from 1
from 1
from 1
from 1
from 2
from 1
from 2
from 1
from 1
from 1
from 2
from 1
from 2
from 1
示例二:
package main
import (
"fmt"
"time"
)
func Chann(ch chan int, stopCh chan bool) {
var i int
i = 10
for j := 0; j < 10; j++ {
ch <- i
time.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)
fmt.Println("channel")
case s := <-ch:
fmt.Println("Receive", s)
case _ = <-stopCh:
goto end
}
}
end:
}
输出:
Receive 10
Receive 10
Receive 10
channel
Receive 10
Receive 10
channel
Receive 10
channel
Receive 10
channel
Receive 10
Receive 10
Receive 10
channel
循环语句
协程
协程中停止循环:
package main
import (
"fmt"
"time"
)
func process(ch chan int) {
for {
select {
case val := <-ch:
fmt.Println("Received value:", val)
// 执行一些逻辑
if val == 5 {
return // 提前结束 select 语句的执行
}
default:
fmt.Println("No value received yet.")
time.Sleep(500 * time.Millisecond)
}
}
}
func main() {
ch := make(chan int)
go process(ch)
time.Sleep(2 * time.Second)
ch <- 1
time.Sleep(1 * time.Second)
ch <- 3
time.Sleep(1 * time.Second)
ch <- 5
time.Sleep(1 * time.Second)
ch <- 7
time.Sleep(2 * time.Second)
}
输出:
No value received yet.
No value received yet.
No value received yet.
No value received yet.
Received value: 1
No value received yet.
No value received yet.
Received value: 3
No value received yet.
No value received yet.
Received value: 5
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan send]:
main.main()
G:/GoProject/go-toolbox/main.go:36 +0xe5
exit status 2
如果把上面的 return
换成 break
看一下,输出:
No value received yet.
No value received yet.
No value received yet.
No value received yet.
Received value: 1
No value received yet.
No value received yet.
Received value: 3
No value received yet.
No value received yet.
Received value: 5
No value received yet.
No value received yet.
Received value: 7
No value received yet.
No value received yet.
No value received yet.
No value received yet.
在 Go 语言中,break 语句在 select 语句中的应用是相对特殊的。由于 select 语句的特性, break 语句并不能直接用于跳出 select 语句本身,因为 select 语句是非阻塞的,它会一直等待所有的通信操作都准备就绪。 如果需要提前结束 select 语句的执行,可以使用 return 或者 goto 语句来达到相同的效果, 使用 return 语句会立即终止当前的函数执行。
标号
以下实例有多重循环,演示了使用标记和不使用标记的区别:
package main
import "fmt"
func main() {
// 不使用标记
fmt.Println("---- continue ---- ")
for i := 1; i <= 3; i++ {
fmt.Printf("i: %d\n", i)
for i2 := 11; i2 <= 13; i2++ {
fmt.Printf("i2: %d\n", i2)
continue
}
}
// 使用标记
fmt.Println("---- continue label ----")
re:
for i := 1; i <= 3; i++ {
fmt.Printf("i: %d\n", i)
for i2 := 11; i2 <= 13; i2++ {
fmt.Printf("i2: %d\n", i2)
continue re
}
}
}
输出:
---- continue ----
i: 1
i2: 11
i2: 12
i2: 13
i: 2
i2: 11
i2: 12
i2: 13
i: 3
i2: 11
i2: 12
i2: 13
---- continue label ----
i: 1
i2: 11
i: 2
i2: 11
i: 3
i2: 11
goto
Go 语言的 goto 语句可以无条件地转移到过程中指定的行。
goto 语句通常与条件语句配合使用。可用来实现条件转移, 构成循环,跳出循环体等功能。
但是,在结构化程序设计中一般不主张使用 goto 语句, 以免造成程序流程的混乱,使理解和调试程序都产生困难。
如,打印九九乘法表:
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 := 1
LOOP: if n <= m {
fmt.Printf("%dx%d=%d ",n,m,m*n)
n++
goto LOOP
} else {
fmt.Println("")
}
n++
}
}
输出:
1x1=1
1x2=2 2x2=4
1x3=3 2x3=6 3x3=9
1x4=4 2x4=8 3x4=12 4x4=16
1x5=5 2x5=10 3x5=15 4x5=20 5x5=25
1x6=6 2x6=12 3x6=18 4x6=24 5x6=30 6x6=36
1x7=7 2x7=14 3x7=21 4x7=28 5x7=35 6x7=42 7x7=49
1x8=8 2x8=16 3x8=24 4x8=32 5x8=40 6x8=48 7x8=56 8x8=64
1x9=9 2x9=18 3x9=27 4x9=36 5x9=45 6x9=54 7x9=63 8x9=72 9x9=81
函数
defer、panic、recover等,参阅 https://ibaiyang.github.io/blog/golang/2021/10/27/Go语言快速入门.html#八函数介绍
匿名函数、回调函数、闭包等,参阅 https://ibaiyang.github.io/blog/golang/2021/10/27/Go语言快速入门.html#十四高级函数
变量作用域
Go 语言程序中全局变量与局部变量名称可以相同,但是函数内的局部变量会被优先考虑。实例如下:
package main
import "fmt"
/* 声明全局变量 */
var g int = 20
func main() {
/* 声明局部变量 */
var g int = 10
fmt.Printf ("结果: g = %d\n", g)
}
输出:
结果: g = 10
数组
遍历数组:
package main
import "fmt"
func main() {
// 二维数组
var value = [3][2]int{\{\1, 2\}, \{3, 4}\, \{5, 6\}\}
// 遍历二维数组的其他方法,使用 range
// 其实,这里的 i, j 表示行游标和列游标
// v2 就是具体的每一个元素
// v 就是每一行的所有元素
for i, v := range value {
for j, v2 := range v {
fmt.Printf("value[%v][%v]=%v \t ", i, j, v2)
}
fmt.Print(v)
fmt.Println()
}
}
输出:
value[0][0]=1 value[0][1]=2 [1 2]
value[1][0]=3 value[1][1]=4 [3 4]
value[2][0]=5 value[2][1]=6 [5 6]
数组、切片、指针,函数调用区别:
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]
}
指针
**指针数组 **
可以看下这个底下的辩论,有益于思考 https://www.runoob.com/go/go-array-of-pointers.html
package main
import "fmt"
const max = 3
func main() {
var arr [3]int
var parr [3]*int // 指针数组
var p *[3]int = &arr // 数组指针
for k, _ := range arr {
parr[k] = &arr[k];
}
// 输出地址比对
for i := 0; i < 3; i+=1 {
fmt.Println(&arr[i], parr[i], &(*p)[i]);
}
}
输出:
0xc00009a000 0xc00009a000 0xc00009a000
0xc00009a008 0xc00009a008 0xc00009a008
0xc00009a010 0xc00009a010 0xc00009a010
指向指针的指针
package main
import "fmt"
func main(){
var a int = 1
var ptr1 *int = &a
var ptr2 **int = &ptr1
var ptr3 **(*int) = &ptr2 // 也可以写作:var ptr3 ***int = &ptr2
// 依次类推
fmt.Println("a:", a)
fmt.Println("ptr1", ptr1)
fmt.Println("ptr2", ptr2)
fmt.Println("ptr3", ptr3)
fmt.Println("*ptr1", *ptr1)
fmt.Println("**ptr2", **ptr2)
fmt.Println("**(*ptr3)", **(*ptr3)) // 也可以写作:***ptr3
}
输出:
a: 1
ptr1 0xc000016060
ptr2 0xc00000e028
ptr3 0xc00000e030
*ptr1 1
**ptr2 1
**(*ptr3) 1
向函数传递指针参数
函数中给指针变量赋值挺有意思,居然用的是*x = 100
,在函数中 *x
作为变量时是指针指向的地址,作为值时是指针指向的值。
package main
import "fmt"
func main() {
/* 定义局部变量 */
var a int = 100
var b int= 200
fmt.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) {
fmt.Println(x)
fmt.Println(*x)
var temp int
temp = *x /* 保存 x 地址的值 */
*x = *y /* 将 y 赋值给 x */
*y = temp /* 将 temp 赋值给 y */
fmt.Println(x)
fmt.Println(*x)
}
输出:
交换前 a 的值 : 100
交换前 b 的值 : 200
0xc000016060
100
0xc000016060
200
交换后 a 的值 : 200
交换后 b 的值 : 100
结构体
可以看下这个底下的评论,有益于思考 https://www.runoob.com/go/go-structures.html
tag 标记
结构体中属性的首字母大小写问题
首字母大写相当于 public。
首字母小写相当于 private。
注意: 这个 public 和 private 是相对于包(go 文件首行的 package 后面跟的包名)来说的。
定义的结构体如果只在当前包内使用,结构体的属性不用区分大小写。如果想要被其他的包引用,那么结构体的属性的首字母需要大写。
敲黑板,划重点:如当要将结构体对象转换为 JSON 时,对象中的属性首字母必须是大写,才能正常转换为 JSON。
示例一:
package main
import (
"encoding/json"
"fmt"
)
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,没有age):
{"Name":"小明"}
示例二:
type Person struct {
Name string //都是大写
Age int
}
输出(两个字段都有):
{"Name":"小明","Age":18}
那这样 JSON 字符串以后就只能是大写了么? 当然不是,可以使用 tag 标记要返回的字段名:
package main
import (
"encoding/json"
"fmt"
"time"
)
type Person struct {
Name string `json:"name"` //标记json名字为name
Age int `json:"age"`
Time int64 `json:"-"` // 标记忽略该字段
}
func main() {
person := Person{"小明", 18, time.Now().Unix()}
if result, err := json.Marshal(&person); err == nil {
fmt.Println(string(result))
}
}
输出:
{"name":"小明","age":18}
结构体指针
package main
import "fmt"
type Books struct {
title string
author string
subject string
book_id int
}
func main() {
var book = Books{"Go 入门到放弃", "作者", "go系列教程", 110119}
var b *Books
b = &book
fmt.Println(b) // &{Go 入门到放弃 作者 go系列教程 110119}
fmt.Println(*b) // {Go 入门到放弃 作者 go系列教程 110119}
fmt.Println(&b) // 0xc000088018
fmt.Println(book) // {Go 入门到放弃 作者 go系列教程 110119}
}
解释:
var b *Books // 就是说b这个指针是Books类型的。
b = &book // book是Books的一个实例化的结构,&book 就是把这个结构体的内存地址赋给了b,
*b // 那么在使用的时候,只要在b的前面加个*号,就可以把b这个内存地址对应的值给取出来了
&b // 就是取了b这个指针的内存地址,也就是b这个指针是放在内存空间的什么地方的。
book // 就是book这个结构体,打印出来就是它自己。也就是指针b前面带了*号的效果。
调用成员变量可以使用 变量名.成员名、指针名.成员名 都可以,相当于自动解引用,不需要c语言的 -> 符号。 GO语言的自动解引用只支持到一级指针,多级指针就要至少手动解引用至一级指针。看来GO是做了,但是没完全做。
package main
import "fmt"
type Books struct {
title string
}
func main() {
var book Books
book.title = "Go 语言"
printBook(book)
printBookByPoniter(&book)
// GO语言不支持形如 &&book 这样直接构造多级地址,需要使用变量来构造
p1 := &book
p2 := &p1
printBookByPoniter2(p2)
}
func printBook(book Books) {
// 传参的本质是为局部变量赋值,此book为本函数的局部变量,和外部的book是两个独立的变量。
fmt.Printf("Book title : %s\n", book.title)
}
func printBookByPoniter(book *Books) {
// 仍然遵循传参的本质是为局部变量赋值。此book的值是外部book的内存地址,可以由此间接操作外部变量。
// 在GO语言中,可以直接使用 指针.成员名 ,无需像C语言那样解引用。类似Rust的自动解引用。
fmt.Printf("Book title : %s\n", book.title)
}
func printBookByPoniter2(book **Books) {
// GO语言的自动解引用只支持到一级指针,多级指针就要至少手动解引用至一级指针,否则报错。看来GO是做了,但是没完全做。
fmt.Printf("Book title : %s\n", (*book).title)
}
输出:
Book title : Go 语言
Book title : Go 语言
Book title : Go 语言
切片(Slice)
Slice 是引用类型,如果将一个 Slice 传递给一个函数或赋值给另一个变量,它们都指向同一个底层数据结构, 因此对 Slice 的修改会影响到所有引用它的变量。
范围(Range)
range也可以用来枚举 Unicode 字符串。第一个参数是字符的索引,第二个是字符(Unicode的值)本身。
for i, c := range "go" {
fmt.Println(i, c)
}
输出:
0 103
1 111
range每轮循环中,键值对用的都是同一个地址:
package main
import "fmt"
func main() {
nums := [3]int{5, 6, 7}
for k, v := range nums {
fmt.Println("源值地址:", &nums[k], " \t key的地址:", &k, " \t value的地址:", &v)
}
}
输出:
源值地址: 0xc000014080 key的地址: 0xc000016060 value的地址: 0xc000016068
源值地址: 0xc000014088 key的地址: 0xc000016060 value的地址: 0xc000016068
源值地址: 0xc000014090 key的地址: 0xc000016060 value的地址: 0xc000016068
Map(集合)
Map 是引用类型,如果将一个 Map 传递给一个函数或赋值给另一个变量,它们都指向同一个底层数据结构, 因此对 Map 的修改会影响到所有引用它的变量。
递归函数
类型转换
接口类型转换
接口类型转换有两种情况:类型断言 和 类型转换。
类型断言
类型断言用于将接口类型转换为指定类型:
package main
import "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")
}
}
类型转换
类型转换用于将一个接口类型的值转换为另一个接口类型。 在类型转换中,我们必须保证要转换的值和目标接口类型之间是兼容的,否则编译器会报错。
package main
import "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)
}
接口
可以看下这个底下的评论,有益于思考 https://www.runoob.com/go/go-interfaces.html
错误处理
并发
Go 允许使用 go 语句开启一个新的运行期线程, 即 goroutine,以一个不同的、新创建的 goroutine 来执行一个函数。 同一个程序中的所有 goroutine 共享同一个地址空间。
通道
package main
import (
"fmt"
"time"
)
func put(c chan int) {
for i := 0; i < 5; i++ {
c <- i
time.Sleep(100 * time.Millisecond)
fmt.Println("->放入", i)
}
fmt.Println("=所有的都放进去了!关闭缓冲区,但是里面的数据不会丢失,还能取出。")
close(c)
}
func main() {
ch := make(chan int, 3)
go put(ch)
for {
time.Sleep(1000 * time.Millisecond)
data, ok := <-ch
if ok == true {
fmt.Println("<-取出", data)
} else {
break
}
}
}
输出:
->放入 0
->放入 1
->放入 2
<-取出 0
->放入 3
<-取出 1
->放入 4
=所有的都放进去了!关闭缓冲区,但是里面的数据不会丢失,还能取出。
<-取出 2
<-取出 3
<-取出 4
参考资料
Go语言RUNOOB教程 https://www.runoob.com/go/go-tutorial.html