流就是数据在数据源和程序之间经历的路径。数据源可以是文件数据库或者键盘输入等,程序是运行在内存中的应用。
数据从数据源输入到程序的路径为输入流,从内存输出到数据源的路径为输出流。
流是以内存为核心,输入到内存就是输入流,将数据内存持久化就是输出流。
计算机中的流其实是一种信息的转换。它是一种有序流,因此相对于某一对象,通常我们把对象接收外界的信息输入(Input)称为输入流,相应地从对象向外输出(Output)信息为输出流,合称为输入/输出流(I/O Streams)。对象间进行信息或者数据的交换时总是先将对象或数据转换为某种形式的流,再通过流的传输,到达目的对象后再将流转换为对象数据。所以,可以把流看作是一种数据的载体,通过它可以实现数据交换和传输。
针对计算机来说流的基本单位为字节流,而对程序来说,程序还可以识别字符流。字节流是面向计算机的,字符流是面向应用的。
程序也是通过流与计算机实现数据交换,一般编程语言都提供了内置的流(I/O)接口。
Java I/O
在计算机软件工作的过程成需要大量使用I/O,例如软件的本地数据库,存储了用户的缓存信息等,流是数据持久化的桥梁。将程序中即内存中的数据写入到显示器或者写入到文件系统。
键盘的输入输出
//输出
fmt.Println() //打印换行
fmt.Print() //打印
fmt.Printf() //格式化打印
//输入
fmt.Scanf()
fmt.Scanln()
fmt.Scan()
格式化输入输出(一般带f)
%v 值的默认格式表示
%T 值的类型的Go语法表示
%t 单词true或false
%d 表示为十进制
%f 有小数部分但无指数部分,如123.456
%s 直接输出字符串或者[]byte
fmt.Printf(format string, a ...interface{}) (n int, err error)
var a = "test"
var b = Student{}
var c = "10"
fmt.Printf("默认输出%v,类型输出%T,十进制输出%d,字符输出%s", a, b, c, a)
fmt.Scanf(format string, a ...any) (n int, err error)
在格式化输入时,需要注意以下几点:
- 输入的格式和书写的格式要一致,在格式化符处对应即可
- 输入时必须用地址接收,节约内存
- 书写时格式化符没有用标点符号隔开的,输入是默认用空格隔开
//3特性
var a1 int
var b1 string
fmt.Scanf("%d,%s\n", &a1, &b1)
fmt.Scanf("%d%s\n", &a1, &b1)
输入时第一个用”,“隔开,第二个用空格隔开。
//特性1
fmt.Scanf("a=%d,b=%s\n", &a1, &b1)
fmt.Scanf("输入一个十进制数%d,输入一个字符串%s\n", &a1, &b1)
标准输入输出
func Println(a ...interface{}) (n int, err error)
a1 := 10
b1 := "qwe"
fmt.Println(a1, b1)
Println方法标准化输出时相邻参数的输出之间添加空格并在输出结束后添加换行符,Print不会自动换行。
func Scan(a ...interface{}) (n int, err error)
Scan从标准输入扫描文本,将成功读取的空白分隔的值保存进成功传递给本函数的参数。(注意只能用空格隔开输入,在书写也只能用”,“隔开变量地址接收)
var a1 int
var b1 string
fmt.Scan(&a1,&b1)
fmt.Print(a1, b1)
fmt
包其他格式化I/O,详见中文开发手册
文件操作
file, err := os.Open("D:\\Go\\Go Files\\unit5\\src\\test\\hello.txt")
//file, err := os.Open("D:/Go/Go Files/unit5/src/test/hello.txt")
//file, err := os.Open("../test/hello.txt")
if err != nil {
fmt.Println("file nnot found")
return
}
defer file.Close()
reader := bufio.NewReader(file)
for {
str, err := reader.ReadString('\n')
if err == io.EOF {
break
}
fmt.Print(str)
}
对于Go来说没有严格的/
和\\
区分,均可以读取。
filebyte, err := ioutil.ReadFile("D:\\Go\\Go Files\\unit5\\src\\test\\hello.txt")
if err != nil {
fmt.Println("read file err=", err)
}
fmt.Printf("%v", string(filebyte))
filepath := "./abc.txt"
file, err := os.OpenFile(filepath, os.O_RDONLY|os.O_CREATE, 0666)
if err != nil {
fmt.Printf("open file err=%v\n", err)
return
}
defer file.Close()
str := "hello,gardon\n"
writer := bufio.NewWriter(file)
//循环写入几行
for j := 0; j < len(str); j++ {
writer.WriteString(str)
}
writer.Flush()
在Go语言中,一条件下某些结构体并不需要多次创建,只需要单一的实例即可即工厂模式或者单例模式。例如在数据的datasource
创建时的工厂模式单例即可。
在Go语言中,没有像Java的static关键字,如需要单例模式只需要通过指针来操作即可,即返回变量的地址。
package main
import "fmt"
func main() {
//
var student = StudentFactory(1, "xuwenhui")
fmt.Println((*student).id, (*student).name)
}
type student struct {
id int
name string
}
func StudentFactory(id int, name string) *student {
return &student{
id: id,
name: name,
}
}
在上面代码中student
结构体要设为单例模式,需要设置访问权限,通过公开访问方法StudentFactory
返回结构体的地址,并用指针操作结构体,实现工厂模式。
在Go的多态特性中,不同的特性需要使用断言来调用不同的方法。
type Person struct {
name string
sex string
}
type Student struct {
person Person
sid string
}
// 需要一个父类是两个结构体的父接口
// 空接口是任何类型的父类
type Per interface {
}
在上述代码中需要一个参数能同时接收Person和Student类,同时调用其方法,Go语言中,通过断言
来决策。
断言能够将子类用父类接收实现多态性,具体用法是变量.(类型)
。
func main() {
var a Per
var b = a.(Person)
fmt.Println(b.name, b.sex)
var c = a.(Student)
fmt.Println(c.person.name, c.person.sex, c.sid)
}
type Person struct {
name string
sex string
}
type Student struct {
person Person
sid string
}
// 需要一个父类是两个结构体的父接口
// 空接口是任何类型的父类
type Per interface {
}
在上面代码中通过断言将a接口转化为任意类型(Person和Student)。使用断言可以将任意已知类型转化,并调用转化后的成员与方法。
package main
import "fmt"
func main() {
// var a Per
// var b = a.(Person)
// fmt.Println(b.name, b.sex)
// b.showPn()
// var c = a.(Student)
// fmt.Println(c.person.name, c.person.sex, c.sid)
// c.showStu()
stu1 := Student{
person: Person{
"xu",
"nan",
},
sid: "s001",
}
identufy(stu1)
}
type Person struct {
name string
sex string
}
func (pn Person) showPn() {
fmt.Println(pn.name, pn.sex)
}
type Student struct {
person Person
sid string
}
func (stu Student) showStu() {
fmt.Println(stu.person.name, stu.person.sex, stu.sid)
}
// 需要一个父类是两个结构体的父接口
// 空接口是任何类型的父类
type Per interface {
}
func identufy(per Per) {
//通过断言做类的变换
var x = per.(Student)
fmt.Println(x.person.name, x.person.sex, x.sid)
x.person.showPn()
x.showStu()
}
在上面代码中identufy
方法接收Per
的参数,使用断言将参数转化为已指对象Student
,就可以使用其成员与方法。同时也可以接收Person参数,或者接收任意已知类型。
在多环境开发的情况下,需要更改环境便于测试,因为不同的环境配置不一样,在代码内部可以直接通过读取不同的配置文件来实现,但是当代码打包后就无法通过更改代码来实现了,需要通过外部的参数。
Go的os命令提供了os.Args
切片来存储所有命令行参数。
import (
"fmt"
"os"
)
func main() {
//输出命令行参数长度
fmt.Println("命令行参数长度", len(os.Args))
//循环输出参数
for index, value := range os.Args {
fmt.Printf("args[%v]=%v\n", index, value)
}
}
需要注意的是参数以空格分离。
命令参数解析
flag包实现了命令行参数的解析。
os.Args
可以满足一些基本的参数要求,但是复杂的在使用改参数就不太方便了,Go也提供了flag
包实现了命令行参数的解析。
var usr, pwd, host string
var port int
flag.StringVar(&usr, "user", "", "用户名")
flag.StringVar(&pwd, "password", "", "密码")
flag.StringVar(&host, "h", "localhost", "主机名")
flag.IntVar(&port, "port", 3306, "端口")
flag.Parse()
fmt.Printf("usr=%s,pwd=%s,host=%s,port=%d", usr, pwd, host, port)
flag
的var系列的函数将参数绑定到指定的变量上,通过变量输入没有顺序要求。var系列函数有一般有四个参数第一参数为程序的变量,第二个参数为绑定输入变量参数,第三个为默认值,第四个参数为绑定参数描述。
命令行输入时通过-绑定参数 数值
输入。
序列化
Go语言对JSON操作的包均在encoding/json
包下,包下提供了Marshal
方法来序列化Go内置对象。Marshal函数返回v的json编码。
func Marshal(v interface{}) ([]byte, error)
//创建结构体
type Person struct {
name string
address string
}
//序列化
per := Person{
name: "钢铁侠",
address: "漫威",
}
perjson, err := json.Marshal(&per)
if err != nil {
panic(err)
}
fmt.Printf("per序列化的json数据为为:%v", perjson)
fmt.Printf("per序列化的json字符串为:%v", string(perjson))
上面代码中序列化了一个Person对象,但打印时却打印失败,这是由于结构体成员都是小写的,只能在本包内访问,序列化没有意义,也不支持私有类的序列化。
type Person struct {
Name string
Address string
}
不同于Java一切皆对象的性质,Go中除了结构体还有map,数组,切片等。
//定义一个map类型
func initMap() map[string]string {
var tmp map[string]string = map[string]string{}
tmp["1"] = "aaa"
tmp["2"] = "bbb"
tmp["3"] = "ccc"
return tmp
}
//对map序列化
a := initMap()
ajson, err := json.Marshal(&a)
if err != nil {
panic("ajson序列化失败")
}
fmt.Printf("%v\n", ajson)
fmt.Println(string(ajson))
//输出序列化时的数据类型
fmt.Printf("%T\n", ajson)
fmt.Printf("%T\n", string(ajson))
可以看出序列化后时一个字节数组,通过string()函数将其转化为字符串,即为json字符串。
Go序列化时的tag标签的使用
在序列化时成员变量的名称均是首字母大写,然而在实际使用时需要统一书写规范,需要特定的名称,这就需要使用序列化时的tag标签。
{"Name":"钢铁侠","Address":"漫威"}
Go语言结构体序列化tag
使用规则是在结构体字段后添加json:"name"
并用反引号包裹。
type Person struct {
Name string `json:"person_name"`
Address string `json:"person_address"`
Age int
}
在Name和Address使用了tag那么其序列化后的名称会变为自定义的person_name,Age成员没有使用tag因此不会改变。
per := Person{
Name: "钢铁侠",
Address: "漫威",
Age: 18,
}
perjson, err := json.Marshal(&per)
if err != nil {
panic(err)
}
fmt.Printf("per序列化的json数据为为:%v", perjson)
fmt.Printf("per序列化的json字符串为:%v", string(perjson))
struct的tag只会在序列化时起作用不会影响结构体的使用。
反序列化
反序列化就是将序列化后的json字符串或者字节数组在转化为编程语言的内置数据结构。这个反序列化可以发生正在同一语言内,也可以发生在在不同语言中,例如,Java类序列化后网络传输到前端由json反序列化,或者其他任何后端语言将内置数据序列化后传输到前端。
在同一语言中对序列化的数据反序列化,Java实现方式与Go实现方式。
/**
go语言实现
*/
//定义结构体
type Person struct {
Name string `json:"person_name"`
Address string `json:"person_address"`
Age int `json:"person_age"`
}
//序列化
per := Person{
Name: "钢铁侠",
Address: "漫威",
Age: 18,
}
perjson, err := json.Marshal(&per)
if err != nil {
panic(err)
}
fmt.Printf("per序列化的json数据为为:%v", perjson)
fmt.Printf("per序列化的json字符串为:%v", string(perjson))
序列化后为一个字节数组,可以通过string()
方法转化为字符串。
func json.Unmarshal(data []byte, v any) error
该方法时反序列化方法,第一个参数为需要反序列化的字节数组,第二个参数为反序列化后数据的赋值变量。
//反序列化赋值变量
var per1 Person
err1 := json.Unmarshal(perjson, &per1)
if err1 != nil {
panic(err1)
}
fmt.Printf("反序列化的数据per1%s,%s,%d", per1.Name, per1.Address, per1.Age)
str := `{"person_name":"钢铁侠","person_address":"漫威","person_age":18}`
var per2 Person
err2 := json.Unmarshal([]byte(str), &per2)
if err2 != nil {
panic(err2)
}
fmt.Println(per2)
对字符串也可以反序列化,需要将字符串转化为字节数组,通过[]byte()
方法。
在Go语言的包中,需要测试某些方法是否正确,某些业务逻辑是否合理,需要通过测试,一般情况下需要在mian包中引入模块,再测试模块的方法或逻辑。这样的缺点是测试代码需要编写在主类中,然而在实际项目开发中,主类中有其自己的逻辑,将测试代码写在主类中,很繁琐,另外项目在运行时,无法在主类中修改测试d代码。而且在生产环境时还需要删除这些测试的代码。
为了解决这些问题Go语言提供了单元测试,可以针对包和模块的某些方法编程代码测试,而且这些测试模块是独立的,不用编写在主类中,不会影响主要的业务和代码。部署生产环境时直接删除测试模块的代码即可,非常方便。
Go语言中提供了testing
单元测试框架,能够很好的解决这些问题。testing单元测试框架提供go test
命令来实现单元测试和性能测试,基于这个框架针对相应的函数写测试用例。
testing 提供对 Go 包的自动化测试的支持。通过 go test
命令,能够自动执行如下形式的任何函数:
func TestXxx(*testing.T)
其中 Xxx 可以是任何字母数字字符串,用于识别测试用例。要编写一个新的测试套件,需要创建一个名称以 _test.go 结尾的文件,该文件包含 TestXxx
函数,如上所述。 将该文件放在与被测试的包相同的包中。该文件将被排除在正常的程序包之外,但在运行 “go test” 命令时将被包含。
testing单元测试的机制是编译器创建主类将将xxx_test.go包引入并调用TestXXX测试的方法,这些过程均有testing框架完成。
因此使用testing测试框架必须,按照其规范编写测试用例。
测试用例规范
- 测试的包必须使用
_test.go
后缀结尾;- 测试包需要引入
testing
包- 测试方法必须以
Test
开头即TestXXX,且参数为测试包的的指针参数,一般为*testing.T
//mathutil.go
package mathutil
func add(x int, y int) int {
return x + y
}
func sub(x int, y int) int {
return (x - y)
}
//mathutil_test.go
package mathutil
import (
"fmt"
"testing"
)
func TestAdd(t *testing.T) {
res := add(10, 13)
if res != (10 + 13) {
fmt.Println("add Fail,expect 23 but found", res)
}
}
总结
- 测试用例文件名必须以_test.go结尾;
- 测试用例函数必须以Test开头;
- 测试函数的参数类型必须是*testing.T
- 运行测试用例的指令为
go test
无日志信息;go test -v
打印详细信息- 使用
*testing.T
的方法t.Fatalf
打印错误退出程序和t.Logf
方法可以打印日志。- 测试用例独立于主程序,只在go test命令生效,PASS表示运行成功,FAIL表示运行失败。