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
}