I'm having trouble creating an iterative version of a program I wrote recursively in GoLang. The goal is to take a directory path and return a JSON tree that contains file information from that directory and preserves the directory structure. Here is what I have so far:
I've created a File struct that will contain the information of each entry in the directory tree:
type File struct {
ModifiedTime time.Time `json:"ModifiedTime"`
IsLink bool `json:"IsLink"`
IsDir bool `json:"IsDir"`
LinksTo string `json:"LinksTo"`
Size int64 `json:"Size"`
Name string `json:"Name"`
Path string `json:"Path"`
Children []File `json:"Children"`
}
In my iterative program, I create a stack to simulate the recursive calls.
func iterateJSON(path string) {
var stack []File
var child File
var file File
rootOSFile, _ := os.Stat(path)
rootFile := toFile(rootOSFile, path) //start with root file
stack = append(stack, rootFile) //append root to stack
for len(stack) > 0 { //until stack is empty,
file = stack[len(stack)-1] //pop entry from stack
stack = stack[:len(stack)-1]
children, _ := ioutil.ReadDir(file.Path) //get the children of entry
for i := 0; i < len(children); i++ { //for each child
child = (toFile(children[i], path+"/"+children[i].Name())) //turn it into a File object
file.Children = append(file.Children, child) //append it to the children of the current file popped
stack = append(stack, child) //append the child to the stack, so the same process can be run again
}
}
rootFile.Children
output, _ := json.MarshalIndent(rootFile, "", " ")
fmt.Println(string(output))
}
func toFile(file os.FileInfo, path string) File {
var isLink bool
var linksTo string
if file.Mode()&os.ModeSymlink == os.ModeSymlink {
isLink = true
linksTo, _ = filepath.EvalSymlinks(path + "/" + file.Name())
} else {
isLink = false
linksTo = ""
}
JSONFile := File{ModifiedTime: file.ModTime(),
IsDir: file.IsDir(),
IsLink: isLink,
LinksTo: linksTo,
Size: file.Size(),
Name: file.Name(),
Path: path,
Children: []File{}}
return JSONFile
}
Theoretically, the child files should be appended to the root file as we move through the stack. However, the only thing that is returned is the root file (without any children appended). Any idea as to why this is happening?
The main problem is that structs are not descriptor values like slices or maps, that is if you assign a struct value to a variable, it will be copied. If you assign a struct value to an element of a slice or array, the slice will be copied. They will not be linked!
So when you add your rootFile
to stack
, and then you pop an element from the stack
(which will be equal to rootFile
) and you modify the popped element, you will not observe the changes in your local variable rootFile
.
Solution is simple: use pointers to structs.
You also have a mistake in your code:
child = (toFile(children[i], path+"/"+children[i].Name())) //turn it into a File object
It should be:
child = (toFile(children[i], file.Path+"/"+children[i].Name())) // ...
I would rather use path.Join()
or filepath.Join()
to join path elements:
child = toFile(children[i], filepath.Join(file.Path, children[i].Name()))
Your code might not even work if the initial path ends with a slash or backslash and you explicitly concatenate it with another slash. Join()
will take care of these so you don't have to.
Don't declare all local variables ahead in the beginning of your function, only when you need them, and in the most inner block you need them. This will ensure you don't accidentally assign to the wrong variable, and you will know it is not modified outside of the innermost block (because outside of it it is not in scope) - this helps understanding your code much easier. You may also use short variable declaration.
Make use of the for ... range
construct, much cleaner. For example:
for _, chld := range children {
child := toFile(chld, filepath.Join(file.Path, chld.Name()))
file.Children = append(file.Children, child)
stack = append(stack, child)
}
Also make use of zero values, for example if a file is not a link, you don't need to set the IsLink
and LinksTo
fields as the zero values are false
and ""
which is what you would end up with.
And although it may not be important here, but always handle errors, print or log them as a minimum so you won't end up wasting time figuring out what is wrong if something is not what you expect (you will end up searching bugs in your code, and hours later you finally add print errors and see the bug wasn't in your code but somewhere else).
type File struct {
ModifiedTime time.Time `json:"ModifiedTime"`
IsLink bool `json:"IsLink"`
IsDir bool `json:"IsDir"`
LinksTo string `json:"LinksTo"`
Size int64 `json:"Size"`
Name string `json:"Name"`
Path string `json:"Path"`
Children []*File `json:"Children"`
}
func iterateJSON(path string) {
rootOSFile, _ := os.Stat(path)
rootFile := toFile(rootOSFile, path) //start with root file
stack := []*File{rootFile}
for len(stack) > 0 { //until stack is empty,
file := stack[len(stack)-1] //pop entry from stack
stack = stack[:len(stack)-1]
children, _ := ioutil.ReadDir(file.Path) //get the children of entry
for _, chld := range children { //for each child
child := toFile(chld, filepath.Join(file.Path, chld.Name())) //turn it into a File object
file.Children = append(file.Children, child) //append it to the children of the current file popped
stack = append(stack, child) //append the child to the stack, so the same process can be run again
}
}
output, _ := json.MarshalIndent(rootFile, "", " ")
fmt.Println(string(output))
}
func toFile(file os.FileInfo, path string) *File {
JSONFile := File{ModifiedTime: file.ModTime(),
IsDir: file.IsDir(),
Size: file.Size(),
Name: file.Name(),
Path: path,
Children: []*File{},
}
if file.Mode()&os.ModeSymlink == os.ModeSymlink {
JSONFile.IsLink = true
JSONFile.LinksTo, _ = filepath.EvalSymlinks(filepath.Join(path, file.Name()))
} // Else case is the zero values of the fields
return &JSONFile
}