3.函数

Thursday, June 29, 2023

Printf 与 Sprintf 格式化输出

Printf 代表“带格式的打印”,接受一个字符串并将一个或多个值插入其中,以特定的方式进行格式化,然后打印结果字符串。
Sprintf函数(也是fmt包的一部分)的工作方式与Printf 类似,不过返回格式化的字符串而不是打印。

fmt.Printf("About one-third: %0.2f\n", 1.0/3.0)
resultString := fmt.Sprintf("About one-third: %0.2f\n", 1.0/3.0)
fmt.Printf(resultString)

Printf的两个特性:

  • 格式化动词(字符串中的%0.2f是动词)
  • 值的宽度(动词中间的0.2)

格式化动词

Printf第一个参数是一个字符串,用于格式化输出,大部分格式与字符串中显示的格式完全相同,但是任何 % 都将被视为格式化动词的开始,字符串的一部分将被特定格式的值所替换,其余参数用作这些动词的值。

% 后面的字母表示要使用哪个动词,常见动词:

动词 输出
%f 浮点数
%d 十进制整数
%s 字符串
%t 布尔值(true或false)
%v 任何值(根据所提供的值的类型选择适当的格式)
%#v 任何值,按其在go程序代码中显示的格式进行格式化
%T 所提供值的类型(int、string)
%% 一个完全的百分号

示例:

fmt.Printf("A float: %f\n", 3.1415)
fmt.Printf("An integer: %d\n", 15)
fmt.Printf("A string: %s\n", "hello")
fmt.Printf("A boolean: %t\n", false)
fmt.Printf("Values: %v %v %v\n", 1.2, "\t", true)
fmt.Printf("Values: %#v %#v %#v\n", 1.2, "\t", true)
fmt.Printf("Types: %T %T %T\n", 1.2, "\t", true)
fmt.Printf("Percent sign: %%\n")

输出:

A float: 3.141500
An integer: 15
A string: hello
A boolean: false
Values: 1.2       true
Values: 1.2  "\t" true
Types: float64 string bool
Percent sign: %

确保使用 \n 转义序列在每个格式化字符串的末尾添加一个换行符,因为与Println不同,Printf不会自动添加新行。

fmt.Printf("%v %v %v", "", "\t", "\n")
fmt.Printf("%#v %#v %#v", "", "\t", "\n")

输出:

                  //%v打印所有值。。
"" "\t" "\n"      //只有用 %#v 才能真正看到它们

%#v以Go代码中的显示方式而不是通常的显示方式来打印,%#v 可以显示一些值,若不使用 %#v ,这些值可能会在输出中隐藏;例如上述代码中,%#v 显示了一个 空字符串、制表符、换行符,在 %v 打印时是不可见的。

格式化宽度

可以在格式化动词的百分号后面指定最小宽度,若该动词参数比最小宽度短,则使用空格填充。

fmt.Printf("%12s | %s\n", "Product", "Cost in Cents")
fmt.Printf("%12s | %s\n", "Stamps", 50)
fmt.Printf("%12s | %s\n", "Paper Clips", 5)
fmt.Printf("%12s | %s\n", "Tape", 99)

输出:

     Product | Cost in Cents
      Stamps | 50
 Paper Clips |  5
        Tape | 99

格式化小数宽度

整个数的最小宽度包括数字位和小数点,若包括,较短的数将在开始处填充空格,直到达到这个宽度,若省略,则不会添加任何空格。
小数点后的宽度是要显示的小数位数,若是更精确的数字,将被四舍五入。

fmt.Printf("%%7.3f: %7.3f\n", 12.3456)
fmt.Printf("%%7.2f: %7.2f\n", 12.3456)
fmt.Printf("%%7.1f: %7.1f\n", 12.3456)
fmt.Printf("%%.1f: %.1f\n", 12.3456)
fmt.Printf("%%.2f: %.2f\n", 12.3456)

输出:

%7.3f:   12.346
%7.2f:    12.35
%7.1f:     12.3
%.1f:  12.3
%.2f:  12.35

%.2f 将允许取任意精度的浮点数,并将它们四舍五入到小数点后两位。(不会做任何不必要的填充)。

声明函数

声明以func关键字开头,后面跟希望函数具有的名称、一对圆括号(),然后是包含函数代码的块。
一旦声明了一个函数,就可以在包的其他地方调用,只需输入函数名,后面跟一对圆括号。当执行此操作时,函数快中的代码就会运行。

示例:

package main
import "fmt"
func sayHi() {
       fmt.Println("Hi!")
}
func main() {
       sayHi()
}

当调用当前包中定义的函数时,不应该指定包名,(输入 main.sayHi() 将导致编译错误)。

函数名的规则与变量名的规则相同:

  • 名称必须以 字母开头 ,后跟任何数量的数字和字母;
  • 名称以 大写字母 开头的函数是可导出的,且可以在当前包之外使用;
  • 名称以 小写字母 开头的函数是不可导出的,只能在当前包中使用;
  • 名称包含多个单词应符合驼峰式大小写;

声明函数参数

若希望对函数的调用包含参数,则需声明一个或多个参数,参数是函数的局部变量,其值是在调用函数时设置的。

package main

import "fmt"

func main() {
	repeatLine("hello", 3)
}
func repeatLine(line string, times int) {
	for i := 0; i < times; i++ {
		fmt.Println(line)
	}
}

在函数声明中的圆括号中声明一个或多个参数,用逗号分隔,与变量一样,需要为声明的每个参数提供一个名称,后面跟一个类型(float64、bool等)。

若函数定义了参数,那么在调用时需要传递一组匹配的参数。当函数运行时,每个参数都将被设置为对应参数中的值的副本,然后这些参数值在函数块的代码中被使用。

使用函数

package main

import "fmt"

func paintNeeded(width float64, height float64) {
	area := width * height
	fmt.Printf("%.2f liters needed\n", area/10.0)
}
func main() {
	paintNeeded(4.2, 3.0)
	paintNeeded(5.2, 3.5)
	paintNeeded(5.0, 3.3)
}

函数和变量的作用域

上述在paintNeeded函数块中声明了一个area变量,与条件块和循环块一样,函数块中声明的变量只在该函数块的作用域内。

与条件块和循环块一样,在函数外部声明的变量将在该函数块的作用域内,这意味着可以在包级别上声明一个变量,并且可以在包中的任何函数内访问。

函数返回值

函数总是返回特定类型的值(只返回该类型),若要声明函数返回值,在函数声明中的参数后面添加该返回值类型,在函数块中使用 return 关键字,后面跟要返回的值。函数的调用者可以将返回值分配给一个变量,直接将它传递给另一个函数,或用它做任何其他需要做的事。

package main

import "fmt"

func double(number float64) float64 {
	return number * 2
}
func main() {
	dozen := double(6.0) //将返回值赋给一个变量
	fmt.Println(dozen)
	fmt.Println(double(4.2))  //将返回值传递给另一个函数
}

当 return 语句运行时,函数立即退出,不运行它后面的任何代码,可以将其与if语句一起使用,在没有必要运行剩余代码的情况下(由于一些错误或其他情况)退出函数。

func status(grade float64) string {
	if grade < 60.0 {
            return "failing"  //若不合格,立即返回
	}
        return "passing"  //只有当>=60时才运行
}
func main() {
        fmt.Println(status(60.1))
        fmt.Println(status(59))
}

若包含一个不属于 if 块的 return 语句,有可能使代码在任何情况都不会运行,go要求声明了返回类型的任何函数都必须以 return 语句结束,以任何其他语句结束都将导致编译错误。

若返回值类型与声明的返回类型不匹配,也将编译错误。

使用返回值

package main

import "fmt"

func paintNeeded(width float64, height float64) float64 {
	area := width * height
	return area / 10.0
}
func main() {
	var amount, total float64
	amount = paintNeeded(4.2, 3.0)
	fmt.Printf("%0.2f liters needed\n", amount)
	total += amount
	amount = paintNeeded(5.2, 3.5)
	fmt.Printf("%0.2f liters needed\n", amount)
	total += amount
	fmt.Printf("Total: %0.2f liters\n", total)
}

返回值允许main函数决定所计算的数量做什么,而不是依赖paintNeeded函数打印它。

对下述代码进行更改,会怎么样?

func paintNeeded(width float64, height float64) float64 {
        area := width * height
        return area / 10.0
}
如果这样 会失败,因为
func paintNeeded(width float64, height float64) float64 {
area := width * height
return area / 10.0
}
删除return 语句
如果函数声明了返回类型,go要求包含一个return语句
func paintNeeded(width float64, height float64) float64 {
area := width * height
return area / 10.0
fmt.Println(area / 10.0)
}
在return语句后加一行
如果函数声明了返回类型,go要求最后一个语句是return语句
func paintNeeded(width float64, height float64) float64 {
area := width * height
return area / 10.0
}
删除返回类型声明
go不允许返回未声明的值
func paintNeeded(width float64, height float64) float64 {
area := width * height
return int(area / 10.0)
}
更改要返回值的类型
go要求返回值的类型与声明的类型匹配

错误值

一个错误值是一个可以返回字符串的名为 Error 的方法返回的任何值。创建错误值的最简单方法是将字符串传递给 errors 包的 New 函数,该函数将返回一个新的错误值。若对该错误值调用 Error 方法,将会得到传递给 errors.New 的字符串。

package main

import (
      "errors"
      "fmt"
)

func main() {
      err := errors.New("height can't be negative")  //创建一个新的错误值
      fmt.Println(err.Error())  //返回错误信息
}

若将错误值传递给fmt或log包中的函数,则可能不需要调用它的Error方法。fmt和log中的函数已经被编写成能够检查是否传递给它们的值有Error方法,若有,则打印Error的返回值。

err := errors.New("height can't be negative")
fmt.Println(err)  //打印错误信息
log.Fatal(err)   //再次打印错误信息,然后退出程序

若需要格式化数字或其他值以便在错误信息中使用,可以使用 fmt.Errorf 函数,就像 fmt.Printf 或 fmt.Sprintf 一样,将值插入格式字符串中,但是不会打印或返回一个字符串,而是返回一个错误值。

err := fmt.Errorf("a height of %0.2f is invalid", -2.33333)
fmt.Println(err.Error())
fmt.Println(err)

输出:

a height of -2.33 is invalid
a height of -2.33 is invalid

声明多个返回值

要声明函数的多个返回值,需将返回值类型放在函数声明的第二组圆括号内(在函数参数的圆括号之后),用逗号分隔。(当只有一个返回值时,返回值周围的圆括号是可选的,但是若有多个返回值,则必须使用圆括号。)当调用该函数时,需要考虑额外的返回值,通常通过将它们分配给额外的变量来实现。

package main

import "fmt"

func manyReturns() (int, bool, string) {   //返回一个整数,一个布尔值,一个字符串
	return 1, true, "hello"
}
func main() {
	myInt, myBool, myString := manyReturns()  //将每个返回值存储在一个变量中
	fmt.Println(myInt, myBool, myString)
}

若要使返回值目的更清楚,可以为每个返回值提供名称,类似于参数名称,命名返回值的主要用途是作为程序员阅读代码的文档。

package main

import (
	"fmt"
	"math"
)

func floatParts(number float64) (integerPart int, fractionalPart float64) {
	wholeNumber := math.Floor(number)
	return int(wholeNumber), number - wholeNumber
}
func main() {
	cans, remainder := floatParts(1.26)
	fmt.Println(cans, remainder)
}

在paintNeeded函数中使用多个返回值

如上所见,可以返回任何类型的多个返回值,但是对于多个返回值,常见的做法是返回一个主返回值,后跟一个额外值,表示函数是否遇到错误,若无问题,通常将额外值设置为nil,若发生错误,则设置为错误值。

下面代码,将声明其返回两个值,一个 float64 和一个 error (错误值有一种 error类型),在函数块中要做的第一件事是检查参数是否有效,若 width 或 height 参数小于0,将返回 0(这样无意义,但必须返回),以及通过调用 fmt.Errorf 产生的错误值,在函数开始的地方检查错误,若有问题,允许通过调用 return 轻松跳过函数的其余代码。若参数无问题,会像之前那样继续计算并返回,函数代码唯一不同之处在于,返回第二个值 nil 以及油漆量,表示没有错误。

package main

import "fmt"

func paintNeeded(width float64, height float64) (float64, error) {
	if width < 0 {
		return 0, fmt.Errorf("a width of %0.2f is invalid", width)
	}
	if height < 0 {
		return 0, fmt.Errorf("a height of %0.2f is invalid", height)
	}
	area := width * height
	return area / 10.0, nil
}
func main() {
	amount, err := paintNeeded(4.2, -3.0)
	fmt.Println(err)
	fmt.Printf("%0.2f liters needed\n", amount)
}

在 main 函数中,添加第二个变量记录 paintNeeded 中的错误值,若将一个无效的参数传递给paintNeeded,将得到一个错误返回值,并打印该错误。

始终处理错误

当将一个无效的参数传递给paintNeeded时,得到一个错误返回值,并将其打印出来,但也得到了一个无效的值。

func main() {
	amount, err := paintNeeded(4.2, -3.0)   //amount将被设置为0,err将被设置为一个错误值
	fmt.Println(err)   //打印错误
	fmt.Printf("%0.2f liters needed\n", amount)  //打印无意义的值
}

当函数返回一个错误值时,通常也必须返回一个主返回值,但伴随错误值的任何其他返回值都应被认为不可靠,被忽略。当调用返回错误值的函数时,在继续运行前测试该值是否为 nil 是重要的,若不是 nil ,则意味着有一个错误必须进行处理。

如何处理错误取决于具体情况,对于 paintNeeded 函数,也许最好是简单跳过当前计算,并继续执行程序其余部分:

func main() {
	amount, err := paintNeeded(4.2, -3.0)
	if err != nil {   //若不是nil,则一定有问题
		fmt.Println(err)
	} else {  //否则,错误值为nil
		fmt.Printf("%0.2f liters needed\n", amount)  //因此可以打印我们得到的数量
	}
}

可以调用 Log.Fatal 来显示错误信息并退出程序,因为这个程序比较简短。

func main() {
	amount, err := paintNeeded(4.2, -3.0)
	if err != nil {
		log.Fatal(err)  //打印错误并退出程序
	}
        fmt.Printf("%0.2f liters needed\n", amount)  //若有错误,则永远不会执行这里
}

要记住,应该始终检查返回值以查看是否存在错误。

如下是一个计算一个数字平方根的程序,若一个负数传递给 squareRoot 函数,将返回一个错误:

package main

import (
	"fmt"
	"math"
)

func squareRoot(number float64) (float64, error) {
	if number < 0 {
		return 0, fmt.Errorf("can't get square root of number")
	}
	return math.Sqrt(number), nil
}
func main() {
	root, err := squareRoot(-9.3)
	if err != nil {
		fmt.Println(err)
	} else {
		fmt.Printf("%0.3f", root)
	}
}
若如此 会失败,因为
return math.Sqrt(number), nil
删除要返回的参数之一
要返回的参数的数量必须始终与函数声明中的返回值的数量匹配
root, err := squareRoot(-9.3)
删除返回值所赋值给的变量之一
若使用函数的任何一个返回值,go要求使用所有的返回值
root, err := squareRoot(-9.3)
if err != nil {
fmt.Println(err)
} else {
fmt.Printf("%0.3f", root)
}
删除使用其中一个返回值的代码
go要求使用声明的每个变量,当涉及错误返回值时,实际上是一个非常有用的特性,因为有助于避免意外地忽略错误

函数形参接收实参的副本

当调用一个声明了行参的函数时,需要为调用提供实参,每个实参中的值被复制到相应的行参变量。(执行此操作的编程语言有时称为“值传递”)。这在大多数情况是可以的,但如果想把一个变量的值传递给一个函数并让它以某种方式改变这个值,就会遇到麻烦。

函数只能更改行参中的该值的副本,而不能更改原始值,因此,在函数内部所做的任何更改在函数外部都将不可见。

Go是一种“值传递”语言;函数行参从函数调用中接收实参的副本。

下面代码,接受一个数字,将其乘以2,然后输出结果。(使用*=运算符,工作方式与+=类似,但它会乘以变量所保存的值,而不是将其想加。)

package main

import "fmt"

func double(number int) {  //行参设置为实参的一个副本
	number *= 2
	fmt.Println(number)
}

func main() {
	amount := 6
	double(amount)   //向函数传递一个实参
}

输出:

12

此时,将打印双倍值的语句从double函数移动回调用它的函数,它就不会工作,因为double函数只会更改其值的副本。回到调用函数中,当尝试打印时,将得到原始值,而不是双倍值。

func double(number int) { //行参设置为实参的一个副本
	number *= 2   //更改副本值,而不是原始值
}

func main() {
	amount := 6
	double(amount) //向函数传递一个实参
	fmt.Println(amount)  //打印原始值
}

输出:

6   //未变化

指针

需要一种方法来允许函数改变变量所保存的原始值,而不是副本。

可以使用 & 符号来获取变量的地址,是go的“地址”运算符。
例如:

amount := 6
fmt.Println(amount)   //得到变量的值
fmt.Println(&amount)  //得到变量的地址

输出:

6  //变量的值
0x1040a124  //变量的地址

可以获得任何类型变量的地址,每个变量的地址不同。

计算机为程序留出的内存是一个拥挤的地方,其充满了变量值:布尔值、整数、字符串等,如果有变量的地址,可以用其找到变量所包含的值。

表示变量地址的值称为指针,它指向可以找到变量的位置。

指针类型

指针的类型可以写为一个 * 符号,后面跟着指针指向的变量的类型。

例如,指向一个 int 变量的指针的类型写为 *int ,可以大声读作:“指向int的指针”。

可以使用 reflect.TypeOf 函数来显示之前程序中指针的类型:

package main

import (
	"fmt"
	"reflect"
)

func main() {
	var myInt int
	fmt.Println(reflect.TypeOf(&myInt))  //得到指向myInt的指针并打印该指针的类型
	var myFloat float64
	fmt.Println(reflect.TypeOf(&myFloat))  //得到指向myFloat的指针并打印该指针的类型
	var myBool bool
	fmt.Println(reflect.TypeOf(&myBool))  //得到指向myBool的指针并打印该指针的类型
}

输出:

*int
*float64
*bool

可以声明保存指针的变量,指针变量只能保存指向一种类型值的指针,因此变量可能只保存 *int 指针,只保存 *float64 指针。。。。

var myInt int
var myIntPointer *int  //声明一个指向int的指针变量
myIntPointer = &myInt  //给变量分配一个指针
fmt.Println(myIntPointer)

var myFloat float64
var myFloatPointer *float64  //声明一个指向float64的指针变量
myFloatPointer = &myFloat    //给变量分配一个指针
fmt.Println(myFloatPointer)

与其他类型一样,若要立即为指针变量赋值,可以使用短变量声明:

var myBool bool
myBoolPointer := &myBool
fmt.Println(myBoolPointer)

获取和更改指针的值

通过在代码中的指针之前输入 * 运算符来获得指针引用的变量的值。 *myIntPointer 可以读作:“myIntPointer 处的值”

myInt := 4
myIntPointer := &myInt
fmt.Println(myIntPointer)  //打印指针本身
fmt.Println(*myIntPointer)  //打印指针处的值

myFloat := 98.6
myFloatPointer := &myFloat
fmt.Println(myFloatPointer)
fmt.Println(*myFloatPointer)

myBool := true
myBoolPointer := &myBool
fmt.Println(myBoolPointer)
fmt.Println(*myBoolPointer)

输出:

0x1040a124
4
0x1040a140
98.6
0x1040a150
true

* 运算符还可以更新指针处的值:

myInt := 4
fmt.Println(myInt)
myIntPointer := &myInt
*myIntPointer = 8    //给指针处的变量(myInt)赋一个新值
fmt.Println(*myIntPointer)   //打印指针处变量的值
fmt.Println(myInt)     //打印变量的值

输出:

4    //myInt初始值
8    //*myIntPointer的更新结果
8    //myInt的更新值(与*myIntPointer)相同

函数指针

可以从函数返回指针,只需声明函数的返回类型是指针类型。

func createPointer() *float64 {    //声明函数返回一个float64指针
        var myFloat = 98.5
        return &myFloat    //返回指定类型的指针
}

func main() {
        var myFloatPointer *float64 = createPointer()   //将返回的指针赋给一个变量
        fmt.Println(*myFloatPointer)    //打印指针处的值
}

输出:

98.5

在go中,返回一个指向函数局部变量的指针是可以的,即使该变量不在作用域内,只要仍然拥有指针,go将确保仍然可以访问该值。

还可以将指针作为参数传递给函数,只需说明一个或多个参数的类型是指针。

func printPointer(myBoolPointer *bool) {    //为该参数使用一个指针类型
        fmt.Println(*myBoolPointer)         //打印传入指针处的值
}
func main() {
        var myBool bool = true
        printPointer(&myBool)    //向函数传递一个指针
}

输出:

true

确保只使用指针作为参数,若函数声明它将使用指针,若试图将值传递给期望指针的函数,将会编译错误。

func main() {
	var myBool bool = true
	printPointer(myBool)
}

输出:

cannot use myBool (type bool) as type *bool in argument to printPointer

使用指针修复double函数

有一个double函数,接受一个 int 值并将其乘以2,希望能传入一个值并使该值加倍。但 go 是一种值传递语言,意味着函数参数从调用方接收任何参数的副本,函数将值的副本加倍,原始值不变。

func main() {
        amount := 6
        double(amount)   //向函数传递实参
        fmt.Println(amount)   //打印原始值
}

func double(number int) {    //行参设置为实参的一个副本
        number *= 2     //改变副本值,不改变原始值
}

若向函数传递一个指针,然后更改该指针处的值,那么这些更改在函数外部仍然有效。

修改:

func main() {
        amount := 6
        double(&amount)     //传递一个指针而不是一个变量值
        fmt.Println(amount)
}
func double(number *int) {   //接受一个指针而不是一个int值
        *number *= 2    //更新指针处的值
}

在double函数中,需要更新number参数的类型来获取 *int 而不是 int ,然后修改函数代码来更新number指针处的值,而不是直接更新变量,在main函数中,只需更新对double的调用来传递一个指针,而不是一个直接的值。 当运行更新后的代码时,指向amount变量的指针将被传递给double函数,double函数将获取该指针处的值并使其加倍,从而更改amount变量中的值。

Golang打怪升级

4.代码集:包

2.条件与循环

comments powered by Disqus