go语言圣经第一章与第二章
strings
课后有一个思考题比较strings.join与简单的字符串相加速度上的区别
以下是自己编写的测试代码
func generate() []string {
number := 10000
numberList := make([]string, number)
for i := 0; i < number; i++{
numberList[i] = string(rand.NewSource(42).Int63())
}
return numberList
}
func compare() {
numbers := generate()
currentTime := time.Now().Nanosecond()
var s string = ""
for i := 0; i < len(numbers); i++{
s += numbers[i]
}
fmt.Println("s 耗时 ", time.Now().Nanosecond() - currentTime, " 长度", len(s))
joinTime := time.Now().Nanosecond()
value := strings.Join(numbers, "")
fmt.Println("join 耗时 ", time.Now().Nanosecond() - joinTime , " 长度", len(value))
}
//s 耗时 27093000 长度 30000
//join 耗时 62000 长度 30000
很明显join速度远大于普通字符串相加。 golang中的string类型也是只读且不可变的。于是在+的过程中会频繁创建和销毁string比较消耗性能。阅读strings.Join阅读源码可知
strings.Join
func Join(elems []string, sep string) string {
switch len(elems) {
case 0:
return ""
case 1:
return elems[0]
}
//计算长度
n := len(sep) * (len(elems) - 1)
for i := 0; i < len(elems); i++ {
n += len(elems[i])
}
// 初始化一个拥有byte数组的对象
var b Builder
b.Grow(n)
b.WriteString(elems[0])
for _, s := range elems[1:] {
b.WriteString(sep)
b.WriteString(s)
}
return b.String()
}
他是先计算出将要拼接的字符串的长度,然后在将其写入Builder中。
strings.Builder
type Builder struct {
addr *Builder // of receiver, to detect copies by value
buf []byte
}
func (b *Builder) grow(n int) {
buf := make([]byte, len(b.buf), 2*cap(b.buf)+n)
copy(buf, b.buf)
b.buf = buf
}
// Write appends the contents of p to b's buffer.
// Write always returns len(p), nil.
func (b *Builder) Write(p []byte) (int, error) {
b.copyCheck()
b.buf = append(b.buf, p...)
return len(p), nil
}
// WriteByte appends the byte c to b's buffer.
// The returned error is always nil.
func (b *Builder) WriteByte(c byte) error {
b.copyCheck()
b.buf = append(b.buf, c)
return nil
}
如上,他是一个byte数组,提供了写入的能力,strings.Join提前grow了其长度,避免了因内存不足的复制。从而使其字符串拼接更加高效。
在需要字符串拼接时,推荐使用strings.join
变量
go中方法外的变量为包变量,包变量同包内可以访问,但你会发现你访问不了main包下面的包变量。不能在其他任何地方访问" main"包中的符号,原因很简单,因为Go不允许导入循环。
一般情况更推荐简短的变量声明,通常来说 如果一个名字的作用域比较大,生命周期也比较长,那么用长的名字将会更有意义。比如在一个很小的循环里面的变量声明 i 比 loopindex更加好。
make函数创造的都是引用,所以进行传递时,直接更改的是原数据
当前变量覆盖外层变量
类型推断
简短变量说明,可以用于声明和初始化局部变量。语法名字:= 表达式
。变量类型更具表达式来自动推荐。
类型推断在一定情况下,属于动态类型,其类型是可以改变的,所以也会带来一些动态类型编程语言所带来的一部分优势,即程序灵活性的明显提升。Go 语言的类型推断可以明显提升程序的灵活性,使得代码重构变得更加容易,同时又不会给代码的维护带来额外负担(实际上,它恰恰可以避免散弹式的代码修改),更不会损失程序的运行效率。
我们通常把不改变某个程序与外界的任何交互方式和规则,而只改变其内部实现”的代码修改方式,叫做对该程序的重构。重构的对象可以是一行代码、一个函数、一个功能模块,甚至一个软件系统。
例如以下代码
package main
import (
"flag"
"fmt"
)
func main() {
var name = getTheFlag()
fmt.Printf("Hello, %v!\n", *name)
}
func getTheFlag() *string {
return flag.String("name", "everyone", "The greeting object.")
}
可以随意改变getTheFlag函数的内部实现,及其返回结果的类型,而不用修改main函数中的任何代码。
表达式new(T)将创建 一个类型为T的匿名对象,初始化为T类型的零值,然后返回变量地址,返回的指针为*T。
p := new(int)
fmt.Println(*p)
*p = 2
fmt.Println(*p)
变量的分配
对于在包一级生命的变量来说,他们的生命周期和整个程序的运行周期是一致的。
编译器会自动选择出在栈上还是堆上分配局部变量的存储空间。
var global *int
func f(){
var x int
x = 1
global = &x
}
func g(){
y := new(int)
*y = 1
}
f函数里的x必须分配在堆上。因为它在函数退出后依然可以通过包一级的变量global找到。这叫做x局部变量从函数f中逃逸了。当g()函数返回时*y将是不可达的,也就是说可以马上回收。一次编译器可以选择在栈上分配 *y的存储空间(也可以选择在堆上)。
对于每一个类型T,都有一个对应的类型转换操作T(x),用于将x转为T类型。只有当2个类型的底层基础类型相同时,才允许这种转型操作,或者是两者都是指向相同底层结构的指针类型,这些转换只改变类型而不会影响值本身。底层数据类型决定了内部结构和表达方式,也决定是否可以像底层类型一样对内置运算符的支持。例如下面的Tempx和Tempy
type Tempx int
type Tempy int
const (
tempx Tempx = 4
tempy Tempy = 4
)
func CastToTempy() Tempy{
return Tempy(tempx/2)
}
func IsSimple() bool{
return Tempy(tempx) == tempy
}