您现在的位置是:首页 >技术交流 >Go语言基础语法网站首页技术交流

Go语言基础语法

242030 2024-10-28 12:01:05
简介Go语言基础语法

1、Go语言基础语法

1.1 Go标记

Go 程序可以由多个标记组成,可以是关键字,标识符,常量,字符串,符号。如以下 GO 语句由 6 个标记组成:

fmt.Println("Hello, World!")

6 个标记是(每行一个):

fmt
.
Println
(
"Hello, World!"
)

1.2 行分隔符

在 Go 程序中,一行代表一个语句结束。每个语句不需要像 C 家族中的其它语言一样以分号 ; 结尾,因为这些工

作都将由 Go 编译器自动完成。

如果你打算将多个语句写在同一行,它们则必须使用 ; 人为区分,但在实际开发中我们并不鼓励这种做法。

如以下为两个语句:

fmt.Println("Hello, World!")
fmt.Println("Hello Go!")

1.3 注释

注释不会被编译,每一个包应该有相关注释。

单行注释是最常见的注释形式,你可以在任何地方使用以 // 开头的单行注释。多行注释也叫块注释,均已以 /*

开头,并以 */ 结尾。如:

// 单行注释
/*
 Author
 我是多行注释
 */

1.4 标识符

标识符用来命名变量、类型等程序实体。一个标识符实际上就是一个或是多个字母(AZ和az)数字(0~9)、下划线_

组成的序列,但是第一个字符必须是字母或下划线而不能是数字。

以下是有效的标识符:

mahesh   kumar   abc   move_name   a_123
myname50   _temp   j   a23b9   retVal

以下是无效的标识符:

  • 1ab(以数字开头)
  • case(Go 语言的关键字)
  • a+b(运算符是不允许的)

1.5 字符串连接

Go 语言的字符串连接可以通过 + 实现。

package main

import "fmt"

func main() {
	// GoogleRunoob
	fmt.Println("Google" + "Runoob")
}

1.6 关键字

下面列举了 Go 代码中会使用到的 25 个关键字或保留字。

breakdefaultfuncinterfaceselect
casedefergomapstruct
chanelsegotopackageswitch
constfallthroughifrangetype
continueforimportreturnvar

除了以上介绍的这些关键字,Go 语言还有 36 个预定义标识符:

appendboolbytecapclosecomplexcomplex64complex128uint16
copyfalsefloat32float64imagintint8int16uint32
int32int64iotalenmakenewnilpanicuint64
printprintlnrealrecoverstringtrueuintuint8uintptr

程序一般由关键字、常量、变量、运算符、类型和函数组成。

程序中可能会使用到这些分隔符:括号 (),中括号 [] 和大括号 {}。

程序中可能会使用到这些标点符号:.,;:

1.7 Go 语言的空格

Go 语言中变量的声明必须使用空格隔开,如:

var age int;

语句中适当使用空格能让程序更易阅读。

无空格:

fruit=apples+oranges;

在变量与运算符间加入空格,程序看起来更加美观,如:

fruit = apples + oranges;

1.8 格式化字符串

Go 语言中使用 fmt.Sprintf 格式化字符串并赋值给新串。

package main

import (
	"fmt"
)

func main() {
	// go中格式化字符串并赋值给新串,使用fmt.Sprintf
	// %s表示字符串
	var stockcode = "000987"
	var enddate = "2020-12-31"
	var url = "Code=%s&endDate=%s"
	var target_url = fmt.Sprintf(url, stockcode, enddate)
	// Code=000987&endDate=2020-12-31
	fmt.Println(target_url)
	// 另外一个实例,%d表示整型
	const name, age = "Kim", 22
	s := fmt.Sprintf("%s is %d years old.
", name, age)
	// Kim is 22 years old.
	fmt.Println(s)
}

Go 可以使用 fmt.Sprintf 来格式化字符串,格式如下:

fmt.Sprintf(格式化样式, 参数列表…)
  • 格式化样式:字符串形式,格式化符号以 % 开头, %s 字符串格式,%d 十进制的整数格式。

  • 参数列表:多个参数以逗号分隔,个数必须与格式化样式中的个数一一对应,否则运行时会报错。

Go 字符串格式化符号:

格 式描 述
%v按值的本来值输出
%+v在 %v 基础上,对结构体字段名和值进行展开
%#v输出 Go 语言语法格式的值
%T输出 Go 语言语法格式的类型和值
%%输出 % 本体
%ttrue或false
%b整型以二进制方式显示(整数)或者是无小数部分,指数为二的幂的科学计数法(浮点数)
%o整型以八进制方式显示
%d整型以十进制方式显示
%x整型以十六进制、字母小写方式显示或者字符串或切片显示,每字节两个字符
%X整型以十六进制、字母大写方式显示或者字符串或切片显示,每字节两个字符
%c相应Unicode码所表示的字符
%q单引号围绕的字符字面值,由Go语言安全的转义(整数)或者是双引号围绕的字符串,由Go语言安全的转义(字符串)
%UUnicode 字符
%e科学计数法
%E科学计数法
%f浮点数
%g根据情况选择%e或%f产生更紧凑的无末尾0的输出
%G根据情况选择%E或%f产生更紧凑的无末尾0的输出
%p指针,十六进制方式显示
%s字符串或切片显示
+总打印数值的正负号;对于%q(%+q)保证只输出ASCII编码的字符
-在右侧而非左侧填充空格(左对齐该区域)
#备用格式:对八进制添加前导0(%#o),对十六进制添加前导0x(%#x),对%p(%#p)去掉前导0x

格式化例子:

package main

import (
	"fmt"
)

type point struct {
	x, y int
}

func main() {
	p := point{1, 2}
	// {1 2}
	fmt.Printf("%v
", p)
	// {x:1 y:2}
	fmt.Printf("%+v
", p)
	// main.point{x:1, y:2}
	fmt.Printf("%#v
", p)
	// main.point
	fmt.Printf("%T
", p)
	// 100%
	fmt.Printf("%d%%
", 100)
	// true
	fmt.Printf("%t
", true)
	// 123
	fmt.Printf("%d
", 123)
	// 1110
	fmt.Printf("%b
", 14)
	// 7886928847432581p-49
	fmt.Printf("%b
", 14.01)
	// 16
	fmt.Printf("%o
", 14)
	// 14
	fmt.Printf("%d
", 14)
	// e
	fmt.Printf("%x
", 14)
	// E
	fmt.Printf("%X
", 14)
	// 616263
	fmt.Printf("%x
", "abc")
	// 616263
	fmt.Printf("%X
", "abc")
	// c
	fmt.Printf("%c
", 'c')
	// 'a'
	fmt.Printf("%q
", 'a')
	// "abc"
	fmt.Printf("%q
", "abc")
	// U+0061
	fmt.Printf("%U
", 'a')
	// 1.234000e+08
	fmt.Printf("%e
", 123400000.0)
	// 1.234000E+08
	fmt.Printf("%E
", 123400000.0)
	// 78.900000
	fmt.Printf("%f
", 78.9)
	// 78.9
	fmt.Printf("%g
", 78.9)
	// 78.9
	fmt.Printf("%G
", 78.9)
	// 0xc0000160b0
	fmt.Printf("%p
", &p)
	// "abc"
	fmt.Printf("%s
", ""abc"")
	// +的演示
	fmt.Printf("%+d
", 10)
	fmt.Printf("%+d
", -10)
	// '我'
	fmt.Printf("%q
", '我')
	// "a我们b"
	fmt.Printf("%q
", "a我们b")
	// 'u6211'
	fmt.Printf("%+q
", '我')
	// "au6211u4eecb"
	fmt.Printf("%+q
", "a我们b")
	// -的演示
	// 默认右对齐
	// |    12|   345|
	fmt.Printf("|%6d|%6d|
", 12, 345)
	// |  1.20|  3.45|
	fmt.Printf("|%6.2f|%6.2f|
", 1.2, 3.45)
	// 加-表示左对齐
	// |1.20  |3.45  |
	fmt.Printf("|%-6.2f|%-6.2f|
", 1.2, 3.45)
	// 默认右对齐
	//|   foo|     b|
	fmt.Printf("|%6s|%6s|
", "foo", "b")
	// 加-表示左对齐
	// |foo   |b     |
	fmt.Printf("|%-6s|%-6s|
", "foo", "b")
	// #的演示
	// 016
	fmt.Printf("%#o
", 14)
	// 0xe
	fmt.Printf("%#x
", 14)
	// c0000160b0
	fmt.Printf("%#p
", &p)
}

宽度和精度:

宽度是%之后的值,如果没有指定,则使用该值的默认值,精度是跟在宽度之后的值,如果没有指定,也使用要打

印的值的默认精度。%9.2f,宽度9,精度2

%fdefault width,default precision
%9fwidth 9,default precision
%.2fdefault width,precision 2
%9.2fwidth 9,precision 2
%9.fwidth 9,precision 0

对数值而言,宽度为该数值占用区域的最小宽度;精度为小数点之后的位数。但对于 %g/%G 而言,精度为所有数

字的总数。例如,对于123.45,格式 %6.2f会打印123.45,而 %.4g 会打印123.5。%e 和 %f 的默认精度为6;但

对于 %g 而言,它的默认精度为确定该值所必须的最小位数。

对大多数值而言,宽度为输出的最小字符数,如果必要的话会为已格式化的形式填充空格。对字符串而言,精度为

输出的最大字符数,如果必要的话会直接截断。

宽度是指必要的最小宽度,若结果字符串的宽度超过指定宽度时, 指定宽度就会失效。

1.9 Go 程序的一般结构

// 当前程序的包名
package main

// 导入其他包
import . "fmt"

// 常量定义
const PI = 3.14

// 全局变量的声明和赋值
var name = "gopher"

// 一般类型声明
type newType int

// 结构的声明
type gopher struct{}

// 接口的声明
type golang interface{}

// 由main函数作为程序入口点启动
func main() {
	// Hello World!
	Println("Hello World!")
}
  • Go 程序是通过 package 来组织的。

  • 只有 package 名称为 main 的源码文件可以包含 main 函数。

  • 一个可执行程序有且仅有一个 main 包。

  • 通过 import 关键字来导入其他非 main 包。

可以通过 import 关键字单个导入:

import "fmt"
import "io"

也可以同时导入多个:

import (
    "fmt"
    "math"
)

使用 <PackageName>.<FunctionName> 调用:

package main

import (
	"fmt"
	"math"
)

func main() {
	// 1024
	fmt.Println(math.Exp2(10))
}

package别名:

// 为fmt起别名为fmt2
import fmt2 "fmt"

省略调用(不建议使用):

// 调用的时候只需要Println(),而不需要fmt.Println()
import . "fmt"

前面加个点表示省略调用,那么调用该模块里面的函数,可以不用写模块名称了:

package main

import . "fmt"

func main() {
	// hello,world
	Println("hello,world")
}
  • 通过 const 关键字来进行常量的定义。

  • 通过在函数体外部使用 var 关键字来进行全局变量的声明和赋值。

  • 通过 type 关键字来进行结构(struct)和接口(interface)的声明。

  • 通过 func 关键字来进行函数的声明。

可见性规则:Go语言中,使用大小写来决定该常量、变量、类型、接口、结构或函数是否可以被外部包所调用。

函数名首字母小写即为 private :

func getId() {}

函数名首字母大写即为 public :

func Printf() {}

Go 语言的包引入一般为: 项目名/包名

import "test/controllers"

方法的调用为: 包名.方法名()

controllers.Test()

本包内方法名可为小写,包外调用方法名首字母必须为大写。

1.10 Go fmt 库

1.10.1 Printing

fmt包实现了格式化I/O函数,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)

实例:

package main

import "fmt"

func main() {
	// ab1 2 3cd
	fmt.Print("a", "b", 1, 2, 3, "c", "d", "
")
	// a b 1 2 3 c d
	fmt.Println("a", "b", 1, 2, 3, "c", "d")
	// ab 1 2 3 cd
	fmt.Printf("ab %d %d %d cd
", 1, 2, 3)
}

以下三个函数功能同上面三个函数,只不过将转换结果写入到 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)

实例:

package main

import (
	"fmt"
	"os"
)

func main() {
	// ab1 2 3cd
	fmt.Fprint(os.Stdout, "a", "b", 1, 2, 3, "c", "d", "
")
	// a b 1 2 3 c d
	fmt.Fprintln(os.Stdout, "a", "b", 1, 2, 3, "c", "d")
	// ab 1 2 3 cd
	fmt.Fprintf(os.Stdout, "ab %d %d %d cd
", 1, 2, 3)
}

以下三个函数功能同上面三个函数,只不过将转换结果以字符串形式返回。

func Sprint(a ...interface{}) string
func Sprintln(a ...interface{}) string
func Sprintf(format string, a ...interface{}) string

实例:

package main

import (
	"fmt"
)

func main() {
	// ab1 2 3cd
	var s1 = fmt.Sprint("a", "b", 1, 2, 3, "c", "d", "
")
	fmt.Print(s1)
	// a b 1 2 3 c d
	var s2 = fmt.Sprintln("a", "b", 1, 2, 3, "c", "d")
	fmt.Print(s2)
	// ab 1 2 3 cd
	var s3 = fmt.Sprintf("ab %d %d %d cd
", 1, 2, 3)
	fmt.Print(s3)
}

以下函数功能同 Sprintf() 函数,只不过结果字符串被包装成了 error 类型。

func Errorf(format string, a ...interface{}) error

实例:

package main

import (
	"fmt"
)

func main() {
	var error = fmt.Errorf("ab %d %d %d cd
", 1, 2, 3)
	// ab 1 2 3 cd
	fmt.Print(error)
}

不考虑占位符的话,如果操作数是接口值,就会使用其内部的具体值,而非接口本身。 因此:

var i interface{} = 23
# 23
fmt.Printf("%v
", i)

若其格式(它对于Println等函数是隐式的%v)对于字符串是有效的(%s %q %v %x %X),以下两条规则也适用:

  • 若一个操作数实现了 error 接口,Error 方法就能将该对象转换为字符串,随后会根据占位符的需要进行格

    式化。

  • 若一个操作数实现了 String() string 方法,该方法能将该对象转换为字符串,随后会根据占位符的需要进

    行格式化。

1.10.2 Formatter 接口

Formatter 由自定义类型实现,用于实现该类型的自定义格式化过程。

当格式化器需要格式化该类型的变量时,会调用其 Format 方法。

Formatter接口的定义如下:

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
}

通过实现Formatter接口可以做到自定义输出格式(自定义占位符):

package main

import (
	"bytes"
	"fmt"
	"strconv"
)

type Person struct {
	Name string
	Age  int
	Sex  int
}

func (person *Person) String() string {
	buffer := bytes.NewBufferString("This is ")
	buffer.WriteString(person.Name + ", ")
	if person.Sex == 0 {
		buffer.WriteString("He ")
	} else {
		buffer.WriteString("She ")
	}
	buffer.WriteString("is ")
	buffer.WriteString(strconv.Itoa(person.Age))
	buffer.WriteString(" years old.")
	return buffer.String()
}

func (this *Person) Format(f fmt.State, c rune) {
	if c == 'L' {
		f.Write([]byte(this.String()))
		f.Write([]byte(" Person has three fields."))
	} else {
		// 没有此句,会导致 fmt.Printf("%s", p) 啥也不输出
		f.Write([]byte(fmt.Sprintln(this.String())))
	}
	f.Write([]byte("
"))
}

func main() {
	p1 := &Person{"polaris", 28, 0}
	// This is polaris, He is 28 years old. Person has three fields.
	fmt.Printf("%L", p1)
	p2 := &Person{"marry", 38, 1}
	// This is marry, She is 38 years old.
	fmt.Printf("%s", p2)
}

这里需要解释以下几点:

1)、fmt.State是一个接口,由于Format方法是被fmt包调用的,它内部会实例化好一个fmt.State接口的实

例,我们不需要关心该接口。

2)、可以实现自定义占位符,同时fmt包中和类型相对应的预定义占位符会无效。因此例子中Format的实现加

上了else子句。

3)、实现了Formatter接口,相应的Stringer接口不起作用。但实现了Formatter接口的类型应该实现Stringer

接口,这样方便在Format方法中调用String()方法。就像本例的做法。

4)、Format方法的第二个参数是占位符中%后的字母(有精度和宽度会被忽略,只保留字母)。

一般地,我们不需要实现Formatter接口。

1.10.3 Stringer 接口

Stringer 由自定义类型实现,用于实现该类型的自定义格式化过程。

当格式化器需要输出该类型的字符串格式时就会调用其 String 方法。

Stringer接口的定义如下:

type Stringer interface {
    String() string
}

根据Go语言中实现接口的定义,一个类型只要有 String() string 方法,我们就说它实现了Stringer接口。

如果格式化输出某种类型的值,只要它实现了String()方法,那么会调用String()方法进行处理。

实例:

package main

import (
	"bytes"
	"fmt"
	"strconv"
)

type Person struct {
	Name string
	Age  int
	Sex  int
}

func (person *Person) String() string {
	buffer := bytes.NewBufferString("This is ")
	buffer.WriteString(person.Name + ", ")
	if person.Sex == 0 {
		buffer.WriteString("He ")
	} else {
		buffer.WriteString("She ")
	}
	buffer.WriteString("is ")
	buffer.WriteString(strconv.Itoa(person.Age))
	buffer.WriteString(" years old.")
	return buffer.String()
}

func main() {
	p1 := &Person{"polaris", 28, 0}
	// This is polaris, He is 28 years old.
	fmt.Println(p1)
	p2 := &Person{"marry", 38, 1}
	// This is tom, She is 38 years old.
	fmt.Println(p2)
}

可见,Stringer接口和Java中的ToString方法类似。

1.10.4 GoStringer 接口

GoStringer 接口定义如下:

GoStringer由自定义类型实现,用于实现该类型的自定义格式化过程。

当格式化器需要输出该类型的 Go 语法字符串(%#v)时就会调用其 GoString方法。

type GoStringer interface {
    GoString() string
}

实例:

package main

import (
	"fmt"
	"strconv"
)

type Person struct {
	Name string
	Age  int
	Sex  int
}

func (this *Person) GoString() string {
	return "&Person{Name is " + this.Name + ", Age is " + strconv.Itoa(this.Age) + ", Sex is " + strconv.Itoa(this.Sex) + "}"
}

func main() {
	p := &Person{"polaris", 28, 0}
	// &Person{Name is polaris, Age is 28, Sex is 0}
	fmt.Printf("%#v", p)
}

一般的,我们不需要实现该接口。

1.10.5 格式化综合实例

实例:

package main

import (
	"fmt"
	"strconv"
	"strings"
)

type Ustr string

func (us Ustr) String() string {
	return strings.ToUpper(string(us))
}

func (us Ustr) GoString() string {
	return `"` + strings.ToUpper(string(us)) + `"`
}

// c会输出格式化字符,如m,M,v,s,v,d
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:] {
			// 通过Flag方法获取占位符中的旗标[+- 0#]是否被设置
			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" 标记不能同时存在
	// 旗标:[+- #] | 宽度:8 | 精度:5
	fmt.Printf("%-+ 0#8.5m
", u)
	// 旗标:[+ 0#] | 宽度:8 | 精度:5
	fmt.Printf("%+ 0#8.5M
", u)
	// 会调用String()
	// HELLO WORLD!
	fmt.Println(u)
	// HELLO WORLD!
	fmt.Printf("%s
", u)
	// "HELLO WORLD!"
	fmt.Printf("%#v
", u)
	// 无效格式:d
	fmt.Printf("%d
", u)
}

1.10.6 Scanning

Scan 从标准输入中读取数据,并将数据用空白分割并解析后存入 a 提供的变量中(换行符会被当作空白处理),

变量必须以指针传入。

当读到 EOF 或所有变量都填写完毕则停止扫描。

返回成功解析的参数数量。

func Scan(a ...interface{}) (n int, err error)

ScanlnScan 类似,只不过遇到换行符就停止扫描。

func Scanln(a ...interface{}) (n int, err error)

Scanf 从标准输入中读取数据,并根据格式字符串 format 对数据进行解析,将解析结果存入参数 a 所提供的变

量中,变量必须以指针传入。

输入端的换行符必须和 format 中的换行符相对应(如果格式字符串中有换行符,则输入端必须输入相应的换行

符)。

占位符 %c 总是匹配下一个字符,包括空白,比如空格符、制表符、换行符。

返回成功解析的参数数量。

func Scanf(format string, a ...interface{}) (n int, err error)

实例:

package main

import "fmt"

// 对于Scan而言,回车视为空白
func main() {
	a, b, c := "", 0, false
	fmt.Scan(&a, &b, &c)
	fmt.Println(a, b, c)
	// 在终端执行后,输入 abc 1 回车 true 回车
	// 结果 abc 1 true
}
package main

import "fmt"

// 对于Scanln而言,回车结束扫描
func main() {
	a, b, c := "", 0, false
	fmt.Scanln(&a, &b, &c)
	fmt.Println(a, b, c)
	// 在终端执行后,输入 abc 1 true 回车
	// 结果 abc 1 true
}
package main

import "fmt"

// 格式字符串可以指定宽度
func main() {
	a, b, c := "", 0, false
	fmt.Scanf("%4s%d%t", &a, &b, &c)
	fmt.Println(a, b, c)
	// 在终端执行后,输入 1234567true 回车
	// 结果 1234 567 true
}

以下三个函数功能同上面三个函数,只不过从 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)
  • ScanScanfScanlnos.Stdin中读取;

  • FscanFscanfFscanln从指定的io.Reader中读取;

  • SscanSscanfSscanln从实参字符串中读取;

  • ScanlnFscanlnSscanln在换行符处停止扫描,且需要条目紧随换行符之后;

  • ScanfFscanfSscanf需要输入换行符来匹配格式中的换行符;

  • ScanFscanSscan将换行符视为空格;

ScanfFscanfSscanf根据格式字符串解析实参,类似于Printf,实例:

package main

import "fmt"

func main() {

	// Sscan、Sscanf和Sscanln从实参字符串中读取
	var (
		name1 string
		name2 string
		name3 string
		name4 string
		name5 string
		name6 string
		age1  int
		age2  int
		age3  int
		age4  int
		age5  int
		age6  int
	)
	// 可以将"polaris 28"中的空格换成"
"试试
	n1, _ := fmt.Sscan("polaris 28", &name1, &age1)
	n2, _ := fmt.Sscan("polaris
28", &name2, &age2)
	// 2 polaris 28
	fmt.Println(n1, name1, age1)
	// 2 polaris 28
	fmt.Println(n2, name2, age2)

	// 可以将"polaris 28"中的空格换成"
"试试
	n3, _ := fmt.Sscanf("polaris 28", "%s%d", &name3, &age3)
	n4, _ := fmt.Sscanf("polaris
28", "%s%d", &name4, &age4)
	// 2 polaris 28
	fmt.Println(n3, name3, age3)
	// 1 polaris 0
	fmt.Println(n4, name4, age4)

	// 可以将"polaris 28"中的空格换成"
"试试
	n5, _ := fmt.Sscanln("polaris 28", &name5, &age5)
	n6, _ := fmt.Sscanln("polaris
28", &name6, &age6)
	// 2 polaris 28
	fmt.Println(n5, name5, age5)
	// 1 polaris 0
	fmt.Println(n6, name6, age6)
}

实例:

package main

import "fmt"

func main() {
	for i := 0; i < 2; i++ {
		var name string
		fmt.Print("Input Name:")
		n, err := fmt.Scanf("%s", &name)
		fmt.Println(n, err, name)
	}
}
# 输入第一个元素zsx1然后按回车输入第二个元素,会出现如下现象
Input Name:1 <nil> zsx1
Input Name:0 unexpected newline

同样的代码在Linux下正常,这可能是go在Windows下的一个bug。

目前的解决方法是:换用Scanln或者改为Scanf("%s ", &name)或者使用Scan

正常的结果为:

# 输入zsx1按回车然后输入zsx2
Input Name:1 <nil> zsx1
Input Name:1 <nil> zsx2

1.10.7 Scanner 和 ScanState 接口

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)
}

基本上,我们不会去自己实现这两个接口,只需要使用上文中相应的Scan函数就可以了。

任何实现了Scan方法的对象都实现了Scanner接口,Scan方法会从输入读取数据并将处理结果存入接收端,接收

端必须是有效的指针。Scan方法会被任何Scan、Scanf、Scanln等函数调用,只要对应的参数实现了该方法。

Scan方法接收的第一个参数为ScanState接口类型。

ScanState是一个交给用户定制的Scanner接口的参数的接口。Scanner接口可能会进行一次一个字符的扫描或者

要求ScanState去探测下一个空白分隔的token。

实例:

package main

import "fmt"

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
}

1.11 结构体格式化输出

在软件系统中定位问题时日志不可或缺,但是当一个系统功能繁多,需要打印的日志也多如牛毛,此时为了提高我

们浏览日志的效率,便于阅读的输出格式必不可少。

打印结构体是打印日志时最长见的操作,但是当结构体内容较多都在一行时,不易于阅读。在 Go 中结构体可以方

便的转为 JSON,因此我们可以借助 JSON 完成对 struct 的格式化输出。

1.11.1 输出结构体字段(%+v)

package main

import (
	"fmt"
)

// Student 学生信息
type Student struct {
	Name string
	Addr HomeInfo
	M    map[string]string
}

// HomeInfo 家庭住址
type HomeInfo struct {
	Province     string
	City         string
	County       string
	Street       string
	DetailedAddr string
}

var student = Student{
	Name: "dablelv",
	Addr: HomeInfo{
		Province:     "Guangdong",
		City:         "Shenzhen",
		County:       "Baoan",
		Street:       "Xixiang",
		DetailedAddr: "Shengtianqi",
	},
	M: map[string]string{
		"hobby": "pingpopng",
	},
}

func main() {
    // student={Name:dablelv Addr:{Province:Guangdong City:Shenzhen County:Baoan Street:Xixiang DetailedAddr:Shengtianqi} M:map[hobby:pingpopng]}
	fmt.Printf("student=%+v
", student)
}

这种输出方式虽然可以将结构体的字段名称打印出来,但是当结构体字段多,嵌套层次深时,扎堆输出在一

行时不便于阅读。

1.11.2 输出格式化 JSON 串

package main

import (
	"bytes"
	"encoding/json"
	"fmt"
)

// Student 学生信息
type Student struct {
	Name   string
	Addr   HomeInfo
	M      map[string]string
}

// HomeInfo 家庭住址
type HomeInfo struct {
	Province     string
	City         string
	County       string
	Street       string
	DetailedAddr string
}

var student = Student{
	Name: "dablelv",
	Addr: HomeInfo{
		Province:     "Guangdong",
		City:         "Shenzhen",
		County:       "Baoan",
		Street:       "Xixiang",
		DetailedAddr: "Shengtianqi",
	},
	M: map[string]string{
		"hobby": "pingpopng",
	},
}

func main() {
	bs, _ := json.Marshal(student)
	var out bytes.Buffer
	json.Indent(&out, bs, "", "	")
	/*
	student={
        "Name": "dablelv",
        "Addr": {
                "Province": "Guangdong",
                "City": "Shenzhen",
                "County": "Baoan",
                "Street": "Xixiang",
                "DetailedAddr": "Shengtianqi"
        },
        "M": {
                "hobby": "pingpopng"
        }
	}
	*/
	fmt.Printf("student=%v
", out.String())
}

将struct 转化为 json串后再格式化输出,大大增加了可读性。

1.11.3 使用 go-huge-util

package main

import (
	"fmt"
	huge "github.com/dablelv/go-huge-util"
)

// Student 学生信息
type Student struct {
	Name   string
	Addr   HomeInfo
	M      map[string]string
}

// HomeInfo 家庭住址
type HomeInfo struct {
	Province     string
	City         string
	County       string
	Street       string
	DetailedAddr string
}

var student = Student{
	Name: "dablelv",
	Addr: HomeInfo{
		Province:     "Guangdong",
		City:         "Shenzhen",
		County:       "Baoan",
		Street:       "Xixiang",
		DetailedAddr: "Shengtianqi",
	},
	M: map[string]string{
		"hobby": "pingpopng",
	},
}

func main() {
	s, _ := huge.ToIndentJSON(&student)
    /*
    student={
        "Name": "dablelv",
        "Addr": {
                "Province": "Guangdong",
                "City": "Shenzhen",
                "County": "Baoan",
                "Street": "Xixiang",
                "DetailedAddr": "Shengtianqi"
        },
        "M": {
                "hobby": "pingpopng"
        }
	}
    */
	fmt.Printf("student=%v
", s)
}
风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。