我们将用几节来学习Go语言基础,本文结构如下:
数据new 分配构造函数与复合字面make 分配数组切片二维切片映射打印追加初始化常量变量init 函数
数据
本节包含了 Go 为变量分配内存的方式,和常用的数组,map两种数据结构。
Go提供了两种分配方式,即内建函数 new 和 make。
关键点:
new 函数格式为: new(T)
特点:它返回一个指针, 该指针指向新分配的,类型为 T 的零值
内建函数 new 是个用来分配内存的内建函数, 但与其它语言中的同名函数不同,它不会初始化内存,只会将内存置零。
Go 的 new比于java的情形是,java可以通过 new 执行构造来初始化一个对象,而Go不能初始化(赋初值),它只能置为”零值“
也就是说,new(T) 会为类型为 T 的新项分配已置零的内存空间, 并返回它的地址,也就是一个类型为 *T 的值。用Go的术语来说,它返回一个指针, 该指针指向新分配的,类型为 T 的零值
。
这样的设计,使得无需像Java那样面对不同对象的丰富多彩的构造函数和参数。
既然 new 返回的内存已置零,就不必进一步初始化了,使用者只需用 new 创建一个新的对象就能正常工作。
例如:
p := new(SyncedBuffer) // type *SyncedBuffer
var v SyncedBuffer // type SyncedBuffer
如上的两种方式,都会分配好内存空间,而类型是不同的。
有些场景下,仍然需要一个初始化构造函数,就像 os 包中的这段代码所示:
func NewFile(fd int, name string) *File {if fd <0 {return nil}f :&#61; new(File)f.fd &#61; fdf.name &#61; namef.dirinfo &#61; nilf.nepipe &#61; 0return f
}
上面的代码过于冗长。我们可通过复合字面来简化它&#xff1a;
func NewFile(fd int, name string) *File {if fd <0 {return nil}f :&#61; File{fd, name, nil, 0}return &f
}
注意 File{fd, name, nil, 0} 这样的写法就是 复合字面
的写法。该表达式在每次求值时都会创建新的实例。
复合字面的字段必须按顺序全部列出
。但如果以 字段:值
对的形式明确地标出元素&#xff0c;初始化字段时就可以按任何顺序出现&#xff0c;未给出的字段值将赋予零值。 因此&#xff0c;我们可以用如下形式&#xff1a;
return &File{fd: fd, name: name}
内建函数 make 的格式为&#xff1a; make(T, args)
特点&#xff1a;它只用于创建切片、映射和信道&#xff0c;并返回类型为 T&#xff08;而非 *T&#xff09;的一个已初始化 &#xff08;而非置零&#xff09;的值。
切片、映射和信道 本质上为引用数据类型&#xff0c;在使用前必须初始化。 例如&#xff0c;切片是一个具有三项内容的描述符&#xff0c;包含一个指向&#xff08;数组内部&#xff09;数据的指针、长度以及容量&#xff0c; 在这三项被初始化之前&#xff0c;该切片为 nil。
对于切片、映射和信道&#xff0c;make 用于初始化其内部的数据结构并准备好将要使用的值。
例如&#xff1a;
make([]int, 10, 100) 分配一个具有100个 int 的数组空间&#xff0c;接着创建一个长度为10&#xff0c; 容量为100并指向该数组中前10个元素的切片结构
new([]int) 会返回一个指向新分配的&#xff0c;已置零的切片结构&#xff0c; 即一个指向 nil 切片值的指针。
下面的例子阐明了 new 和 make 之间的区别&#xff1a;
var p *[]int &#61; new([]int) // 分配切片结构&#xff1b;*p &#61;&#61; nil&#xff1b;基本没用
var v []int &#61; make([]int, 100) // 切片 v 现在引用了一个具有 100 个 int 元素的新数组// 没必要的复杂&#xff1a;
var p *[]int &#61; new([]int)
*p &#61; make([]int, 100, 100)// 习惯用法&#xff1a;
v :&#61; make([]int, 100)
再次说明关键点&#xff1a;
在规划内存布局时&#xff0c;数组是非常有用的&#xff0c;有时还能避免过多的内存分配&#xff0c; 在Go中&#xff0c;数组主要用作切片的构件&#xff0c;在构建切片时使用。
数组在Go和C中的主要区别。在Go中&#xff1a;
数组为值的属性很有用&#xff0c;但代价高昂&#xff1b;若你想要C那样的行为和效率&#xff0c;你可以传递一个指向该数组的指针。
在 Go 中&#xff0c;更习惯的的用法是使用 切片。
切片通过对数组进行封装&#xff0c;为有序列的数据提供了更通用、强大而方便的方式。
除了矩阵变换这类需要明确维度的情况外&#xff0c;Go中的大部分数组编程都是通过切片来完成的。
切片保存了对底层数组的引用&#xff0c;若你将某个切片赋予另一个切片&#xff0c;它们会引用同一个数组。 若某个函数将一个切片作为参数传入&#xff0c;则它对该切片元素的修改对调用者而言同样可见&#xff0c; 这可以理解为传递了底层数组的指针。
修改长度&#xff1a;只要切片不超出底层数组的限制&#xff0c;它的长度就是可变的&#xff0c;只需产生新的切片再次指向自身变量即可。
切片的长度&#xff1a;
len(切片)
切片的容量可通过内建函数 cap 获得&#xff0c;它将给出该切片可取得的最大长度。函数为&#xff1a;
cap(切片)
若数据超出其容量&#xff0c;则会重新分配该切片。返回值即为所得的切片。
向切片追加东西的很常用&#xff0c;因此有专门的内建函数 append。
一般情况下&#xff0c;如果我们要写一个 append 方法的话&#xff0c;最终返回值必须返回切片。示例&#xff1a;
func Append(slice, data[]byte) []byte {l :&#61; len(slice)if l &#43; len(data) > cap(slice) { // 重新分配// 为了后面的增长&#xff0c;需分配两份。newSlice :&#61; make([]byte, (l&#43;len(data))*2)// copy 函数是预声明的&#xff0c;且可用于任何切片类型。copy(newSlice, slice)slice &#61; newSlice}slice &#61; slice[0:l&#43;len(data)]for i, c :&#61; range data {slice[l&#43;i] &#61; c}return slice}
如上&#xff0c;输入参数是切片和插入的元素值&#xff0c;返回值是切片&#xff0c;注意切片的长度会发生变化。
因为尽管 Append 可修改 切片 的元素&#xff0c;但切片自身&#xff08;其运行时数据结构包含指针、长度和容量&#xff09;是通过值传递的。
要创建等价的二维数组或切片&#xff0c;就必须定义一个数组的数组&#xff0c; 或切片的切片&#xff0c;示例&#xff1a;
type Transform [3][3]float64 // 一个 3x3 的数组&#xff0c;其实是包含多个数组的一个数组。
type LinesOfText [][]byte // 包含多个字节切片的一个切片。
每行都有其自己的长度&#xff1a;
由于切片长度是可变的&#xff0c;因此其内部可能拥有多个不同长度的切片。
映射 是Go中 数据结构中的 map结构实现&#xff0c;即 key: value的形式存储。
映射的值可以是各种类型。
映射的键可以是整数、浮点数、复数、字符串、指针、接口等。
映射的键&#xff08;或者叫索引&#xff09;可以是任何相等性操作符支持的类型&#xff0c; 如整数、浮点数、复数、字符串、指针、接口&#xff08;只要其动态类型支持相等性判断&#xff09;、结构以及数组。 切片不能用作映射键&#xff0c;因为它们的相等性还未定义。与切片一样&#xff0c;映射也是引用类型。
如果将映射作为参数传入函数中&#xff0c;并更改了该映射的内容&#xff0c;则此修改对调用者同样可见。
映射可使用一般的复合字面语法进行构建&#xff0c;其键-值对使用逗号分隔&#xff0c;有点像JSON:
var timeZone &#61; map[string]int{"UTC": 0*60*60,"EST": -5*60*60,"CST": -6*60*60,"MST": -7*60*60,"PST": -8*60*60,
}
获取值&#xff1a;
offset :&#61; timeZone["EST"]
注意&#xff1a;若试图通过映射中不存在的键来取值&#xff0c;就会返回与该映射中项的类型对应的零值。例如&#xff0c;若某个映射包含整数&#xff0c;当查找一个不存在的键时会返回 0。
判断某个值是否存在&#xff1a;
seconds, ok &#61; timeZone[tz]
上面是惯用的 "逗号 ok” 法&#xff1a;
若仅需判断映射中是否存在某项而不关心实际的值&#xff0c;可使用空白标识符 _
来代替该值的一般变量。
_, present :&#61; timeZone[tz]
要删除映射中的某项&#xff0c;可使用内建函数 delete
。即便对应的键不在该映射中&#xff0c;此操作也是安全的。
delete(timeZone, "PDT")
Go的格式化打印风格和C的 printf 类似&#xff0c;但却更加丰富而通用。 这些函数位于 fmt 包中&#xff0c;且函数名首字母均为大写&#xff1a;如 fmt.Printf、fmt.Fprintf&#xff0c;fmt.Sprintf 等。
看例子&#xff1a;
// 以f 结尾的这几个&#xff0c;传入格式化字符串作为参数, 不换行
fmt.Printf("hello, %v \n","zhang3")
fmt.Fprintf(os.Stdout,"hello, %v \n","zhang3")
str :&#61; fmt.Sprintf("hello, %v \n","zhang3")//下面这几个&#xff0c;会换行
fmt.Println(str)
// 注意下面这个&#xff0c;会自动在元素间插入空格
fmt.Fprintln(os.Stdout,"f1","f2","f3")
Sprintf 用于构造字符串&#xff1a; 字符串函数&#xff08;Sprintf 等&#xff09;会返回一个字符串&#xff0c;而不是写入到数据流中。
Fprint 用于写入到各种流中&#xff1a;fmt.Fprint 一类的格式化打印函数可接受任何实现了 io.Writer 接口的对象作为第一个实参&#xff1b;比如 os.Stdout 与 os.Stderr 。
下面对 Printf 支持的格式化的字符做一些说明&#xff1a;
-- 格式&#xff1a; %d
像 %d 不接受表示符号或大小的标记&#xff0c; 会根据实际的类型来决定这些属性。
var x uint64 &#61; 1<<64 - 1 // x 是无符号整数&#xff0c; 下面的 int64(x) 转换为有符合整数
fmt.Printf("%d %x; %d %x\n", x, x, int64(x), int64(x))
将打印
18446744073709551615 ffffffffffffffff; -1 -1
-- 格式&#xff1a; %v
%v 可理解为 实际的 value。
它还能打印任意值&#xff0c;甚至包括数组、结构体和映射。
fmt.Printf("%v\n", timeZone) // 或只用 fmt.Println(timeZone)
这会输出
map[CST:-21600 PST:-28800 EST:-18000 UTC:0 MST:-25200]
%&#43;v 和 %#v
当打印结构体时&#xff0c;格式 %&#43;v 会带上每个字段的字段名&#xff0c;而格式 %#v 会带上类型。
type T struct {a intb float64c string}t :&#61; &T{ 7, -2.35, "abc\tdef" }fmt.Printf("%v\n", t)fmt.Printf("%&#43;v\n", t)fmt.Printf("%#v\n", t)
将打印
&{7 -2.35 abc def} // 请注意其中的&符号
&{a:7 b:-2.35 c:abc def} // 有了字段名
&main.T{a:7, b:-2.35, c:"abc\tdef"} //有了类型
-- 格式&#xff1a;%q
当遇到 string 或 []byte 值时&#xff0c; 可使用 %q 产生带引号的字符串&#xff1b;而格式 %#q 会尽可能使用反引号。
--格式&#xff1a;%x
%x 还可用于字符串、字节数组以及整数&#xff0c;并生成一个很长的十六进制字符串&#xff0c; 而带空格的格式&#xff08;% x&#xff09;还会在字节之间插入空格。
--格式&#xff1a; %T
它会打印某个值的类型.
fmt.Printf("%T\n", timeZone)
会打印
map[string] int
-- 为结构图自定义输出
类似 java 中的 toString()&#xff0c;对结构图自定义类型的默认格式&#xff0c;只需为该类型定义一个具有 String() string 签名的方法。对于我们简单的类型 T&#xff0c;可进行如下操作。
func (t *T) String() string {return fmt.Sprintf("%d/%g/%q", t.a, t.b, t.c)
}fmt.Printf("%v\n", t)
会打印出如下格式&#xff1a;
7/-2.35/"abc\tdef"
-- 任意数量的
Printf 的签名为其最后的实参使用了 ...interface{} 类型&#xff0c;这样格式的后面就能出现任意数量&#xff0c;任意类型的形参了。
func Printf(format string, v ...interface{}) (n int, err error) {
在 Printf 函数的实现中&#xff0c;v 看起来更像是 []interface{} 类型的变量&#xff0c;但如果将它传递到另一个变参函数中&#xff0c;它就像是常规实参列表了。实际上&#xff0c;它直接将其实参传递给 fmt.Sprintln 进行实际的格式化。
// Println 通过 fmt.Println 的方式将日志打印到标准记录器。
func Println(v ...interface{}) {std.Output(2, fmt.Sprintln(v...)) // Output 接受形参 (int, string)
}
注意上面的 ...interface{} 和 v... 的写法。
append 函数的签名就像这样&#xff1a;
func append(slice []T, 元素 ...T) []T
其中的 T 为任意给定类型的占位符。实际上&#xff0c;你无法编写一个类型 T 由调用者决定的函数。这也就是为何 append 为内建函数的原因&#xff1a;它需要编译器的支持。
append 会在切片末尾追加元素并返回结果。我们必须返回结果&#xff0c; 原因是&#xff0c;底层数组可能会被改变&#xff08;注意数组的长度是类型的一部分&#xff09;。
以下简单的例子
x :&#61; []int{1,2,3}
x &#61; append(x, 4, 5, 6)
fmt.Println(x)
将打印
[1 2 3 4 5 6]
将一个切片追加到另一个切片很简单&#xff1a;在调用的地方使用 ...
x :&#61; []int{1,2,3}
y :&#61; []int{4,5,6}
x &#61; append(x, y...)
fmt.Println(x)
如果没有 ...&#xff0c;它就会由于类型错误而无法编译&#xff0c;因为 y 不是 int 类型的。三个点符号 “ ...
” 的作用有点像“ 展开 ” 的作用&#xff0c;即将 y这个切片的元素放到了这里。
GO 的huaGo的初始化很强大&#xff0c;在初始化过程中&#xff0c;不仅可以构建复杂的结构&#xff0c;还能正确处理不同包对象间的初始化顺序。
常量在编译时被创建&#xff0c;即便函数中定义的局部变量也一样。
常量只能是数字、字符&#xff08;符文&#xff09;、字符串或布尔值。
由于编译时的限制&#xff0c; 定义它们的表达式必须是可被编译器求值的常量表达式。例如 1<<3 就是一个常量表达式。
枚举常量
枚举常量使用枚举器 iota 创建。由于 iota 可为表达式的一部分&#xff0c;而表达式可以被隐式地重复&#xff0c;这样也就更容易构建复杂的值的集合了。
type ByteSize float64const (// 通过赋予空白标识符来忽略第一个值_ &#61; iota // ignore first value by assigning to blank identifierKB ByteSize &#61; 1 <<(10 * iota)MBGBTBPBEBZBYB)
变量的初始化与常量类似&#xff0c;但其初始值也可以是在运行时才被计算的一般表达式。
var (home &#61; os.Getenv("HOME")user &#61; os.Getenv("USER")gopath &#61; os.Getenv("GOPATH")
)
每个源文件都可以通过定义自己的无参数 init 函数来设置一些必要的状态。格式为:
func init() { ...}
而 init 方法执行结束&#xff0c;就意味着初始化结束了&#xff1a;只有该包中的所有变量声明都通过它们的初始化器求值后 init 才会被调用&#xff0c; 而那些 init 只有在所有已导入的包都被初始化后才会被求值。
init 函数还常被用在程序真正开始执行前&#xff0c;检验或校正程序的状态。示例&#xff1a;
func init() {if user &#61;&#61; "" {log.Fatal("$USER not set")}if home &#61;&#61; "" {home &#61; "/home/" &#43; user}if gopath &#61;&#61; "" {gopath &#61; home &#43; "/go"}// gopath 可通过命令行中的 --gopath 标记覆盖掉。flag.StringVar(&gopath, "gopath", gopath, "override default GOPATH")}
END