Go语言RUNOOB教程


Go语言RUNOOB教程


正文

环境安装

结构

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


返回