This is 3rd post of my Go lang experiment, if you are want to read about earlier post then go to
is-it-worth-learning-golang
what-are-golang-types
Struct are cool types, it allows to create user defined type.
Struct basic
Struct can be declared like this
type person struct {
firstName string
lastName string
}
this declares struct with 2 fields.
Struct variables can be declared like this
var p1 person
var construct will initialized p1 to Zero value, so both the string fields are set to "".
DOT (.) construct is used to access field.
How to define struct variables.
Couple of ways by which variable can be created.
var p1 person // Zero value
var p2 = person{} //Zero value
p3 := person{firstName: "James", lastName: "Bond"} //Proper initialization
p4 := person{firstName: "James"} //Partial initialization
p5 := new(person) // Using new operator , this returns pointer
p5.firstName = "James"
p5.lastName = "Bond"
Struct comparison
Same type of struct can be compared using "==" operator.
p1 := person{firstName: "James", lastName: "Bond"}
p2 := person{firstName: "James", lastName: "Bond"}
if p1 == p2 {
fmt.Println("Same person found!!!!", p1)
} else {
fmt.Println("They are different", p1, p2)
}
this shows power of pure value, no equals/hashcode type of things are required to compare, language has first class support to compare by value.
Struct conversion
Go lang does not have casting, it is supports conversion and it is applicable to any types not just struct.
Casting keep source object reference and put target object struct/layout on top of it, so in casting any changes done to source object after casting is visible to target object.
This is good for reducing memory overhead but for safety this can cause big problem because values can change magically from source object.
On other end conversion copies source value, so after conversion both source and target have no link, changing one does not impact other one. This is good for type safety and easy to reason about code.
Lets look into some conversion example of struct.
type person struct {
firstName string
lastName string
}
type anotherperson struct {
firstName string
lastName string
}
Both of the above are same in structure but these two can't be assigned to each other without conversion.
p1 := person{firstName: "James", lastName: "Bond"}
anotherp1 := anotherperson{firstName: "James", lastName: "Bond"}
p1 = anotherp1 //This is compile time error
p1 = person(anotherp1)//This is allowed
Compiler is very smart to figure out that these two types are compatible and conversion is allowed.
Now if go and make change in otherperson struct like drop the field/ new field/change the order then it becomes not compatible and compiler stops this!
When it does allow conversion then it allocate new memory for target variable and copies the value.
For eg
p1 = person(anotherp1)
anotherp1.lastName = "Lee" // Will have not effect on p1
How struct are allocated
Since it is composite type and understanding memory layout of struct is very useful in knowing what type of overhead it comes up.
Current processor will do some cool things for fast & safe read/write.
Memory allocation will be aligned to word size of underlying platform ( 32 bit or 64 bit) and it will be also aligned based on size of the type for eg 4 byte value will be aligned to 4 byte address.
Alignment is very important for speed and correctness.
Lets take example to understand this, in 64 bit platform word size is 64bit or 8 byte, so it will take 1 instruction to read 1 word.
|
Memory Layout |
Value shown in red is 2 byte and if value shown in red is allocated in 2 words(i.e at the boundary of word) then it is going to take multiple operation to read/write value and for write some kind of synchronization might be required.
Since value is only 2 byte, so it can easily fit in single word so compiler will try to allocate this in single word
|
Single word allocation |
Above allocation is optimized for read/write. Struct allocation works on same principle.
Now lets take example of struct and see how what will be memory layout
type layouttest struct {
b byte
v int32
f float64
v2 int32
}
layout of "layoutouttest" will look something like below
[ 1 X X 1 1 1 1 X ][1 1 1 1 X X X X][1 1 1 1 1 1 1 1][1 1 1 1 X X X X]
X - is for padding.
It took 4 word to place this struct and to get the alignment by data type padding is added.
If we calculate size of struct ( 1 + 4 + 4 + 8 = 17) then it should fit value in 3 word( 8*3 = 24) but it took 4 words( 8 * 4 = 32). It might look like 8 bytes are wasted.
Go gives full control to developer about memory layout, much more compact struct can be created to get to 3 word allocation.
type compactlyouttest struct {
f float64
v int32
v2 int32
b byte
}
Above struct has reordered field in descending order by size it takes and this helps in getting to below memory layout
[ 1 1 1 1 1 1 1 1 ][1 1 1 1 1 1 1 1][1 X X X X X X X]
In this arrangement less space is wasted in padding and you might be tempted to use compact representation.
You should should not do this for couple of reason
- This breaks the readability because related fields are moved all over the place.
- Memory might not be issue, so it could be just over optimization.
- Processor are very smart, values are read in cacheline not in word, so CPU will read multiple words and you will never see any slowness in read. You can read about how cache line works in
cpu-cache-access-pattern post.
- Over optimization can result in false sharing issue, read
concurrent-counter-with-no-false-sharing to see impact of false sharing in multi threaded code.
So profile application before doing any optimization.
Go has built in packages for getting memory alignment details & other static information of types.
Below code gives lot of details about memory layout
unsafe & reflect package gives lot of internal details and looks like idea has come from java
Code used in this blog is available @
001-struct github